[flutter_plugin_tools] Add a new 'make-deps-path-based' command (#4575)
Adds a new command that adds `dependency_overrides` to any packages in the repository that depend on a list of target packages, including an option to target packages that will publish a non-breaking change in a given diff.
Adds a new CI step that uses the above in conjunction with a new `--run-on-dirty-packages` to adjust the dependencies of anything in the repository that uses a to-be-published package and then re-run analysis on just those packages. This will allow us to catch in presubmit any changes that are not breaking from a semver standpoint, but will break us due to our strict analysis in CI.
Fixes https://github.com/flutter/flutter/issues/89862
diff --git a/.cirrus.yml b/.cirrus.yml
index 59f686d..453d01b 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -109,13 +109,25 @@
matrix:
CHANNEL: "master"
CHANNEL: "stable"
- tool_script:
+ analyze_tool_script:
- cd script/tool
- dart analyze --fatal-infos
- script:
+ analyze_script:
# DO NOT change the custom-analysis argument here without changing the Dart repo.
# See the comment in script/configs/custom_analysis.yaml for details.
- ./script/tool_runner.sh analyze --custom-analysis=script/configs/custom_analysis.yaml
+ pathified_analyze_script:
+ # Re-run analysis with path-based dependencies to ensure that publishing
+ # the changes won't break analysis of other packages in the respository
+ # that depend on it.
+ - ./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates
+ # This uses --run-on-dirty-packages rather than --packages-for-branch
+ # since only the packages changed by 'make-deps-path-based' need to be
+ # checked.
+ - dart $PLUGIN_TOOL analyze --run-on-dirty-packages --log-timing --custom-analysis=script/configs/custom_analysis.yaml
+ # Restore the tree to a clean state, to avoid accidental issues if
+ # other script steps are added to this task.
+ - git checkout .
### Android tasks ###
- name: android-build_all_plugins
env:
diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md
index a2a2ed8..234700a 100644
--- a/script/tool/CHANGELOG.md
+++ b/script/tool/CHANGELOG.md
@@ -1,6 +1,10 @@
## NEXT
- Ensures that `firebase-test-lab` runs include an `integration_test` runner.
+- Adds a `make-deps-path-based` command to convert inter-repo package
+ dependencies to path-based dependencies.
+- Adds a (hidden) `--run-on-dirty-packages` flag for use with
+ `make-deps-path-based` in CI.
## 0.7.3
diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart
index 3311873..32d30e6 100644
--- a/script/tool/lib/src/common/git_version_finder.dart
+++ b/script/tool/lib/src/common/git_version_finder.dart
@@ -31,10 +31,16 @@
}
/// Get a list of all the changed files.
- Future<List<String>> getChangedFiles() async {
+ Future<List<String>> getChangedFiles(
+ {bool includeUncommitted = false}) async {
final String baseSha = await getBaseSha();
final io.ProcessResult changedFilesCommand = await baseGitDir
- .runCommand(<String>['diff', '--name-only', baseSha, 'HEAD']);
+ .runCommand(<String>[
+ 'diff',
+ '--name-only',
+ baseSha,
+ if (!includeUncommitted) 'HEAD'
+ ]);
final String changedFilesStdout = changedFilesCommand.stdout.toString();
if (changedFilesStdout.isEmpty) {
return <String>[];
diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart
index f40a102..7166c75 100644
--- a/script/tool/lib/src/common/plugin_command.dart
+++ b/script/tool/lib/src/common/plugin_command.dart
@@ -75,9 +75,15 @@
help: 'Run the command on changed packages/plugins.\n'
'If no packages have changed, or if there have been changes that may\n'
'affect all packages, the command runs on all packages.\n'
- 'The packages excluded with $_excludeArg is also excluded even if changed.\n'
+ 'Packages excluded with $_excludeArg are excluded even if changed.\n'
'See $_baseShaArg if a custom base is needed to determine the diff.\n\n'
'Cannot be combined with $_packagesArg.\n');
+ argParser.addFlag(_runOnDirtyPackagesArg,
+ help:
+ 'Run the command on packages with changes that have not been committed.\n'
+ 'Packages excluded with $_excludeArg are excluded even if changed.\n'
+ 'Cannot be combined with $_packagesArg.\n',
+ hide: true);
argParser.addFlag(_packagesForBranchArg,
help:
'This runs on all packages (equivalent to no package selection flag)\n'
@@ -103,6 +109,7 @@
static const String _packagesForBranchArg = 'packages-for-branch';
static const String _pluginsArg = 'plugins';
static const String _runOnChangedPackagesArg = 'run-on-changed-packages';
+ static const String _runOnDirtyPackagesArg = 'run-on-dirty-packages';
static const String _shardCountArg = 'shardCount';
static const String _shardIndexArg = 'shardIndex';
@@ -289,6 +296,7 @@
final Set<String> packageSelectionFlags = <String>{
_packagesArg,
_runOnChangedPackagesArg,
+ _runOnDirtyPackagesArg,
_packagesForBranchArg,
};
if (packageSelectionFlags
@@ -300,7 +308,7 @@
throw ToolExit(exitInvalidArguments);
}
- Set<String> plugins = Set<String>.from(getStringListArg(_packagesArg));
+ Set<String> packages = Set<String>.from(getStringListArg(_packagesArg));
final bool runOnChangedPackages;
if (getBoolArg(_runOnChangedPackagesArg)) {
@@ -331,7 +339,21 @@
final List<String> changedFiles =
await gitVersionFinder.getChangedFiles();
if (!_changesRequireFullTest(changedFiles)) {
- plugins = _getChangedPackages(changedFiles);
+ packages = _getChangedPackages(changedFiles);
+ }
+ } else if (getBoolArg(_runOnDirtyPackagesArg)) {
+ final GitVersionFinder gitVersionFinder =
+ GitVersionFinder(await gitDir, 'HEAD');
+ print('Running for all packages that have uncommitted changes\n');
+ // _changesRequireFullTest is deliberately not used here, as this flag is
+ // intended for use in CI to re-test packages changed by
+ // 'make-deps-path-based'.
+ packages = _getChangedPackages(
+ await gitVersionFinder.getChangedFiles(includeUncommitted: true));
+ // For the same reason, empty is not treated as "all packages" as it is
+ // for other flags.
+ if (packages.isEmpty) {
+ return;
}
}
@@ -347,7 +369,7 @@
in dir.list(followLinks: false)) {
// A top-level Dart package is a plugin package.
if (_isDartPackage(entity)) {
- if (plugins.isEmpty || plugins.contains(p.basename(entity.path))) {
+ if (packages.isEmpty || packages.contains(p.basename(entity.path))) {
yield PackageEnumerationEntry(
RepositoryPackage(entity as Directory),
excluded: excludedPluginNames.contains(entity.basename));
@@ -364,9 +386,9 @@
path.relative(subdir.path, from: dir.path);
final String packageName = path.basename(subdir.path);
final String basenamePath = path.basename(entity.path);
- if (plugins.isEmpty ||
- plugins.contains(relativePath) ||
- plugins.contains(basenamePath)) {
+ if (packages.isEmpty ||
+ packages.contains(relativePath) ||
+ packages.contains(basenamePath)) {
yield PackageEnumerationEntry(
RepositoryPackage(subdir as Directory),
excluded: excludedPluginNames.contains(basenamePath) ||
diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart
index 3b4417a..e0c4e4a 100644
--- a/script/tool/lib/src/common/repository_package.dart
+++ b/script/tool/lib/src/common/repository_package.dart
@@ -4,6 +4,7 @@
import 'package:file/file.dart';
import 'package:path/path.dart' as p;
+import 'package:pubspec_parse/pubspec_parse.dart';
import 'core.dart';
@@ -47,6 +48,14 @@
/// The package's top-level pubspec.yaml.
File get pubspecFile => directory.childFile('pubspec.yaml');
+ late final Pubspec _parsedPubspec =
+ Pubspec.parse(pubspecFile.readAsStringSync());
+
+ /// Returns the parsed [pubspecFile].
+ ///
+ /// Caches for future use.
+ Pubspec parsePubspec() => _parsedPubspec;
+
/// True if this appears to be a federated plugin package, according to
/// repository conventions.
bool get isFederated =>
diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart
index 5d9b4ed..82f29bd 100644
--- a/script/tool/lib/src/create_all_plugins_app_command.dart
+++ b/script/tool/lib/src/create_all_plugins_app_command.dart
@@ -178,8 +178,7 @@
final RepositoryPackage package = entry.package;
final Directory pluginDirectory = package.directory;
final String pluginName = pluginDirectory.basename;
- final File pubspecFile = package.pubspecFile;
- final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync());
+ final Pubspec pubspec = package.parsePubspec();
if (pubspec.publishTo != 'none') {
pathDependencies[pluginName] = PathDependency(pluginDirectory.path);
diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart
index 70a6ab5..3e8f19b 100644
--- a/script/tool/lib/src/main.dart
+++ b/script/tool/lib/src/main.dart
@@ -20,6 +20,7 @@
import 'lint_android_command.dart';
import 'lint_podspecs_command.dart';
import 'list_command.dart';
+import 'make_deps_path_based_command.dart';
import 'native_test_command.dart';
import 'publish_check_command.dart';
import 'publish_plugin_command.dart';
@@ -58,6 +59,7 @@
..addCommand(LintPodspecsCommand(packagesDir))
..addCommand(ListCommand(packagesDir))
..addCommand(NativeTestCommand(packagesDir))
+ ..addCommand(MakeDepsPathBasedCommand(packagesDir))
..addCommand(PublishCheckCommand(packagesDir))
..addCommand(PublishPluginCommand(packagesDir))
..addCommand(PubspecCheckCommand(packagesDir))
diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart
new file mode 100644
index 0000000..0486963
--- /dev/null
+++ b/script/tool/lib/src/make_deps_path_based_command.dart
@@ -0,0 +1,249 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/file.dart';
+import 'package:flutter_plugin_tools/src/common/repository_package.dart';
+import 'package:git/git.dart';
+import 'package:path/path.dart' as p;
+import 'package:pub_semver/pub_semver.dart';
+import 'package:pubspec_parse/pubspec_parse.dart';
+
+import 'common/core.dart';
+import 'common/git_version_finder.dart';
+import 'common/plugin_command.dart';
+
+const int _exitPackageNotFound = 3;
+const int _exitCannotUpdatePubspec = 4;
+
+/// Converts all dependencies on target packages to path-based dependencies.
+///
+/// This is to allow for pre-publish testing of changes that could affect other
+/// packages in the repository. For instance, this allows for catching cases
+/// where a non-breaking change to a platform interface package of a federated
+/// plugin would cause post-publish analyzer failures in another package of that
+/// plugin.
+class MakeDepsPathBasedCommand extends PluginCommand {
+ /// Creates an instance of the command to convert selected dependencies to
+ /// path-based.
+ MakeDepsPathBasedCommand(
+ Directory packagesDir, {
+ GitDir? gitDir,
+ }) : super(packagesDir, gitDir: gitDir) {
+ argParser.addMultiOption(_targetDependenciesArg,
+ help:
+ 'The names of the packages to convert to path-based dependencies.\n'
+ 'Ignored if --$_targetDependenciesWithNonBreakingUpdatesArg is '
+ 'passed.',
+ valueHelp: 'some_package');
+ argParser.addFlag(
+ _targetDependenciesWithNonBreakingUpdatesArg,
+ help: 'Causes all packages that have non-breaking version changes '
+ 'when compared against the git base to be treated as target '
+ 'packages.',
+ );
+ }
+
+ static const String _targetDependenciesArg = 'target-dependencies';
+ static const String _targetDependenciesWithNonBreakingUpdatesArg =
+ 'target-dependencies-with-non-breaking-updates';
+
+ @override
+ final String name = 'make-deps-path-based';
+
+ @override
+ final String description =
+ 'Converts package dependencies to path-based references.';
+
+ @override
+ Future<void> run() async {
+ final Set<String> targetDependencies =
+ getBoolArg(_targetDependenciesWithNonBreakingUpdatesArg)
+ ? await _getNonBreakingUpdatePackages()
+ : getStringListArg(_targetDependenciesArg).toSet();
+
+ if (targetDependencies.isEmpty) {
+ print('No target dependencies; nothing to do.');
+ return;
+ }
+ print('Rewriting references to: ${targetDependencies.join(', ')}...');
+
+ final Map<String, RepositoryPackage> localDependencyPackages =
+ _findLocalPackages(targetDependencies);
+
+ final String repoRootPath = (await gitDir).path;
+ for (final File pubspec in await _getAllPubspecs()) {
+ if (await _addDependencyOverridesIfNecessary(
+ pubspec, localDependencyPackages)) {
+ // Print the relative path of the changed pubspec.
+ final String displayPath = p.posix.joinAll(path
+ .split(path.relative(pubspec.absolute.path, from: repoRootPath)));
+ print(' Modified $displayPath');
+ }
+ }
+ }
+
+ Map<String, RepositoryPackage> _findLocalPackages(Set<String> packageNames) {
+ final Map<String, RepositoryPackage> targets =
+ <String, RepositoryPackage>{};
+ for (final String packageName in packageNames) {
+ final Directory topLevelCandidate =
+ packagesDir.childDirectory(packageName);
+ // If packages/<packageName>/ exists, then either that directory is the
+ // package, or packages/<packageName>/<packageName>/ exists and is the
+ // package (in the case of a federated plugin).
+ if (topLevelCandidate.existsSync()) {
+ final Directory appFacingCandidate =
+ topLevelCandidate.childDirectory(packageName);
+ targets[packageName] = RepositoryPackage(appFacingCandidate.existsSync()
+ ? appFacingCandidate
+ : topLevelCandidate);
+ continue;
+ }
+ // If there is no packages/<packageName> directory, then either the
+ // packages doesn't exist, or it is a sub-package of a federated plugin.
+ // If it's the latter, it will be a directory whose name is a prefix.
+ for (final FileSystemEntity entity in packagesDir.listSync()) {
+ if (entity is Directory && packageName.startsWith(entity.basename)) {
+ final Directory subPackageCandidate =
+ entity.childDirectory(packageName);
+ if (subPackageCandidate.existsSync()) {
+ targets[packageName] = RepositoryPackage(subPackageCandidate);
+ break;
+ }
+ }
+ }
+
+ if (!targets.containsKey(packageName)) {
+ printError('Unable to find package "$packageName"');
+ throw ToolExit(_exitPackageNotFound);
+ }
+ }
+ return targets;
+ }
+
+ /// If [pubspecFile] has any dependencies on packages in [localDependencies],
+ /// adds dependency_overrides entries to redirect them to the local version
+ /// using path-based dependencies.
+ ///
+ /// Returns true if any changes were made.
+ Future<bool> _addDependencyOverridesIfNecessary(File pubspecFile,
+ Map<String, RepositoryPackage> localDependencies) async {
+ final String pubspecContents = pubspecFile.readAsStringSync();
+ final Pubspec pubspec = Pubspec.parse(pubspecContents);
+ // Fail if there are any dependency overrides already. If support for that
+ // is needed at some point, it can be added, but currently it's not and
+ // relying on that makes the logic here much simpler.
+ if (pubspec.dependencyOverrides.isNotEmpty) {
+ printError(
+ 'Plugins with dependency overrides are not currently supported.');
+ throw ToolExit(_exitCannotUpdatePubspec);
+ }
+
+ final Iterable<String> packagesToOverride = pubspec.dependencies.keys.where(
+ (String packageName) => localDependencies.containsKey(packageName));
+ if (packagesToOverride.isNotEmpty) {
+ final String commonBasePath = packagesDir.path;
+ // Find the relative path to the common base.
+ final int packageDepth = path
+ .split(path.relative(pubspecFile.parent.absolute.path,
+ from: commonBasePath))
+ .length;
+ final List<String> relativeBasePathComponents =
+ List<String>.filled(packageDepth, '..');
+ // This is done via strings rather than by manipulating the Pubspec and
+ // then re-serialiazing so that it's a localized change, rather than
+ // rewriting the whole file (e.g., destroying comments), which could be
+ // more disruptive for local use.
+ String newPubspecContents = pubspecContents +
+ '''
+
+# FOR TESTING ONLY. DO NOT MERGE.
+dependency_overrides:
+''';
+ for (final String packageName in packagesToOverride) {
+ // Find the relative path from the common base to the local package.
+ final List<String> repoRelativePathComponents = path.split(
+ path.relative(localDependencies[packageName]!.directory.path,
+ from: commonBasePath));
+ newPubspecContents += '''
+ $packageName:
+ path: ${p.posix.joinAll(<String>[
+ ...relativeBasePathComponents,
+ ...repoRelativePathComponents,
+ ])}
+''';
+ }
+ pubspecFile.writeAsStringSync(newPubspecContents);
+ return true;
+ }
+ return false;
+ }
+
+ /// Returns all pubspecs anywhere under the packages directory.
+ Future<List<File>> _getAllPubspecs() => packagesDir.parent
+ .list(recursive: true, followLinks: false)
+ .where((FileSystemEntity entity) =>
+ entity is File && p.basename(entity.path) == 'pubspec.yaml')
+ .map((FileSystemEntity file) => file as File)
+ .toList();
+
+ /// Returns all packages that have non-breaking published changes (i.e., a
+ /// minor or bugfix version change) relative to the git comparison base.
+ ///
+ /// Prints status information about what was checked for ease of auditing logs
+ /// in CI.
+ Future<Set<String>> _getNonBreakingUpdatePackages() async {
+ final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
+ final String baseSha = await gitVersionFinder.getBaseSha();
+ print('Finding changed packages relative to "$baseSha"\n');
+
+ final Set<String> changedPackages = <String>{};
+ for (final String path in await gitVersionFinder.getChangedFiles()) {
+ // Git output always uses Posix paths.
+ final List<String> allComponents = p.posix.split(path);
+ // Only pubspec changes are potential publishing events.
+ if (allComponents.last != 'pubspec.yaml' ||
+ allComponents.contains('example')) {
+ continue;
+ }
+ final RepositoryPackage package =
+ RepositoryPackage(packagesDir.fileSystem.file(path).parent);
+ final String packageName = package.parsePubspec().name;
+ if (!await _hasNonBreakingVersionChange(package)) {
+ // Log packages that had pubspec changes but weren't included for ease
+ // of auditing CI.
+ print(' Skipping $packageName; no non-breaking version change.');
+ continue;
+ }
+ changedPackages.add(packageName);
+ }
+ return changedPackages;
+ }
+
+ Future<bool> _hasNonBreakingVersionChange(RepositoryPackage package) async {
+ final Pubspec pubspec = package.parsePubspec();
+ if (pubspec.publishTo == 'none') {
+ return false;
+ }
+
+ final String pubspecGitPath = p.posix.joinAll(path.split(path.relative(
+ package.pubspecFile.absolute.path,
+ from: (await gitDir).path)));
+ final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
+ final Version? previousVersion =
+ await gitVersionFinder.getPackageVersion(pubspecGitPath);
+ if (previousVersion == null) {
+ // The plugin is new, so nothing can be depending on it yet.
+ return false;
+ }
+ final Version newVersion = pubspec.version!;
+ if ((newVersion.major > 0 && newVersion.major != previousVersion.major) ||
+ (newVersion.major == 0 && newVersion.minor != previousVersion.minor)) {
+ // Breaking changes aren't targetted since they won't be picked up
+ // automatically.
+ return false;
+ }
+ return newVersion != previousVersion;
+ }
+}
diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart
index 563e090..8fd96b8 100644
--- a/script/tool/lib/src/publish_check_command.dart
+++ b/script/tool/lib/src/publish_check_command.dart
@@ -123,13 +123,12 @@
}
Pubspec? _tryParsePubspec(RepositoryPackage package) {
- final File pubspecFile = package.pubspecFile;
-
try {
- return Pubspec.parse(pubspecFile.readAsStringSync());
+ return package.parsePubspec();
} on Exception catch (exception) {
print(
- 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}',
+ 'Failed to parse `pubspec.yaml` at ${package.pubspecFile.path}: '
+ '$exception',
);
return null;
}
diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart
index 4fdecf6..28d17a3 100644
--- a/script/tool/lib/src/publish_plugin_command.dart
+++ b/script/tool/lib/src/publish_plugin_command.dart
@@ -217,16 +217,15 @@
/// In cases where a non-null result is returned, that should be returned
/// as the final result for the package, without further processing.
Future<PackageResult?> _checkNeedsRelease(RepositoryPackage package) async {
- final File pubspecFile = package.pubspecFile;
- if (!pubspecFile.existsSync()) {
+ if (!package.pubspecFile.existsSync()) {
logWarning('''
-The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}.
+The pubspec file for ${package.displayName} does not exist, so no publishing will happen.
Safe to ignore if the package is deleted in this commit.
''');
return PackageResult.skip('package deleted');
}
- final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync());
+ final Pubspec pubspec = package.parsePubspec();
if (pubspec.name == 'flutter_plugin_tools') {
// Ignore flutter_plugin_tools package when running publishing through flutter_plugin_tools.
diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart
index 1ec5dc4..fcaea33 100644
--- a/script/tool/lib/src/version_check_command.dart
+++ b/script/tool/lib/src/version_check_command.dart
@@ -463,10 +463,8 @@
}
Pubspec? _tryParsePubspec(RepositoryPackage package) {
- final File pubspecFile = package.pubspecFile;
-
try {
- final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync());
+ final Pubspec pubspec = package.parsePubspec();
return pubspec;
} on Exception catch (exception) {
printError('${indentation}Failed to parse `pubspec.yaml`: $exception}');
diff --git a/script/tool/test/common/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart
index fa8b1c4..ad1a26f 100644
--- a/script/tool/test/common/git_version_finder_test.dart
+++ b/script/tool/test/common/git_version_finder_test.dart
@@ -88,6 +88,18 @@
verify(gitDir
.runCommand(<String>['diff', '--name-only', customBaseSha, 'HEAD']));
});
+
+ test('include uncommitted files if requested', () async {
+ const String customBaseSha = 'aklsjdcaskf12312';
+ gitDiffResponse = '''
+file1/pubspec.yaml
+file2/file2.cc
+''';
+ final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha);
+ await finder.getChangedFiles(includeUncommitted: true);
+ // The call should not have HEAD as a final argument like the default diff.
+ verify(gitDir.runCommand(<String>['diff', '--name-only', customBaseSha]));
+ });
}
class MockProcessResult extends Mock implements ProcessResult {}
diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart
index 6d586e4..222df54 100644
--- a/script/tool/test/common/plugin_command_test.dart
+++ b/script/tool/test/common/plugin_command_test.dart
@@ -486,6 +486,104 @@
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
});
});
+
+ group('test run-on-dirty-packages', () {
+ test('no packages should be tested if there are no changes.', () async {
+ createFakePackage('a_package', packagesDir);
+ await runCapturingPrint(
+ runner, <String>['sample', '--run-on-dirty-packages']);
+
+ expect(command.plugins, unorderedEquals(<String>[]));
+ });
+
+ test(
+ 'no packages should be tested if there are no plugin related changes.',
+ () async {
+ processRunner.mockProcessesForExecutable['git-diff'] = <Process>[
+ MockProcess(stdout: 'AUTHORS'),
+ ];
+ createFakePackage('a_package', packagesDir);
+ await runCapturingPrint(
+ runner, <String>['sample', '--run-on-dirty-packages']);
+
+ expect(command.plugins, unorderedEquals(<String>[]));
+ });
+
+ test('no packages should be tested even if special repo files change.',
+ () async {
+ processRunner.mockProcessesForExecutable['git-diff'] = <Process>[
+ MockProcess(stdout: '''
+.cirrus.yml
+.ci.yaml
+.ci/Dockerfile
+.clang-format
+analysis_options.yaml
+script/tool_runner.sh
+'''),
+ ];
+ createFakePackage('a_package', packagesDir);
+ await runCapturingPrint(
+ runner, <String>['sample', '--run-on-dirty-packages']);
+
+ expect(command.plugins, unorderedEquals(<String>[]));
+ });
+
+ test('Only changed packages should be tested.', () async {
+ processRunner.mockProcessesForExecutable['git-diff'] = <Process>[
+ MockProcess(stdout: 'packages/a_package/lib/a_package.dart'),
+ ];
+ final Directory packageA = createFakePackage('a_package', packagesDir);
+ createFakePlugin('b_package', packagesDir);
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['sample', '--run-on-dirty-packages']);
+
+ expect(
+ output,
+ containsAllInOrder(<Matcher>[
+ contains(
+ 'Running for all packages that have uncommitted changes'),
+ ]));
+
+ expect(command.plugins, unorderedEquals(<String>[packageA.path]));
+ });
+
+ test('multiple packages changed should test all the changed packages',
+ () async {
+ processRunner.mockProcessesForExecutable['git-diff'] = <Process>[
+ MockProcess(stdout: '''
+packages/a_package/lib/a_package.dart
+packages/b_package/lib/src/foo.dart
+'''),
+ ];
+ final Directory packageA = createFakePackage('a_package', packagesDir);
+ final Directory packageB = createFakePackage('b_package', packagesDir);
+ createFakePackage('c_package', packagesDir);
+ await runCapturingPrint(
+ runner, <String>['sample', '--run-on-dirty-packages']);
+
+ expect(command.plugins,
+ unorderedEquals(<String>[packageA.path, packageB.path]));
+ });
+
+ test('honors --exclude flag', () async {
+ processRunner.mockProcessesForExecutable['git-diff'] = <Process>[
+ MockProcess(stdout: '''
+packages/a_package/lib/a_package.dart
+packages/b_package/lib/src/foo.dart
+'''),
+ ];
+ final Directory packageA = createFakePackage('a_package', packagesDir);
+ createFakePackage('b_package', packagesDir);
+ createFakePackage('c_package', packagesDir);
+ await runCapturingPrint(runner, <String>[
+ 'sample',
+ '--exclude=b_package',
+ '--run-on-dirty-packages'
+ ]);
+
+ expect(command.plugins, unorderedEquals(<String>[packageA.path]));
+ });
+ });
});
group('--packages-for-branch', () {
diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart
index 4c20389..29e3b58 100644
--- a/script/tool/test/common/repository_package_test.dart
+++ b/script/tool/test/common/repository_package_test.dart
@@ -5,6 +5,7 @@
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_plugin_tools/src/common/repository_package.dart';
+import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:test/test.dart';
import '../util.dart';
@@ -155,4 +156,23 @@
expect(RepositoryPackage(plugin).isPlatformImplementation, true);
});
});
+
+ group('pubspec', () {
+ test('file', () async {
+ final Directory plugin = createFakePlugin('a_plugin', packagesDir);
+
+ final File pubspecFile = RepositoryPackage(plugin).pubspecFile;
+
+ expect(pubspecFile.path, plugin.childFile('pubspec.yaml').path);
+ });
+
+ test('parsing', () async {
+ final Directory plugin = createFakePlugin('a_plugin', packagesDir,
+ examples: <String>['example1', 'example2']);
+
+ final Pubspec pubspec = RepositoryPackage(plugin).parsePubspec();
+
+ expect(pubspec.name, 'a_plugin');
+ });
+ });
}
diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart
new file mode 100644
index 0000000..29e4f72
--- /dev/null
+++ b/script/tool/test/make_deps_path_based_command_test.dart
@@ -0,0 +1,304 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io' as io;
+
+import 'package:args/command_runner.dart';
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_plugin_tools/src/common/repository_package.dart';
+import 'package:flutter_plugin_tools/src/make_deps_path_based_command.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test/test.dart';
+
+import 'common/plugin_command_test.mocks.dart';
+import 'mocks.dart';
+import 'util.dart';
+
+void main() {
+ FileSystem fileSystem;
+ late Directory packagesDir;
+ late CommandRunner<void> runner;
+ late RecordingProcessRunner processRunner;
+
+ setUp(() {
+ fileSystem = MemoryFileSystem();
+ packagesDir = createPackagesDirectory(fileSystem: fileSystem);
+
+ final MockGitDir gitDir = MockGitDir();
+ when(gitDir.path).thenReturn(packagesDir.parent.path);
+ when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError')))
+ .thenAnswer((Invocation invocation) {
+ final List<String> arguments =
+ invocation.positionalArguments[0]! as List<String>;
+ // Route git calls through the process runner, to make mock output
+ // consistent with other processes. Attach the first argument to the
+ // command to make targeting the mock results easier.
+ final String gitCommand = arguments.removeAt(0);
+ return processRunner.run('git-$gitCommand', arguments);
+ });
+
+ processRunner = RecordingProcessRunner();
+ final MakeDepsPathBasedCommand command =
+ MakeDepsPathBasedCommand(packagesDir, gitDir: gitDir);
+
+ runner = CommandRunner<void>(
+ 'make-deps-path-based_command', 'Test for $MakeDepsPathBasedCommand');
+ runner.addCommand(command);
+ });
+
+ /// Adds dummy 'dependencies:' entries for each package in [dependencies]
+ /// to [package].
+ void _addDependencies(
+ RepositoryPackage package, Iterable<String> dependencies) {
+ final List<String> lines = package.pubspecFile.readAsLinesSync();
+ final int dependenciesStartIndex = lines.indexOf('dependencies:');
+ assert(dependenciesStartIndex != -1);
+ lines.insertAll(dependenciesStartIndex + 1, <String>[
+ for (final String dependency in dependencies) ' $dependency: ^1.0.0',
+ ]);
+ package.pubspecFile.writeAsStringSync(lines.join('\n'));
+ }
+
+ test('no-ops for no plugins', () async {
+ RepositoryPackage(createFakePackage('foo', packagesDir, isFlutter: true));
+ final RepositoryPackage packageBar = RepositoryPackage(
+ createFakePackage('bar', packagesDir, isFlutter: true));
+ _addDependencies(packageBar, <String>['foo']);
+ final String originalPubspecContents =
+ packageBar.pubspecFile.readAsStringSync();
+
+ final List<String> output =
+ await runCapturingPrint(runner, <String>['make-deps-path-based']);
+
+ expect(
+ output,
+ containsAllInOrder(<Matcher>[
+ contains('No target dependencies'),
+ ]),
+ );
+ // The 'foo' reference should not have been modified.
+ expect(packageBar.pubspecFile.readAsStringSync(), originalPubspecContents);
+ });
+
+ test('rewrites references', () async {
+ final RepositoryPackage simplePackage = RepositoryPackage(
+ createFakePackage('foo', packagesDir, isFlutter: true));
+ final Directory pluginGroup = packagesDir.childDirectory('bar');
+
+ RepositoryPackage(createFakePackage('bar_platform_interface', pluginGroup,
+ isFlutter: true));
+ final RepositoryPackage pluginImplementation =
+ RepositoryPackage(createFakePlugin('bar_android', pluginGroup));
+ final RepositoryPackage pluginAppFacing =
+ RepositoryPackage(createFakePlugin('bar', pluginGroup));
+
+ _addDependencies(simplePackage, <String>[
+ 'bar',
+ 'bar_android',
+ 'bar_platform_interface',
+ ]);
+ _addDependencies(pluginAppFacing, <String>[
+ 'bar_platform_interface',
+ 'bar_android',
+ ]);
+ _addDependencies(pluginImplementation, <String>[
+ 'bar_platform_interface',
+ ]);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'make-deps-path-based',
+ '--target-dependencies=bar,bar_platform_interface'
+ ]);
+
+ expect(
+ output,
+ containsAll(<String>[
+ 'Rewriting references to: bar, bar_platform_interface...',
+ ' Modified packages/bar/bar/pubspec.yaml',
+ ' Modified packages/bar/bar_android/pubspec.yaml',
+ ' Modified packages/foo/pubspec.yaml',
+ ]));
+ expect(
+ output,
+ isNot(contains(
+ ' Modified packages/bar/bar_platform_interface/pubspec.yaml')));
+
+ expect(
+ simplePackage.pubspecFile.readAsLinesSync(),
+ containsAllInOrder(<String>[
+ '# FOR TESTING ONLY. DO NOT MERGE.',
+ 'dependency_overrides:',
+ ' bar:',
+ ' path: ../bar/bar',
+ ' bar_platform_interface:',
+ ' path: ../bar/bar_platform_interface',
+ ]));
+ expect(
+ pluginAppFacing.pubspecFile.readAsLinesSync(),
+ containsAllInOrder(<String>[
+ 'dependency_overrides:',
+ ' bar_platform_interface:',
+ ' path: ../../bar/bar_platform_interface',
+ ]));
+ });
+
+ group('target-dependencies-with-non-breaking-updates', () {
+ test('no-ops for no published changes', () async {
+ final Directory package = createFakePackage('foo', packagesDir);
+
+ final String changedFileOutput = <File>[
+ package.childFile('pubspec.yaml'),
+ ].map((File file) => file.path).join('\n');
+ processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[
+ MockProcess(stdout: changedFileOutput),
+ ];
+ // Simulate no change to the version in the interface's pubspec.yaml.
+ processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[
+ MockProcess(
+ stdout: RepositoryPackage(package).pubspecFile.readAsStringSync()),
+ ];
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'make-deps-path-based',
+ '--target-dependencies-with-non-breaking-updates'
+ ]);
+
+ expect(
+ output,
+ containsAllInOrder(<Matcher>[
+ contains('No target dependencies'),
+ ]),
+ );
+ });
+
+ test('includes bugfix version changes as targets', () async {
+ const String newVersion = '1.0.1';
+ final Directory package =
+ createFakePackage('foo', packagesDir, version: newVersion);
+
+ final File pubspecFile = RepositoryPackage(package).pubspecFile;
+ final String changedFileOutput = <File>[
+ pubspecFile,
+ ].map((File file) => file.path).join('\n');
+ processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[
+ MockProcess(stdout: changedFileOutput),
+ ];
+ final String gitPubspecContents =
+ pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0');
+ // Simulate no change to the version in the interface's pubspec.yaml.
+ processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[
+ MockProcess(stdout: gitPubspecContents),
+ ];
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'make-deps-path-based',
+ '--target-dependencies-with-non-breaking-updates'
+ ]);
+
+ expect(
+ output,
+ containsAllInOrder(<Matcher>[
+ contains('Rewriting references to: foo...'),
+ ]),
+ );
+ });
+
+ test('includes minor version changes to 1.0+ as targets', () async {
+ const String newVersion = '1.1.0';
+ final Directory package =
+ createFakePackage('foo', packagesDir, version: newVersion);
+
+ final File pubspecFile = RepositoryPackage(package).pubspecFile;
+ final String changedFileOutput = <File>[
+ pubspecFile,
+ ].map((File file) => file.path).join('\n');
+ processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[
+ MockProcess(stdout: changedFileOutput),
+ ];
+ final String gitPubspecContents =
+ pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0');
+ // Simulate no change to the version in the interface's pubspec.yaml.
+ processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[
+ MockProcess(stdout: gitPubspecContents),
+ ];
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'make-deps-path-based',
+ '--target-dependencies-with-non-breaking-updates'
+ ]);
+
+ expect(
+ output,
+ containsAllInOrder(<Matcher>[
+ contains('Rewriting references to: foo...'),
+ ]),
+ );
+ });
+
+ test('does not include major version changes as targets', () async {
+ const String newVersion = '2.0.0';
+ final Directory package =
+ createFakePackage('foo', packagesDir, version: newVersion);
+
+ final File pubspecFile = RepositoryPackage(package).pubspecFile;
+ final String changedFileOutput = <File>[
+ pubspecFile,
+ ].map((File file) => file.path).join('\n');
+ processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[
+ MockProcess(stdout: changedFileOutput),
+ ];
+ final String gitPubspecContents =
+ pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0');
+ // Simulate no change to the version in the interface's pubspec.yaml.
+ processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[
+ MockProcess(stdout: gitPubspecContents),
+ ];
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'make-deps-path-based',
+ '--target-dependencies-with-non-breaking-updates'
+ ]);
+
+ expect(
+ output,
+ containsAllInOrder(<Matcher>[
+ contains('No target dependencies'),
+ ]),
+ );
+ });
+
+ test('does not include minor version changes to 0.x as targets', () async {
+ const String newVersion = '0.8.0';
+ final Directory package =
+ createFakePackage('foo', packagesDir, version: newVersion);
+
+ final File pubspecFile = RepositoryPackage(package).pubspecFile;
+ final String changedFileOutput = <File>[
+ pubspecFile,
+ ].map((File file) => file.path).join('\n');
+ processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[
+ MockProcess(stdout: changedFileOutput),
+ ];
+ final String gitPubspecContents =
+ pubspecFile.readAsStringSync().replaceAll(newVersion, '0.7.0');
+ // Simulate no change to the version in the interface's pubspec.yaml.
+ processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[
+ MockProcess(stdout: gitPubspecContents),
+ ];
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'make-deps-path-based',
+ '--target-dependencies-with-non-breaking-updates'
+ ]);
+
+ expect(
+ output,
+ containsAllInOrder(<Matcher>[
+ contains('No target dependencies'),
+ ]),
+ );
+ });
+ });
+}
diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart
index 14e99a1..2cb3fc2 100644
--- a/script/tool/test/publish_plugin_command_test.dart
+++ b/script/tool/test/publish_plugin_command_test.dart
@@ -675,7 +675,7 @@
contains('Running `pub publish ` in ${pluginDir1.path}...'),
contains('Published plugin1 successfully!'),
contains(
- 'The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n'),
+ 'The pubspec file for plugin2/plugin2 does not exist, so no publishing will happen.\nSafe to ignore if the package is deleted in this commit.\n'),
contains('SKIPPING: package deleted'),
contains('skipped (with warning)'),
]));