Files
packages/script/tool/test/common/package_command_test.dart
stuartmorgan-g cc3dca67f2 [all] Omit obvious local types (#10511)
Makes the analysis options changes described in
https://github.com/flutter/flutter/issues/178827:
- Adding
[omit_obvious_local_variable_types](https://dart.dev/tools/linter-rules/omit_obvious_local_variable_types)
- Adding
[specify_nonobvious_local_variable_types](https://dart.dev/tools/linter-rules/specify_nonobvious_local_variable_types)
- Adding
[specify_nonobvious_property_types](https://dart.dev/tools/linter-rules/specify_nonobvious_property_types)
- Adding
[type_annotate_public_apis](https://dart.dev/tools/linter-rules/type_annotate_public_apis)
- Removing
[always_specify_types](https://dart.dev/tools/linter-rules/always_specify_types)

After those changes, makes the following repo-wide changes:
- `dart fix --apply` in all packages and in script/tool/
- `dart format` in all packages and in script/tool/
- `update-excerpts` repo tooling command to update excerpts based on the
changes to their sources

Also updates the min Flutter/Dart SDK version to 3.35/3.9 for the
following packages, to avoid `analyze` failures in the `N-2` legacy
analysis run due to what appears to be a 3.9 change in what the Dart
analyzer continues to be an obvious local type in loop iterations:
- go_router
- google_fonts
- google_identity_services_web
- google_maps_flutter_web
- local_auth_platform_interface
- metrics_center
- multicast_dns
- pigeon
- rfw
- shared_preferences
- two_dimensional_scrollables
- vector_graphics_compiler
- mustache_template
- path_parsing

Because this is causing a significant amount of format churn already, I
took this opportunity to update the repository tooling to a min Dart SDK
of 3.8 (the N-2 stable version, so the earliest version we need the
tooling to support) to pick up the new format style, so the amount of
automated formatter change is higher in script/tool/ than in the
packages.

This does contain two manual changes (other than the repo tooling min
version):
-
d700b45c7d
changes `dynamic` to `Object?` in a few places where `dynamic` caused
analyzer warnings under the new rule set.
- Updates the repo tooling to ignore `.dart_tool/` when looking for
unexpected local `analysis_options.yaml` files, to fix issues running
the repo tool's `analyze` command locally based on recent changes in
`dart` behavior.

This does not include any CHANGELOG or version updates; even though we
normally version any changes to production code, mass automated changes
like this aren't worth the churn of releasing. This includes changes to
lib/example/main.dart and to README.md excerpts; while the style changes
will be user-visible on pub.dev, it's fine for those changes to wait for
the next release of each package.

Part of https://github.com/flutter/flutter/issues/178827
2025-11-24 18:26:06 -05:00

1826 lines
56 KiB
Dart

// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/common/package_command.dart';
import 'package:git/git.dart';
import 'package:mockito/annotations.dart';
import 'package:test/test.dart';
import '../mocks.dart';
import '../util.dart';
@GenerateMocks(<Type>[GitDir])
void main() {
late GitDir gitDir;
late RecordingProcessRunner processRunner;
late RecordingProcessRunner gitProcessRunner;
late SamplePackageCommand command;
late CommandRunner<void> runner;
late MockPlatform mockPlatform;
late Directory packagesDir;
late Directory thirdPartyPackagesDir;
SamplePackageCommand configureCommand({bool includeSubpackages = false}) {
final command = SamplePackageCommand(
packagesDir,
processRunner: processRunner,
platform: mockPlatform,
gitDir: gitDir,
includeSubpackages: includeSubpackages,
);
return command;
}
setUp(() {
mockPlatform = MockPlatform();
(:packagesDir, :processRunner, :gitProcessRunner, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform);
thirdPartyPackagesDir = packagesDir.parent
.childDirectory('third_party')
.childDirectory('packages');
command = configureCommand();
runner = CommandRunner<void>(
'common_command',
'Test for common functionality',
);
runner.addCommand(command);
});
group('plugin iteration', () {
test('all plugins from file system', () async {
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
await runCapturingPrint(runner, <String>['sample']);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path]),
);
});
test('includes both plugins and packages', () async {
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
final RepositoryPackage package3 = createFakePackage(
'package3',
packagesDir,
);
final RepositoryPackage package4 = createFakePackage(
'package4',
packagesDir,
);
await runCapturingPrint(runner, <String>['sample']);
expect(
command.plugins,
unorderedEquals(<String>[
plugin1.path,
plugin2.path,
package3.path,
package4.path,
]),
);
});
test('includes packages without source', () async {
final RepositoryPackage package = createFakePackage(
'package',
packagesDir,
);
package.libDirectory.deleteSync(recursive: true);
await runCapturingPrint(runner, <String>['sample']);
expect(command.plugins, unorderedEquals(<String>[package.path]));
});
test('all plugins includes third_party/packages', () async {
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
final RepositoryPackage plugin3 = createFakePlugin(
'plugin3',
thirdPartyPackagesDir,
);
await runCapturingPrint(runner, <String>['sample']);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path, plugin3.path]),
);
});
test('--packages limits packages', () async {
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
createFakePlugin('plugin2', packagesDir);
createFakePackage('package3', packagesDir);
final RepositoryPackage package4 = createFakePackage(
'package4',
packagesDir,
);
await runCapturingPrint(runner, <String>[
'sample',
'--packages=plugin1,package4',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, package4.path]),
);
});
test('--plugins acts as an alias to --packages', () async {
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
createFakePlugin('plugin2', packagesDir);
createFakePackage('package3', packagesDir);
final RepositoryPackage package4 = createFakePackage(
'package4',
packagesDir,
);
await runCapturingPrint(runner, <String>[
'sample',
'--plugins=plugin1,package4',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, package4.path]),
);
});
test('exclude packages when packages flag is specified', () async {
createFakePlugin('plugin1', packagesDir);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
await runCapturingPrint(runner, <String>[
'sample',
'--packages=plugin1,plugin2',
'--exclude=plugin1',
]);
expect(command.plugins, unorderedEquals(<String>[plugin2.path]));
});
test("exclude packages when packages flag isn't specified", () async {
createFakePlugin('plugin1', packagesDir);
createFakePlugin('plugin2', packagesDir);
await runCapturingPrint(runner, <String>[
'sample',
'--exclude=plugin1,plugin2',
]);
expect(command.plugins, unorderedEquals(<String>[]));
});
test('exclude federated plugins when packages flag is specified', () async {
createFakePlugin('plugin1', packagesDir.childDirectory('federated'));
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
await runCapturingPrint(runner, <String>[
'sample',
'--packages=federated/plugin1,plugin2',
'--exclude=federated/plugin1',
]);
expect(command.plugins, unorderedEquals(<String>[plugin2.path]));
});
test(
'exclude entire federated plugins when packages flag is specified',
() async {
createFakePlugin('plugin1', packagesDir.childDirectory('federated'));
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
await runCapturingPrint(runner, <String>[
'sample',
'--packages=federated/plugin1,plugin2',
'--exclude=federated',
]);
expect(command.plugins, unorderedEquals(<String>[plugin2.path]));
},
);
test('exclude accepts config files', () async {
createFakePlugin('plugin1', packagesDir);
final File configFile = packagesDir.childFile('exclude.yaml');
configFile.writeAsStringSync('- plugin1');
await runCapturingPrint(runner, <String>[
'sample',
'--packages=plugin1',
'--exclude=${configFile.path}',
]);
expect(command.plugins, unorderedEquals(<String>[]));
});
test('exclude accepts empty config files', () async {
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final File configFile1 = packagesDir.childFile('exclude1.yaml');
configFile1.createSync();
final File configFile2 = packagesDir.childFile('exclude2.yaml');
configFile2.writeAsStringSync('\n');
final File configFile3 = packagesDir.childFile('exclude3.yaml');
configFile3.writeAsStringSync('# - plugin1');
await runCapturingPrint(runner, <String>[
'sample',
'--packages=plugin1',
'--exclude=${configFile1.path},${configFile2.path},${configFile3.path}',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
});
test('filter-packages-to accepts config files', () async {
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
createFakePlugin('plugin2', packagesDir);
final File configFile = packagesDir.childFile('exclude.yaml');
configFile.writeAsStringSync('- plugin1');
await runCapturingPrint(runner, <String>[
'sample',
'--packages=plugin1,plugin2',
'--filter-packages-to=${configFile.path}',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
});
test('explicitly specifying the plugin (group) name of a federated plugin '
'should include all plugins in the group', () async {
final Directory pluginGroup = packagesDir.childDirectory('plugin1');
final RepositoryPackage appFacingPackage = createFakePlugin(
'plugin1',
pluginGroup,
);
final RepositoryPackage platformInterfacePackage = createFakePlugin(
'plugin1_platform_interface',
pluginGroup,
);
final RepositoryPackage implementationPackage = createFakePlugin(
'plugin1_web',
pluginGroup,
);
await runCapturingPrint(runner, <String>['sample', '--packages=plugin1']);
expect(
command.plugins,
unorderedEquals(<String>[
appFacingPackage.path,
platformInterfacePackage.path,
implementationPackage.path,
]),
);
});
test('specifying the app-facing package of a federated plugin with '
'--exact-match-only should only include only that package', () async {
final Directory pluginGroup = packagesDir.childDirectory('plugin1');
final RepositoryPackage appFacingPackage = createFakePlugin(
'plugin1',
pluginGroup,
);
createFakePlugin('plugin1_platform_interface', pluginGroup);
createFakePlugin('plugin1_web', pluginGroup);
await runCapturingPrint(runner, <String>[
'sample',
'--packages=plugin1',
'--exact-match-only',
]);
expect(command.plugins, unorderedEquals(<String>[appFacingPackage.path]));
});
test('specifying the app-facing package of a federated plugin using its '
'fully qualified name should include only that package', () async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/plugin1/plugin1/plugin1.dart
''',
),
),
];
final Directory pluginGroup = packagesDir.childDirectory('plugin1');
final RepositoryPackage appFacingPackage = createFakePlugin(
'plugin1',
pluginGroup,
);
createFakePlugin('plugin1_platform_interface', pluginGroup);
createFakePlugin('plugin1_web', pluginGroup);
await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--packages=plugin1/plugin1',
]);
expect(command.plugins, unorderedEquals(<String>[appFacingPackage.path]));
});
test('specifying a package of a federated plugin by its name should '
'include only that package', () async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/plugin1/plugin1/plugin1.dart
''',
),
),
];
final Directory pluginGroup = packagesDir.childDirectory('plugin1');
createFakePlugin('plugin1', pluginGroup);
final RepositoryPackage platformInterfacePackage = createFakePlugin(
'plugin1_platform_interface',
pluginGroup,
);
createFakePlugin('plugin1_web', pluginGroup);
await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--packages=plugin1_platform_interface',
]);
expect(
command.plugins,
unorderedEquals(<String>[platformInterfacePackage.path]),
);
});
test('returns subpackages after the enclosing package', () async {
final SamplePackageCommand localCommand = configureCommand(
includeSubpackages: true,
);
final localRunner = CommandRunner<void>(
'common_command',
'subpackage testing',
);
localRunner.addCommand(localCommand);
final RepositoryPackage package = createFakePackage(
'apackage',
packagesDir,
);
await runCapturingPrint(localRunner, <String>['sample']);
expect(
localCommand.plugins,
containsAllInOrder(<String>[package.path, getExampleDir(package).path]),
);
});
group('conflicting package selection', () {
test(
'does not allow --packages with --run-on-changed-packages',
() async {
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>[
'sample',
'--run-on-changed-packages',
'--packages=plugin1',
],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Only one of the package selection arguments'),
]),
);
},
);
test('does not allow --packages with --packages-for-branch', () async {
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['sample', '--packages-for-branch', '--packages=plugin1'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Only one of the package selection arguments'),
]),
);
});
test('does not allow --packages with --current-package', () async {
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['sample', '--current-package', '--packages=plugin1'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Only one of the package selection arguments'),
]),
);
});
test(
'does not allow --run-on-changed-packages with --packages-for-branch',
() async {
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['sample', '--packages-for-branch', '--packages=plugin1'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Only one of the package selection arguments'),
]),
);
},
);
});
group('current-package', () {
test('throws when run from outside of the packages directory', () async {
packagesDir.fileSystem.currentDirectory = packagesDir.parent;
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['sample', '--current-package'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'--current-package can only be used within a repository '
'package or package group',
),
]),
);
});
test('throws when run directly in the packages directory', () async {
packagesDir.fileSystem.currentDirectory = packagesDir;
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['sample', '--current-package'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'--current-package can only be used within a repository '
'package or package group',
),
]),
);
});
test('runs on a package when run from the package directory', () async {
final RepositoryPackage package = createFakePlugin(
'a_package',
packagesDir,
);
createFakePlugin('another_package', packagesDir);
packagesDir.fileSystem.currentDirectory = package.directory;
await runCapturingPrint(runner, <String>[
'sample',
'--current-package',
]);
expect(command.plugins, unorderedEquals(<String>[package.path]));
});
test(
'runs on a package when run from the third_party/packages directory',
() async {
final RepositoryPackage package = createFakePlugin(
'a_package',
thirdPartyPackagesDir,
);
createFakePlugin('another_package', thirdPartyPackagesDir);
packagesDir.fileSystem.currentDirectory = package.directory;
await runCapturingPrint(runner, <String>[
'sample',
'--current-package',
]);
expect(command.plugins, unorderedEquals(<String>[package.path]));
},
);
test('runs only app-facing package of a federated plugin', () async {
const pluginName = 'foo';
final Directory groupDir = packagesDir.childDirectory(pluginName);
final RepositoryPackage package = createFakePlugin(
pluginName,
groupDir,
);
createFakePlugin('${pluginName}_someplatform', groupDir);
createFakePackage('${pluginName}_platform_interface', groupDir);
packagesDir.fileSystem.currentDirectory = package.directory;
await runCapturingPrint(runner, <String>[
'sample',
'--current-package',
]);
expect(command.plugins, unorderedEquals(<String>[package.path]));
});
test(
'runs on a package when run from a package example directory',
() async {
final RepositoryPackage package = createFakePlugin(
'a_package',
packagesDir,
examples: <String>['a', 'b', 'c'],
);
createFakePlugin('another_package', packagesDir);
packagesDir.fileSystem.currentDirectory = package
.getExamples()
.first
.directory;
await runCapturingPrint(runner, <String>[
'sample',
'--current-package',
]);
expect(command.plugins, unorderedEquals(<String>[package.path]));
},
);
test(
'runs on a package group when run from the group directory',
() async {
final Directory pluginGroup = packagesDir.childDirectory('a_plugin');
final RepositoryPackage plugin1 = createFakePlugin(
'a_plugin_foo',
pluginGroup,
);
final RepositoryPackage plugin2 = createFakePlugin(
'a_plugin_bar',
pluginGroup,
);
createFakePlugin('unrelated_plugin', packagesDir);
packagesDir.fileSystem.currentDirectory = pluginGroup;
await runCapturingPrint(runner, <String>[
'sample',
'--current-package',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path]),
);
},
);
});
group('test run-on-changed-packages', () {
test('all plugins should be tested if there are no changes.', () async {
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path]),
);
});
test(
'all plugins should be tested if there are no plugin related changes.',
() async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: 'AUTHORS')),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path]),
);
},
);
test('all plugins should be tested if .ci.yaml changes', () async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
.ci.yaml
packages/plugin1/CHANGELOG
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
final List<String> output = await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path]),
);
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Running for all packages, since a file has changed '
'that could affect the entire repository.',
),
]),
);
});
test(
'all plugins should be tested if anything in .ci/ changes',
() async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
.ci/Dockerfile
packages/plugin1/CHANGELOG
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
final List<String> output = await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path]),
);
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Running for all packages, since a file has changed '
'that could affect the entire repository.',
),
]),
);
},
);
test(
'all plugins should be tested if anything in script/ changes.',
() async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
script/tool/bin/flutter_plugin_tools.dart
packages/plugin1/CHANGELOG
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
final List<String> output = await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path]),
);
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Running for all packages, since a file has changed '
'that could affect the entire repository.',
),
]),
);
},
);
test(
'all plugins should be tested if the root analysis options change.',
() async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
analysis_options.yaml
packages/plugin1/CHANGELOG
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
final List<String> output = await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path]),
);
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Running for all packages, since a file has changed '
'that could affect the entire repository.',
),
]),
);
},
);
test(
'all plugins should be tested if formatting options change.',
() async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
.clang-format
packages/plugin1/CHANGELOG
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
final List<String> output = await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path]),
);
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Running for all packages, since a file has changed '
'that could affect the entire repository.',
),
]),
);
},
);
test('Only changed plugin should be tested.', () async {
gitProcessRunner
.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
createFakePlugin('plugin2', packagesDir);
final List<String> output = await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Running for all packages that have diffs relative to "main"',
),
]),
);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
});
test(
'multiple files in one plugin should also test the plugin',
() async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/plugin1/plugin1.dart
packages/plugin1/ios/plugin1.m
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
createFakePlugin('plugin2', packagesDir);
await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
},
);
test(
'multiple plugins changed should test all the changed plugins',
() async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/plugin1/plugin1.dart
packages/plugin2/ios/plugin2.m
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin2',
packagesDir,
);
createFakePlugin('plugin3', packagesDir);
await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path]),
);
},
);
test(
'multiple plugins inside the same plugin group changed should output the plugin group name',
() async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/plugin1/plugin1/plugin1.dart
packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart
packages/plugin1/plugin1_web/plugin1_web.dart
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir.childDirectory('plugin1'),
);
createFakePlugin('plugin2', packagesDir);
createFakePlugin('plugin3', packagesDir);
await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
},
);
test(
'changing one plugin in a federated group should only include that plugin',
() async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/plugin1/plugin1/plugin1.dart
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir.childDirectory('plugin1'),
);
createFakePlugin(
'plugin1_platform_interface',
packagesDir.childDirectory('plugin1'),
);
createFakePlugin(
'plugin1_web',
packagesDir.childDirectory('plugin1'),
);
await runCapturingPrint(runner, <String>[
'sample',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
},
);
test('honors --exclude flag', () async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/plugin1/plugin1.dart
packages/plugin2/ios/plugin2.m
packages/plugin3/plugin3.dart
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir.childDirectory('plugin1'),
);
createFakePlugin('plugin2', packagesDir);
createFakePlugin('plugin3', packagesDir);
await runCapturingPrint(runner, <String>[
'sample',
'--exclude=plugin2,plugin3',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
});
test('honors --filter-packages-to flag', () async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/plugin1/plugin1.dart
packages/plugin2/ios/plugin2.m
packages/plugin3/plugin3.dart
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir.childDirectory('plugin1'),
);
createFakePlugin('plugin2', packagesDir);
createFakePlugin('plugin3', packagesDir);
await runCapturingPrint(runner, <String>[
'sample',
'--filter-packages-to=plugin1',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
});
test('honors --filter-packages-to flag when a file is changed that makes '
'all packages potentially changed', () async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
.ci.yaml
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir.childDirectory('plugin1'),
);
createFakePlugin('plugin2', packagesDir);
createFakePlugin('plugin3', packagesDir);
await runCapturingPrint(runner, <String>[
'sample',
'--filter-packages-to=plugin1',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
});
test('--filter-packages-to handles federated plugin groups', () async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/a_plugin/a_plugin/lib/foo.dart
packages/a_plugin/a_plugin_impl/lib/foo.dart
packages/a_plugin/a_plugin_platform_interface/lib/foo.dart
''',
),
),
];
final Directory groupDir = packagesDir.childDirectory('a_plugin');
final RepositoryPackage plugin1 = createFakePlugin(
'a_plugin',
groupDir,
);
final RepositoryPackage plugin2 = createFakePlugin(
'a_plugin_impl',
groupDir,
);
final RepositoryPackage plugin3 = createFakePlugin(
'a_plugin_platform_interface',
groupDir,
);
await runCapturingPrint(runner, <String>[
'sample',
'--filter-packages-to=a_plugin',
'--base-sha=main',
'--run-on-changed-packages',
]);
expect(
command.plugins,
unorderedEquals(<String>[plugin1.path, plugin2.path, plugin3.path]),
);
});
test('--filter-packages-to and --exclude work together', () async {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
.ci.yaml
''',
),
),
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir.childDirectory('plugin1'),
);
createFakePlugin('plugin2', packagesDir);
createFakePlugin('plugin3', packagesDir);
await runCapturingPrint(runner, <String>[
'sample',
'--filter-packages-to=plugin1,plugin2',
'--exclude=plugin2',
'--base-sha=main',
'--run-on-changed-packages',
]);
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 {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(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 {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
.ci.yaml
.ci/Dockerfile
.clang-format
analysis_options.yaml
script/tool/bin/flutter_plugin_tools.dart
''',
),
),
];
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 {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(stdout: 'packages/a_package/lib/a_package.dart'),
),
];
final RepositoryPackage 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 {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/a_package/lib/a_package.dart
packages/b_package/lib/src/foo.dart
''',
),
),
];
final RepositoryPackage packageA = createFakePackage(
'a_package',
packagesDir,
);
final RepositoryPackage 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 {
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/a_package/lib/a_package.dart
packages/b_package/lib/src/foo.dart
''',
),
),
];
final RepositoryPackage 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', () {
test(
'only tests changed packages relative to the merge base on a branch',
() async {
gitProcessRunner
.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')),
];
gitProcessRunner.mockProcessesForExecutable['git-rev-parse'] =
<FakeProcessInfo>[FakeProcessInfo(MockProcess(stdout: 'a-branch'))];
gitProcessRunner
.mockProcessesForExecutable['git-merge-base'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(exitCode: 1), <String>['--is-ancestor']),
FakeProcessInfo(MockProcess(stdout: 'abc123'), <String>[
'--fork-point',
]), // finding merge base
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
createFakePlugin('plugin2', packagesDir);
final List<String> output = await runCapturingPrint(runner, <String>[
'sample',
'--packages-for-branch',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
expect(
output,
containsAllInOrder(<Matcher>[
contains('--packages-for-branch: running on branch "a-branch"'),
contains(
'Running for all packages that have diffs relative to "abc123"',
),
]),
);
// Ensure that it's diffing against the merge-base.
expect(
gitProcessRunner.recordedCalls,
contains(
const ProcessCall('git-diff', <String>[
'--name-only',
'abc123',
'HEAD',
], null),
),
);
},
);
test(
'only tests changed packages relative to the previous commit on main',
() async {
gitProcessRunner
.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')),
];
gitProcessRunner.mockProcessesForExecutable['git-rev-parse'] =
<FakeProcessInfo>[FakeProcessInfo(MockProcess(stdout: 'main'))];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
createFakePlugin('plugin2', packagesDir);
final List<String> output = await runCapturingPrint(runner, <String>[
'sample',
'--packages-for-branch',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
expect(
output,
containsAllInOrder(<Matcher>[
contains('--packages-for-branch: running on default branch.'),
contains(
'--packages-for-branch: using parent commit as the diff base',
),
contains(
'Running for all packages that have diffs relative to "HEAD~"',
),
]),
);
// Ensure that it's diffing against the prior commit.
expect(
gitProcessRunner.recordedCalls,
contains(
const ProcessCall('git-diff', <String>[
'--name-only',
'HEAD~',
'HEAD',
], null),
),
);
},
);
test('only tests changed packages relative to the previous commit if '
'running on a specific hash from main', () async {
gitProcessRunner
.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')),
];
gitProcessRunner.mockProcessesForExecutable['git-rev-parse'] =
<FakeProcessInfo>[FakeProcessInfo(MockProcess(stdout: 'HEAD'))];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
createFakePlugin('plugin2', packagesDir);
final List<String> output = await runCapturingPrint(runner, <String>[
'sample',
'--packages-for-branch',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'--packages-for-branch: running on a commit from default branch.',
),
contains(
'--packages-for-branch: using parent commit as the diff base',
),
contains(
'Running for all packages that have diffs relative to "HEAD~"',
),
]),
);
// Ensure that it's diffing against the prior commit.
expect(
gitProcessRunner.recordedCalls,
contains(
const ProcessCall('git-diff', <String>[
'--name-only',
'HEAD~',
'HEAD',
], null),
),
);
});
test('only tests changed packages relative to the previous commit if '
'running on a specific hash from origin/main', () async {
gitProcessRunner
.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')),
];
gitProcessRunner.mockProcessesForExecutable['git-rev-parse'] =
<FakeProcessInfo>[FakeProcessInfo(MockProcess(stdout: 'HEAD'))];
gitProcessRunner.mockProcessesForExecutable['git-merge-base'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(exitCode: 128), <String>[
'--is-ancestor',
'HEAD',
'main',
]), // Fail with a non-1 exit code for 'main'
FakeProcessInfo(MockProcess(), <String>[
'--is-ancestor',
'HEAD',
'origin/main',
]), // Succeed for the variant.
];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
createFakePlugin('plugin2', packagesDir);
final List<String> output = await runCapturingPrint(runner, <String>[
'sample',
'--packages-for-branch',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'--packages-for-branch: running on a commit from default branch.',
),
contains(
'--packages-for-branch: using parent commit as the diff base',
),
contains(
'Running for all packages that have diffs relative to "HEAD~"',
),
]),
);
// Ensure that it's diffing against the prior commit.
expect(
gitProcessRunner.recordedCalls,
contains(
const ProcessCall('git-diff', <String>[
'--name-only',
'HEAD~',
'HEAD',
], null),
),
);
});
test(
'only tests changed packages relative to the previous commit on master',
() async {
gitProcessRunner
.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')),
];
gitProcessRunner.mockProcessesForExecutable['git-rev-parse'] =
<FakeProcessInfo>[FakeProcessInfo(MockProcess(stdout: 'master'))];
final RepositoryPackage plugin1 = createFakePlugin(
'plugin1',
packagesDir,
);
createFakePlugin('plugin2', packagesDir);
final List<String> output = await runCapturingPrint(runner, <String>[
'sample',
'--packages-for-branch',
]);
expect(command.plugins, unorderedEquals(<String>[plugin1.path]));
expect(
output,
containsAllInOrder(<Matcher>[
contains('--packages-for-branch: running on default branch.'),
contains(
'--packages-for-branch: using parent commit as the diff base',
),
contains(
'Running for all packages that have diffs relative to "HEAD~"',
),
]),
);
// Ensure that it's diffing against the prior commit.
expect(
gitProcessRunner.recordedCalls,
contains(
const ProcessCall('git-diff', <String>[
'--name-only',
'HEAD~',
'HEAD',
], null),
),
);
},
);
test('throws if getting the branch fails', () async {
gitProcessRunner
.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')),
];
gitProcessRunner.mockProcessesForExecutable['git-rev-parse'] =
<FakeProcessInfo>[FakeProcessInfo(MockProcess(exitCode: 1))];
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['sample', '--packages-for-branch'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[contains('Unable to determine branch')]),
);
});
});
group('sharding', () {
test('distributes evenly when evenly divisible', () async {
final expectedShards = <List<RepositoryPackage>>[
<RepositoryPackage>[
createFakePackage('package1', packagesDir),
createFakePackage('package2', packagesDir),
createFakePackage('package3', packagesDir),
],
<RepositoryPackage>[
createFakePackage('package4', packagesDir),
createFakePackage('package5', packagesDir),
createFakePackage('package6', packagesDir),
],
<RepositoryPackage>[
createFakePackage('package7', packagesDir),
createFakePackage('package8', packagesDir),
createFakePackage('package9', packagesDir),
],
];
for (var i = 0; i < expectedShards.length; ++i) {
final SamplePackageCommand localCommand = configureCommand();
final localRunner = CommandRunner<void>(
'common_command',
'Shard testing',
);
localRunner.addCommand(localCommand);
await runCapturingPrint(localRunner, <String>[
'sample',
'--shardIndex=$i',
'--shardCount=3',
]);
expect(
localCommand.plugins,
unorderedEquals(
expectedShards[i]
.map((RepositoryPackage package) => package.path)
.toList(),
),
);
}
});
test(
'distributes as evenly as possible when not evenly divisible',
() async {
final expectedShards = <List<RepositoryPackage>>[
<RepositoryPackage>[
createFakePackage('package1', packagesDir),
createFakePackage('package2', packagesDir),
createFakePackage('package3', packagesDir),
],
<RepositoryPackage>[
createFakePackage('package4', packagesDir),
createFakePackage('package5', packagesDir),
createFakePackage('package6', packagesDir),
],
<RepositoryPackage>[
createFakePackage('package7', packagesDir),
createFakePackage('package8', packagesDir),
],
];
for (var i = 0; i < expectedShards.length; ++i) {
final SamplePackageCommand localCommand = configureCommand();
final localRunner = CommandRunner<void>(
'common_command',
'Shard testing',
);
localRunner.addCommand(localCommand);
await runCapturingPrint(localRunner, <String>[
'sample',
'--shardIndex=$i',
'--shardCount=3',
]);
expect(
localCommand.plugins,
unorderedEquals(
expectedShards[i]
.map((RepositoryPackage package) => package.path)
.toList(),
),
);
}
},
);
// In CI (which is the use case for sharding) we often want to run muliple
// commands on the same set of packages, but the exclusion lists for those
// commands may be different. In those cases we still want all the commands
// to operate on a consistent set of plugins.
//
// E.g., some commands require running build-examples in a previous step;
// excluding some plugins from the later step shouldn't change what's tested
// in each shard, as it may no longer align with what was built.
test('counts excluded plugins when sharding', () async {
final expectedShards = <List<RepositoryPackage>>[
<RepositoryPackage>[
createFakePackage('package1', packagesDir),
createFakePackage('package2', packagesDir),
createFakePackage('package3', packagesDir),
],
<RepositoryPackage>[
createFakePackage('package4', packagesDir),
createFakePackage('package5', packagesDir),
createFakePackage('package6', packagesDir),
],
<RepositoryPackage>[createFakePackage('package7', packagesDir)],
];
// These would be in the last shard, but are excluded.
createFakePackage('package8', packagesDir);
createFakePackage('package9', packagesDir);
for (var i = 0; i < expectedShards.length; ++i) {
final SamplePackageCommand localCommand = configureCommand();
final localRunner = CommandRunner<void>(
'common_command',
'Shard testing',
);
localRunner.addCommand(localCommand);
await runCapturingPrint(localRunner, <String>[
'sample',
'--shardIndex=$i',
'--shardCount=3',
'--exclude=package8,package9',
]);
expect(
localCommand.plugins,
unorderedEquals(
expectedShards[i]
.map((RepositoryPackage package) => package.path)
.toList(),
),
);
}
});
});
}
class SamplePackageCommand extends PackageCommand {
SamplePackageCommand(
super.packagesDir, {
super.processRunner,
super.platform,
super.gitDir,
this.includeSubpackages = false,
});
final List<String> plugins = <String>[];
final bool includeSubpackages;
@override
final String name = 'sample';
@override
final String description = 'sample command';
@override
Future<void> run() async {
final Stream<PackageEnumerationEntry> packages = includeSubpackages
? getTargetPackagesAndSubpackages()
: getTargetPackages();
await for (final PackageEnumerationEntry entry in packages) {
plugins.add(entry.package.path);
}
}
}