Files
packages/script/tool/test/common/package_command_test.dart
stuartmorgan 8c287e99d6 [tool] Move changed file detection to base command class (#8730)
Consolidates the code to find all changed file paths into the `PackageLoopingCommand` class that is the base of almost all of the repo tooling commands. This in a preparatory PR for a future change to allow each command to define a list of files or file patterns that definitively *don't* affect that test, so that CI can be smarter about what tests to run (e.g., not running expensive integration tests for README changes).

A side effect of this change is that tests of almost all commands now need a mock `GitDir` instance. This would add a lot of copy/pasted boilerplate to the test setup, and there is already too much of that, so instead this refactors common test setup:
- Creating a memory file system
- Populating it with a packages directory
- Creating a RecordingProcessRunner to mock out process calls
- Creating a mock GitDir that forwards to a RecordingProcessRunner

into a helper method (using records and destructuring to easily return multiple values). While some tests don't need all of these steps, those that don't can easily ignore parts of it, and it will make it much easier to update tests in the future if they need them, and it makes the setup much more consistent which makes it easier to reason about test setup in general.

Prep for https://github.com/flutter/flutter/issues/136394
2025-03-25 21:29:17 +00:00

1405 lines
51 KiB
Dart

// 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: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 SamplePackageCommand 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 CommandRunner<void> 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 String 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 List<List<RepositoryPackage>> 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 (int i = 0; i < expectedShards.length; ++i) {
final SamplePackageCommand localCommand = configureCommand();
final CommandRunner<void> 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 List<List<RepositoryPackage>> 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 (int i = 0; i < expectedShards.length; ++i) {
final SamplePackageCommand localCommand = configureCommand();
final CommandRunner<void> 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 List<List<RepositoryPackage>> 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 (int i = 0; i < expectedShards.length; ++i) {
final SamplePackageCommand localCommand = configureCommand();
final CommandRunner<void> 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);
}
}
}