diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index 08622df281..ce4f37873b 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -7,8 +7,12 @@ import 'dart:io' as io; import 'dart:math'; import 'package:args/command_runner.dart'; +import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; import 'package:yaml/yaml.dart'; typedef void Print(Object object); @@ -140,6 +144,13 @@ bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { return pluginSupportsPlatform(kLinux, entity, fileSystem); } +/// Throws a [ToolExit] with `exitCode` and log the `errorMessage` in red. +void printErrorAndExit({@required String errorMessage, int exitCode = 1}) { + final Colorize redError = Colorize(errorMessage)..red(); + print(redError); + throw ToolExit(exitCode); +} + /// Error thrown when a command needs to exit with a non-zero exit code. class ToolExit extends Error { ToolExit(this.exitCode); @@ -152,6 +163,7 @@ abstract class PluginCommand extends Command { this.packagesDir, this.fileSystem, { this.processRunner = const ProcessRunner(), + this.gitDir, }) { argParser.addMultiOption( _pluginsArg, @@ -179,12 +191,23 @@ abstract class PluginCommand extends Command { help: 'Exclude packages from this command.', defaultsTo: [], ); + argParser.addFlag(_runOnChangedPackagesArg, + help: 'Run the command on changed packages/plugins.\n' + 'If the $_pluginsArg is specified, this flag is ignored.\n' + 'The packages excluded with $_excludeArg is also excluded even if changed.\n' + 'See $_kBaseSha if a custom base is needed to determine the diff.'); + argParser.addOption(_kBaseSha, + help: 'The base sha used to determine git diff. \n' + 'This is useful when $_runOnChangedPackagesArg is specified.\n' + 'If not specified, merge-base is used as base sha.'); } static const String _pluginsArg = 'plugins'; static const String _shardIndexArg = 'shardIndex'; static const String _shardCountArg = 'shardCount'; static const String _excludeArg = 'exclude'; + static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _kBaseSha = 'base-sha'; /// The directory containing the plugin packages. final Directory packagesDir; @@ -199,6 +222,11 @@ abstract class PluginCommand extends Command { /// This can be overridden for testing. final ProcessRunner processRunner; + /// The git directory to use. By default it uses the parent directory. + /// + /// This can be mocked for testing. + final GitDir gitDir; + int _shardIndex; int _shardCount; @@ -273,9 +301,13 @@ abstract class PluginCommand extends Command { /// "client library" package, which declares the API for the plugin, as /// well as one or more platform-specific implementations. Stream _getAllPlugins() async* { - final Set plugins = Set.from(argResults[_pluginsArg]); + Set plugins = Set.from(argResults[_pluginsArg]); final Set excludedPlugins = Set.from(argResults[_excludeArg]); + final bool runOnChangedPackages = argResults[_runOnChangedPackagesArg]; + if (plugins.isEmpty && runOnChangedPackages) { + plugins = await _getChangedPackages(); + } await for (FileSystemEntity entity in packagesDir.list(followLinks: false)) { @@ -363,6 +395,50 @@ abstract class PluginCommand extends Command { (FileSystemEntity entity) => isFlutterPackage(entity, fileSystem)) .cast(); } + + /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir]. + /// + /// Throws tool exit if [gitDir] nor root directory is a git directory. + Future retrieveVersionFinder() async { + final String rootDir = packagesDir.parent.absolute.path; + String baseSha = argResults[_kBaseSha]; + + GitDir baseGitDir = gitDir; + if (baseGitDir == null) { + if (!await GitDir.isGitDir(rootDir)) { + printErrorAndExit( + errorMessage: '$rootDir is not a valid Git repository.', + exitCode: 2); + } + baseGitDir = await GitDir.fromExisting(rootDir); + } + + final GitVersionFinder gitVersionFinder = + GitVersionFinder(baseGitDir, baseSha); + return gitVersionFinder; + } + + Future> _getChangedPackages() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + + final List allChangedFiles = + await gitVersionFinder.getChangedFiles(); + final Set packages = {}; + allChangedFiles.forEach((String path) { + final List pathComponents = path.split('/'); + final int packagesIndex = + pathComponents.indexWhere((String element) => element == 'packages'); + if (packagesIndex != -1) { + packages.add(pathComponents[packagesIndex + 1]); + } + }); + if (packages.isNotEmpty) { + final String changedPackages = packages.join(','); + print(changedPackages); + } + print('No changed packages.'); + return packages; + } } /// A class used to run processes. @@ -466,3 +542,68 @@ class ProcessRunner { return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; } } + +/// Finding diffs based on `baseGitDir` and `baseSha`. +class GitVersionFinder { + /// Constructor + GitVersionFinder(this.baseGitDir, this.baseSha); + + /// The top level directory of the git repo. + /// + /// That is where the .git/ folder exists. + final GitDir baseGitDir; + + /// The base sha used to get diff. + final String baseSha; + + static bool _isPubspec(String file) { + return file.trim().endsWith('pubspec.yaml'); + } + + /// Get a list of all the pubspec.yaml file that is changed. + Future> getChangedPubSpecs() async { + return (await getChangedFiles()).where(_isPubspec).toList(); + } + + /// Get a list of all the changed files. + Future> getChangedFiles() async { + final String baseSha = await _getBaseSha(); + final io.ProcessResult changedFilesCommand = await baseGitDir + .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); + print('Determine diff with base sha: $baseSha'); + final String changedFilesStdout = changedFilesCommand.stdout.toString() ?? ''; + if (changedFilesStdout.isEmpty) { + return []; + } + final List changedFiles = changedFilesStdout + .split('\n') + ..removeWhere((element) => element.isEmpty); + return changedFiles.toList(); + } + + /// Get the package version specified in the pubspec file in `pubspecPath` and at the revision of `gitRef`. + Future getPackageVersion(String pubspecPath, String gitRef) async { + final io.ProcessResult gitShow = + await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); + final String fileContent = gitShow.stdout; + final String versionString = loadYaml(fileContent)['version']; + return versionString == null ? null : Version.parse(versionString); + } + + Future _getBaseSha() async { + if (baseSha != null && baseSha.isNotEmpty) { + return baseSha; + } + + io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand( + ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], + throwOnError: false); + if (baseShaFromMergeBase == null || + baseShaFromMergeBase.stderr != null || + baseShaFromMergeBase.stdout == null) { + baseShaFromMergeBase = await baseGitDir + .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); + } + return (baseShaFromMergeBase.stdout as String).trim(); + } +} diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 2c6b92bbcb..111239f039 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -6,43 +6,14 @@ import 'dart:async'; import 'dart:io' as io; import 'package:meta/meta.dart'; -import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'package:yaml/yaml.dart'; import 'common.dart'; -const String _kBaseSha = 'base_sha'; - -class GitVersionFinder { - GitVersionFinder(this.baseGitDir, this.baseSha); - - final GitDir baseGitDir; - final String baseSha; - - static bool isPubspec(String file) { - return file.trim().endsWith('pubspec.yaml'); - } - - Future> getChangedPubSpecs() async { - final io.ProcessResult changedFilesCommand = await baseGitDir - .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); - final List changedFiles = - changedFilesCommand.stdout.toString().split('\n'); - return changedFiles.where(isPubspec).toList(); - } - - Future getPackageVersion(String pubspecPath, String gitRef) async { - final io.ProcessResult gitShow = - await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); - final String fileContent = gitShow.stdout; - final String versionString = loadYaml(fileContent)['version']; - return versionString == null ? null : Version.parse(versionString); - } -} +const String _kBaseSha = 'base-sha'; enum NextVersionType { BREAKING_MAJOR, @@ -128,46 +99,28 @@ class VersionCheckCommand extends PluginCommand { Directory packagesDir, FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), - this.gitDir, - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption(_kBaseSha); - } - - /// The git directory to use. By default it uses the parent directory. - /// - /// This can be mocked for testing. - final GitDir gitDir; + GitDir gitDir, + }) : super(packagesDir, fileSystem, + processRunner: processRunner, gitDir: gitDir); @override final String name = 'version-check'; @override final String description = - 'Checks if the versions of the plugins have been incremented per pub specification.\n\n' + 'Checks if the versions of the plugins have been incremented per pub specification.\n' + 'Also checks if the latest version in CHANGELOG matches the version in pubspec.\n\n' 'This command requires "pub" and "flutter" to be in your path.'; @override Future run() async { checkSharding(); - - final String rootDir = packagesDir.parent.absolute.path; - final String baseSha = argResults[_kBaseSha]; - - GitDir baseGitDir = gitDir; - if (baseGitDir == null) { - if (!await GitDir.isGitDir(rootDir)) { - print('$rootDir is not a valid Git repository.'); - throw ToolExit(2); - } - baseGitDir = await GitDir.fromExisting(rootDir); - } - - final GitVersionFinder gitVersionFinder = - GitVersionFinder(baseGitDir, baseSha); + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); final List changedPubspecs = await gitVersionFinder.getChangedPubSpecs(); + final String baseSha = argResults[_kBaseSha]; for (final String pubspecPath in changedPubspecs) { try { final File pubspecFile = fileSystem.file(pubspecPath); @@ -194,9 +147,7 @@ class VersionCheckCommand extends PluginCommand { final String error = '$pubspecPath incorrectly updated version.\n' 'HEAD: $headVersion, master: $masterVersion.\n' 'Allowed versions: $allowedNextVersions'; - final Colorize redError = Colorize(error)..red(); - print(redError); - throw ToolExit(1); + printErrorAndExit(errorMessage: error); } bool isPlatformInterface = pubspec.name.endsWith("_platform_interface"); @@ -205,9 +156,7 @@ class VersionCheckCommand extends PluginCommand { NextVersionType.BREAKING_MAJOR) { final String error = '$pubspecPath breaking change detected.\n' 'Breaking changes to platform interfaces are strongly discouraged.\n'; - final Colorize redError = Colorize(error)..red(); - print(redError); - throw ToolExit(1); + printErrorAndExit(errorMessage: error); } } on io.ProcessException { print('Unable to find pubspec in master for $pubspecPath.' @@ -215,6 +164,74 @@ class VersionCheckCommand extends PluginCommand { } } + await for (Directory plugin in getPlugins()) { + await _checkVersionsMatch(plugin); + } + print('No version check errors found!'); } + + Future _checkVersionsMatch(Directory plugin) async { + // get version from pubspec + final String packageName = plugin.basename; + print('-----------------------------------------'); + print( + 'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for $packageName.'); + + final Pubspec pubspec = _tryParsePubspec(plugin); + if (pubspec == null) { + final String error = 'Cannot parse version from pubspec.yaml'; + printErrorAndExit(errorMessage: error); + } + final Version fromPubspec = pubspec.version; + + // get first version from CHANGELOG + final File changelog = plugin.childFile('CHANGELOG.md'); + final List lines = changelog.readAsLinesSync(); + String firstLineWithText; + final Iterator iterator = lines.iterator; + while (iterator.moveNext()) { + if ((iterator.current as String).trim().isNotEmpty) { + firstLineWithText = iterator.current; + break; + } + } + // Remove all leading mark down syntax from the version line. + final String versionString = firstLineWithText.split(' ').last; + Version fromChangeLog = Version.parse(versionString); + if (fromChangeLog == null) { + final String error = + 'Cannot find version on the first line of ${plugin.path}/CHANGELOG.md'; + printErrorAndExit(errorMessage: error); + } + + if (fromPubspec != fromChangeLog) { + final String error = ''' +versions for $packageName in CHANGELOG.md and pubspec.yaml do not match. +The version in pubspec.yaml is $fromPubspec. +The first version listed in CHANGELOG.md is $fromChangeLog. +'''; + printErrorAndExit(errorMessage: error); + } + print('${packageName} passed version check'); + } + + Pubspec _tryParsePubspec(Directory package) { + final File pubspecFile = package.childFile('pubspec.yaml'); + + try { + Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec == null) { + final String error = + 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}'; + printErrorAndExit(errorMessage: error); + } + return pubspec; + } on Exception catch (exception) { + final String error = + 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}'; + printErrorAndExit(errorMessage: error); + } + return null; + } } diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index b3504c2358..0fb3ce74c3 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -1,6 +1,10 @@ +import 'dart:io'; + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:git/git.dart'; +import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import 'util.dart'; @@ -9,8 +13,21 @@ void main() { RecordingProcessRunner processRunner; CommandRunner runner; List plugins; + List> gitDirCommands; + String gitDiffResponse; setUp(() { + gitDirCommands = >[]; + gitDiffResponse = ''; + final MockGitDir gitDir = MockGitDir(); + when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0]); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + } + return Future.value(mockProcessResult); + }); initializeFakePackages(); processRunner = RecordingProcessRunner(); plugins = []; @@ -19,6 +36,7 @@ void main() { mockPackagesDir, mockFileSystem, processRunner: processRunner, + gitDir: gitDir, ); runner = CommandRunner('common_command', 'Test for common functionality'); @@ -73,6 +91,207 @@ void main() { ]); expect(plugins, unorderedEquals([plugin2.path])); }); + + group('test run-on-changed-packages', () { + test('all plugins should be tested if there are no changes.', () async { + final Directory plugin1 = createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if there are no plugin related changes.', + () async { + gitDiffResponse = ".cirrus"; + final Directory plugin1 = createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('Only changed plugin should be tested.', () async { + gitDiffResponse = "packages/plugin1/plugin1.dart"; + final Directory plugin1 = createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + + test('multiple files in one plugin should also test the plugin', () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin1/ios/plugin1.m +'''; + final Directory plugin1 = createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + + test('multiple plugins changed should test all the changed plugins', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin2/ios/plugin2.m +'''; + final Directory plugin1 = createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + createFakePlugin('plugin3'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test( + 'multiple plugins inside the same plugin group changed should output the plugin group name', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1/plugin1.dart +packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart +packages/plugin1/plugin1_web/plugin1_web.dart +'''; + final Directory plugin1 = + createFakePlugin('plugin1', parentDirectoryName: 'plugin1'); + createFakePlugin('plugin2'); + createFakePlugin('plugin3'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + + test('--plugins flag overrides the behavior of --run-on-changed-packages', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin2/ios/plugin2.m +packages/plugin3/plugin3.dart +'''; + final Directory plugin1 = + createFakePlugin('plugin1', parentDirectoryName: 'plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + createFakePlugin('plugin3'); + await runner.run([ + 'sample', + '--plugins=plugin1,plugin2', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('--exclude flag works with --run-on-changed-packages', () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin2/ios/plugin2.m +packages/plugin3/plugin3.dart +'''; + final Directory plugin1 = + createFakePlugin('plugin1', parentDirectoryName: 'plugin1'); + createFakePlugin('plugin2'); + createFakePlugin('plugin3'); + await runner.run([ + 'sample', + '--exclude=plugin2,plugin3', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + }); + + group('$GitVersionFinder', () { + List> gitDirCommands; + String gitDiffResponse; + String mergeBaseResponse; + MockGitDir gitDir; + + setUp(() { + gitDirCommands = >[]; + gitDiffResponse = ''; + gitDir = MockGitDir(); + when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0]); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + } else if (invocation.positionalArguments[0][0] == 'merge-base') { + when(mockProcessResult.stdout).thenReturn(mergeBaseResponse); + } + return Future.value(mockProcessResult); + }); + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('No git diff should result no files changed', () async { + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + List changedFiles = await finder.getChangedFiles(); + + expect(changedFiles, isEmpty); + }); + + test('get correct files changed based on git diff', () async { + gitDiffResponse = ''' +file1/file1.cc +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + List changedFiles = await finder.getChangedFiles(); + + expect( + changedFiles, equals(['file1/file1.cc', 'file2/file2.cc'])); + }); + + test('get correct pubspec change based on git diff', () async { + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + List changedFiles = await finder.getChangedPubSpecs(); + + expect(changedFiles, equals(['file1/pubspec.yaml'])); + }); + + test('use correct base sha if not specified', () async { + mergeBaseResponse = 'shaqwiueroaaidf12312jnadf123nd'; + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, null); + await finder.getChangedFiles(); + verify(gitDir + .runCommand(['diff', '--name-only', mergeBaseResponse, 'HEAD'])); + }); + + test('use correct base sha if specified', () async { + final String customBaseSha = 'aklsjdcaskf12312'; + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); + await finder.getChangedFiles(); + verify(gitDir.runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); + }); + }); } class SamplePluginCommand extends PluginCommand { @@ -81,7 +300,9 @@ class SamplePluginCommand extends PluginCommand { Directory packagesDir, FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner); + GitDir gitDir, + }) : super(packagesDir, fileSystem, + processRunner: processRunner, gitDir: gitDir); List plugins_; @@ -98,3 +319,7 @@ class SamplePluginCommand extends PluginCommand { } } } + +class MockGitDir extends Mock implements GitDir {} + +class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index ec0000d13f..1538d9b554 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -37,6 +37,8 @@ Directory createFakePlugin( bool isLinuxPlugin = false, bool isMacOsPlugin = false, bool isWindowsPlugin = false, + bool includeChangeLog = false, + bool includeVersion = false, String parentDirectoryName = '', }) { assert(!(withSingleExample && withExamples.isNotEmpty), @@ -57,7 +59,14 @@ Directory createFakePlugin( isLinuxPlugin: isLinuxPlugin, isMacOsPlugin: isMacOsPlugin, isWindowsPlugin: isWindowsPlugin, + includeVersion: includeVersion, ); + if (includeChangeLog) { + createFakeCHANGELOG(pluginDirectory, ''' +## 0.0.1 + * Some changes. + '''); + } if (withSingleExample) { final Directory exampleDir = pluginDirectory.childDirectory('example') @@ -85,6 +94,11 @@ Directory createFakePlugin( return pluginDirectory; } +void createFakeCHANGELOG(Directory parent, String texts) { + parent.childFile('CHANGELOG.md').createSync(); + parent.childFile('CHANGELOG.md').writeAsStringSync(texts); +} + /// Creates a `pubspec.yaml` file with a flutter dependency. void createFakePubspec( Directory parent, { @@ -97,6 +111,7 @@ void createFakePubspec( bool isLinuxPlugin = false, bool isMacOsPlugin = false, bool isWindowsPlugin = false, + String version = '0.0.1', }) { parent.childFile('pubspec.yaml').createSync(); String yaml = ''' @@ -152,8 +167,8 @@ dependencies: } if (includeVersion) { yaml += ''' -version: 0.0.1 -publish_to: none # Hardcoded safeguard to prevent this from somehow being published by a broken test. +version: $version +publish_to: http://no_pub_server.com # Hardcoded safeguard to prevent this from somehow being published by a broken test. '''; } parent.childFile('pubspec.yaml').writeAsStringSync(yaml); diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index b9ace3811b..ac0d378c2a 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:args/command_runner.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; import "package:test/test.dart"; @@ -74,18 +75,18 @@ void main() { }); test('allows valid version', () async { - createFakePlugin('plugin'); + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); gitDiffResponse = "packages/plugin/pubspec.yaml"; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', }; final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); + runner, ['version-check', '--base-sha=master']); expect( output, - orderedEquals([ + containsAllInOrder([ 'No version check errors found!', ]), ); @@ -99,14 +100,14 @@ void main() { }); test('denies invalid version', () async { - createFakePlugin('plugin'); + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); gitDiffResponse = "packages/plugin/pubspec.yaml"; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', }; final Future> result = runCapturingPrint( - runner, ['version-check', '--base_sha=master']); + runner, ['version-check', '--base-sha=master']); await expectLater( result, @@ -122,7 +123,7 @@ void main() { }); test('gracefully handles missing pubspec.yaml', () async { - createFakePlugin('plugin'); + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); gitDiffResponse = "packages/plugin/pubspec.yaml"; mockFileSystem.currentDirectory .childDirectory('packages') @@ -130,11 +131,12 @@ void main() { .childFile('pubspec.yaml') .deleteSync(); final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); + runner, ['version-check', '--base-sha=master']); expect( output, orderedEquals([ + 'Determine diff with base sha: master', 'No version check errors found!', ]), ); @@ -144,7 +146,8 @@ void main() { }); test('allows minor changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface'); + createFakePlugin('plugin_platform_interface', + includeChangeLog: true, includeVersion: true); gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; gitShowResponses = { 'master:packages/plugin_platform_interface/pubspec.yaml': @@ -153,10 +156,10 @@ void main() { 'version: 1.1.0', }; final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); + runner, ['version-check', '--base-sha=master']); expect( output, - orderedEquals([ + containsAllInOrder([ 'No version check errors found!', ]), ); @@ -172,7 +175,8 @@ void main() { }); test('disallows breaking changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface'); + createFakePlugin('plugin_platform_interface', + includeChangeLog: true, includeVersion: true); gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; gitShowResponses = { 'master:packages/plugin_platform_interface/pubspec.yaml': @@ -181,7 +185,7 @@ void main() { 'version: 2.0.0', }; final Future> output = runCapturingPrint( - runner, ['version-check', '--base_sha=master']); + runner, ['version-check', '--base-sha=master']); await expectLater( output, throwsA(const TypeMatcher()), @@ -196,6 +200,138 @@ void main() { expect(gitDirCommands[2].join(' '), equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); }); + + test('Allow empty lines in front of the first version in CHANGELOG', + () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + + final Directory pluginDirectory = + mockPackagesDir.childDirectory('plugin'); + + createFakePubspec(pluginDirectory, + isFlutter: true, includeVersion: true, version: '1.0.1'); + String changelog = ''' + + + +## 1.0.1 + +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expect( + output, + containsAllInOrder([ + 'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for plugin.', + 'plugin passed version check', + 'No version check errors found!' + ]), + ); + }); + + test('Throws if versions in changelog and pubspec do not match', () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + + final Directory pluginDirectory = + mockPackagesDir.childDirectory('plugin'); + + createFakePubspec(pluginDirectory, + isFlutter: true, includeVersion: true, version: '1.0.1'); + String changelog = ''' +## 1.0.2 + +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final Future> output = runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), + ); + try { + List outputValue = await output; + await expectLater( + outputValue, + containsAllInOrder([ + ''' + versions for plugin in CHANGELOG.md and pubspec.yaml do not match. + The version in pubspec.yaml is 1.0.1. + The first version listed in CHANGELOG.md is 1.0.2. + ''', + ]), + ); + } on ToolExit catch (_) {} + }); + + test('Success if CHANGELOG and pubspec versions match', () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + + final Directory pluginDirectory = + mockPackagesDir.childDirectory('plugin'); + + createFakePubspec(pluginDirectory, + isFlutter: true, includeVersion: true, version: '1.0.1'); + String changelog = ''' +## 1.0.1 + +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expect( + output, + containsAllInOrder([ + 'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for plugin.', + 'plugin passed version check', + 'No version check errors found!' + ]), + ); + }); + + test( + 'Fail if pubspec version only matches an older version listed in CHANGELOG', + () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + + final Directory pluginDirectory = + mockPackagesDir.childDirectory('plugin'); + + createFakePubspec(pluginDirectory, + isFlutter: true, includeVersion: true, version: '1.0.0'); + String changelog = ''' +## 1.0.1 + +* Some changes. + +## 1.0.0 + +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + Future> output = runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), + ); + try { + List outputValue = await output; + await expectLater( + outputValue, + containsAllInOrder([ + ''' + versions for plugin in CHANGELOG.md and pubspec.yaml do not match. + The version in pubspec.yaml is 1.0.0. + The first version listed in CHANGELOG.md is 1.0.1. + ''', + ]), + ); + } on ToolExit catch (_) {} + }); }); group("Pre 1.0", () {