// 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:file/memory.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:mockito/mockito.dart'; import 'package:test/test.dart'; import '../mocks.dart'; import '../util.dart'; import 'package_command_test.mocks.dart'; @GenerateMocks([GitDir]) void main() { late RecordingProcessRunner processRunner; late SamplePackageCommand command; late CommandRunner runner; late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; late Directory thirdPartyPackagesDir; setUp(() { fileSystem = MemoryFileSystem(); mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); thirdPartyPackagesDir = packagesDir.parent .childDirectory('third_party') .childDirectory('packages'); final MockGitDir gitDir = MockGitDir(); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { final List arguments = invocation.positionalArguments[0]! as List; // 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(); command = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, gitDir: gitDir, ); runner = CommandRunner('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, ['sample']); expect(command.plugins, unorderedEquals([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, ['sample']); expect( command.plugins, unorderedEquals([ 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, ['sample']); expect( command.plugins, unorderedEquals([ 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, ['sample']); expect(command.plugins, unorderedEquals([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, ['sample', '--packages=plugin1,package4']); expect( command.plugins, unorderedEquals([ 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, ['sample', '--plugins=plugin1,package4']); expect( command.plugins, unorderedEquals([ 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, [ 'sample', '--packages=plugin1,plugin2', '--exclude=plugin1' ]); expect(command.plugins, unorderedEquals([plugin2.path])); }); test("exclude packages when packages flag isn't specified", () async { createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); await runCapturingPrint( runner, ['sample', '--exclude=plugin1,plugin2']); expect(command.plugins, unorderedEquals([])); }); test('exclude federated plugins when packages flag is specified', () async { createFakePlugin('plugin1', packagesDir.childDirectory('federated')); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ 'sample', '--packages=federated/plugin1,plugin2', '--exclude=federated/plugin1' ]); expect(command.plugins, unorderedEquals([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, [ 'sample', '--packages=federated/plugin1,plugin2', '--exclude=federated' ]); expect(command.plugins, unorderedEquals([plugin2.path])); }); test('exclude accepts config files', () async { createFakePlugin('plugin1', packagesDir); final File configFile = packagesDir.childFile('exclude.yaml'); configFile.writeAsStringSync('- plugin1'); await runCapturingPrint(runner, [ 'sample', '--packages=plugin1', '--exclude=${configFile.path}' ]); expect(command.plugins, unorderedEquals([])); }); 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, [ 'sample', '--packages=plugin1,plugin2', '--filter-packages-to=${configFile.path}' ]); expect(command.plugins, unorderedEquals([plugin1.path])); }); test( 'explicitly specifying the plugin (group) name of a federated plugin ' 'should include all plugins in the group', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: ''' packages/plugin1/plugin1/plugin1.dart ''')), ]; 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, ['sample', '--base-sha=main', '--packages=plugin1']); expect( command.plugins, unorderedEquals([ appFacingPackage.path, platformInterfacePackage.path, implementationPackage.path ])); }); test( 'specifying the app-facing package of a federated plugin using its ' 'fully qualified name should include only that package', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ 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, ['sample', '--base-sha=main', '--packages=plugin1/plugin1']); expect(command.plugins, unorderedEquals([appFacingPackage.path])); }); test( 'specifying a package of a federated plugin by its name should ' 'include only that package', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ 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, [ 'sample', '--base-sha=main', '--packages=plugin1_platform_interface' ]); expect(command.plugins, unorderedEquals([platformInterfacePackage.path])); }); test('returns subpackages after the enclosing package', () async { final SamplePackageCommand localCommand = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, gitDir: MockGitDir(), includeSubpackages: true, ); final CommandRunner localRunner = CommandRunner('common_command', 'subpackage testing'); localRunner.addCommand(localCommand); final RepositoryPackage package = createFakePackage('apackage', packagesDir); await runCapturingPrint(localRunner, ['sample']); expect( localCommand.plugins, containsAllInOrder([ package.path, getExampleDir(package).path, ])); }); group('conflicting package selection', () { test('does not allow --packages with --run-on-changed-packages', () async { Error? commandError; final List output = await runCapturingPrint(runner, [ 'sample', '--run-on-changed-packages', '--packages=plugin1', ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Only one of the package selection arguments') ])); }); test('does not allow --packages with --packages-for-branch', () async { Error? commandError; final List output = await runCapturingPrint(runner, [ 'sample', '--packages-for-branch', '--packages=plugin1', ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Only one of the package selection arguments') ])); }); test('does not allow --packages with --current-package', () async { Error? commandError; final List output = await runCapturingPrint(runner, [ 'sample', '--current-package', '--packages=plugin1', ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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 output = await runCapturingPrint(runner, [ 'sample', '--packages-for-branch', '--packages=plugin1', ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Only one of the package selection arguments') ])); }); }); group('current-package', () { test('throws when run from outside of the packages directory', () async { fileSystem.currentDirectory = packagesDir.parent; Error? commandError; final List output = await runCapturingPrint(runner, [ 'sample', '--current-package', ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('--current-package can only be used within a repository ' 'package or package group') ])); }); test('throws when run directly in the packages directory', () async { fileSystem.currentDirectory = packagesDir; Error? commandError; final List output = await runCapturingPrint(runner, [ 'sample', '--current-package', ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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); fileSystem.currentDirectory = package.directory; await runCapturingPrint( runner, ['sample', '--current-package']); expect(command.plugins, unorderedEquals([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); fileSystem.currentDirectory = package.directory; await runCapturingPrint( runner, ['sample', '--current-package']); expect(command.plugins, unorderedEquals([package.path])); }); test('runs on a package when run from a package example directory', () async { final RepositoryPackage package = createFakePlugin( 'a_package', packagesDir, examples: ['a', 'b', 'c']); createFakePlugin('another_package', packagesDir); fileSystem.currentDirectory = package.getExamples().first.directory; await runCapturingPrint( runner, ['sample', '--current-package']); expect(command.plugins, unorderedEquals([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); fileSystem.currentDirectory = pluginGroup; await runCapturingPrint( runner, ['sample', '--current-package']); expect(command.plugins, unorderedEquals([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, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test( 'all plugins should be tested if there are no plugin related changes.', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: 'AUTHORS')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if .ci.yaml changes', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: ''' .ci.yaml packages/plugin1/CHANGELOG ''')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); expect( output, containsAllInOrder([ 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 { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: ''' .ci/Dockerfile packages/plugin1/CHANGELOG ''')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); expect( output, containsAllInOrder([ 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 { processRunner.mockProcessesForExecutable['git-diff'] = [ 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 output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); expect( output, containsAllInOrder([ 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 { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: ''' analysis_options.yaml packages/plugin1/CHANGELOG ''')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); expect( output, containsAllInOrder([ 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 { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: ''' .clang-format packages/plugin1/CHANGELOG ''')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); expect( output, containsAllInOrder([ contains('Running for all packages, since a file has changed ' 'that could affect the entire repository.') ])); }); test('Only changed plugin should be tested.', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect( output, containsAllInOrder([ contains( 'Running for all packages that have diffs relative to "main"'), ])); expect(command.plugins, unorderedEquals([plugin1.path])); }); test('multiple files in one plugin should also test the plugin', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: ''' packages/plugin1/plugin1.dart packages/plugin1/ios/plugin1.m ''')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path])); }); test('multiple plugins changed should test all the changed plugins', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ 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, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test( 'multiple plugins inside the same plugin group changed should output the plugin group name', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ 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, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path])); }); test( 'changing one plugin in a federated group should only include that plugin', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ 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, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path])); }); test('honors --exclude flag', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ 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, [ 'sample', '--exclude=plugin2,plugin3', '--base-sha=main', '--run-on-changed-packages' ]); expect(command.plugins, unorderedEquals([plugin1.path])); }); test('honors --filter-packages-to flag', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ 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, [ 'sample', '--filter-packages-to=plugin1', '--base-sha=main', '--run-on-changed-packages' ]); expect(command.plugins, unorderedEquals([plugin1.path])); }); test( 'honors --filter-packages-to flag when a file is changed that makes ' 'all packages potentially changed', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: ''' .ci.yaml ''')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); await runCapturingPrint(runner, [ 'sample', '--filter-packages-to=plugin1', '--base-sha=main', '--run-on-changed-packages' ]); expect(command.plugins, unorderedEquals([plugin1.path])); }); test('--filter-packages-to handles federated plugin groups', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ 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, [ 'sample', '--filter-packages-to=a_plugin', '--base-sha=main', '--run-on-changed-packages' ]); expect( command.plugins, unorderedEquals( [plugin1.path, plugin2.path, plugin3.path])); }); test('--filter-packages-to and --exclude work together', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: ''' .ci.yaml ''')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); await runCapturingPrint(runner, [ 'sample', '--filter-packages-to=plugin1,plugin2', '--exclude=plugin2', '--base-sha=main', '--run-on-changed-packages' ]); expect(command.plugins, unorderedEquals([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, ['sample', '--run-on-dirty-packages']); expect(command.plugins, unorderedEquals([])); }); test( 'no packages should be tested if there are no plugin related changes.', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: 'AUTHORS')), ]; createFakePackage('a_package', packagesDir); await runCapturingPrint( runner, ['sample', '--run-on-dirty-packages']); expect(command.plugins, unorderedEquals([])); }); test('no packages should be tested even if special repo files change.', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ 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, ['sample', '--run-on-dirty-packages']); expect(command.plugins, unorderedEquals([])); }); test('Only changed packages should be tested.', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess(stdout: 'packages/a_package/lib/a_package.dart')), ]; final RepositoryPackage packageA = createFakePackage('a_package', packagesDir); createFakePlugin('b_package', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--run-on-dirty-packages']); expect( output, containsAllInOrder([ contains( 'Running for all packages that have uncommitted changes'), ])); expect(command.plugins, unorderedEquals([packageA.path])); }); test('multiple packages changed should test all the changed packages', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ 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, ['sample', '--run-on-dirty-packages']); expect(command.plugins, unorderedEquals([packageA.path, packageB.path])); }); test('honors --exclude flag', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ 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, [ 'sample', '--exclude=b_package', '--run-on-dirty-packages' ]); expect(command.plugins, unorderedEquals([packageA.path])); }); }); }); group('--packages-for-branch', () { test('only tests changed packages relative to the merge base on a branch', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')), ]; processRunner.mockProcessesForExecutable['git-rev-parse'] = [ FakeProcessInfo(MockProcess(stdout: 'a-branch')), ]; processRunner.mockProcessesForExecutable['git-merge-base'] = [ FakeProcessInfo(MockProcess(exitCode: 1), ['--is-ancestor']), FakeProcessInfo(MockProcess(stdout: 'abc123'), ['--fork-point']), // finding merge base ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--packages-for-branch']); expect(command.plugins, unorderedEquals([plugin1.path])); expect( output, containsAllInOrder([ 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( processRunner.recordedCalls, contains( const ProcessCall( 'git-diff', ['--name-only', 'abc123', 'HEAD'], null), )); }); test('only tests changed packages relative to the previous commit on main', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')), ]; processRunner.mockProcessesForExecutable['git-rev-parse'] = [ FakeProcessInfo(MockProcess(stdout: 'main')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--packages-for-branch']); expect(command.plugins, unorderedEquals([plugin1.path])); expect( output, containsAllInOrder([ 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( processRunner.recordedCalls, contains( const ProcessCall( 'git-diff', ['--name-only', 'HEAD~', 'HEAD'], null), )); }); test( 'only tests changed packages relative to the previous commit if ' 'running on a specific hash from main', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')), ]; processRunner.mockProcessesForExecutable['git-rev-parse'] = [ FakeProcessInfo(MockProcess(stdout: 'HEAD')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--packages-for-branch']); expect(command.plugins, unorderedEquals([plugin1.path])); expect( output, containsAllInOrder([ 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( processRunner.recordedCalls, contains( const ProcessCall( 'git-diff', ['--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 { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')), ]; processRunner.mockProcessesForExecutable['git-rev-parse'] = [ FakeProcessInfo(MockProcess(stdout: 'HEAD')), ]; processRunner.mockProcessesForExecutable['git-merge-base'] = [ FakeProcessInfo(MockProcess(exitCode: 128), [ '--is-ancestor', 'HEAD', 'main' ]), // Fail with a non-1 exit code for 'main' FakeProcessInfo(MockProcess(), [ '--is-ancestor', 'HEAD', 'origin/main' ]), // Succeed for the variant. ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--packages-for-branch']); expect(command.plugins, unorderedEquals([plugin1.path])); expect( output, containsAllInOrder([ 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( processRunner.recordedCalls, contains( const ProcessCall( 'git-diff', ['--name-only', 'HEAD~', 'HEAD'], null), )); }); test( 'only tests changed packages relative to the previous commit on master', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')), ]; processRunner.mockProcessesForExecutable['git-rev-parse'] = [ FakeProcessInfo(MockProcess(stdout: 'master')), ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--packages-for-branch']); expect(command.plugins, unorderedEquals([plugin1.path])); expect( output, containsAllInOrder([ 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( processRunner.recordedCalls, contains( const ProcessCall( 'git-diff', ['--name-only', 'HEAD~', 'HEAD'], null), )); }); test('throws if getting the branch fails', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: 'packages/plugin1/plugin1.dart')), ]; processRunner.mockProcessesForExecutable['git-rev-parse'] = [ FakeProcessInfo(MockProcess(exitCode: 1)), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['sample', '--packages-for-branch'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Unable to determine branch'), ])); }); }); group('sharding', () { test('distributes evenly when evenly divisible', () async { final List> expectedShards = >[ [ createFakePackage('package1', packagesDir), createFakePackage('package2', packagesDir), createFakePackage('package3', packagesDir), ], [ createFakePackage('package4', packagesDir), createFakePackage('package5', packagesDir), createFakePackage('package6', packagesDir), ], [ createFakePackage('package7', packagesDir), createFakePackage('package8', packagesDir), createFakePackage('package9', packagesDir), ], ]; for (int i = 0; i < expectedShards.length; ++i) { final SamplePackageCommand localCommand = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, gitDir: MockGitDir(), ); final CommandRunner localRunner = CommandRunner('common_command', 'Shard testing'); localRunner.addCommand(localCommand); await runCapturingPrint(localRunner, [ '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> expectedShards = >[ [ createFakePackage('package1', packagesDir), createFakePackage('package2', packagesDir), createFakePackage('package3', packagesDir), ], [ createFakePackage('package4', packagesDir), createFakePackage('package5', packagesDir), createFakePackage('package6', packagesDir), ], [ createFakePackage('package7', packagesDir), createFakePackage('package8', packagesDir), ], ]; for (int i = 0; i < expectedShards.length; ++i) { final SamplePackageCommand localCommand = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, gitDir: MockGitDir(), ); final CommandRunner localRunner = CommandRunner('common_command', 'Shard testing'); localRunner.addCommand(localCommand); await runCapturingPrint(localRunner, [ '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> expectedShards = >[ [ createFakePackage('package1', packagesDir), createFakePackage('package2', packagesDir), createFakePackage('package3', packagesDir), ], [ createFakePackage('package4', packagesDir), createFakePackage('package5', packagesDir), createFakePackage('package6', packagesDir), ], [ 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 = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, gitDir: MockGitDir(), ); final CommandRunner localRunner = CommandRunner('common_command', 'Shard testing'); localRunner.addCommand(localCommand); await runCapturingPrint(localRunner, [ '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 plugins = []; final bool includeSubpackages; @override final String name = 'sample'; @override final String description = 'sample command'; @override Future run() async { final Stream packages = includeSubpackages ? getTargetPackagesAndSubpackages() : getTargetPackages(); await for (final PackageEnumerationEntry entry in packages) { plugins.add(entry.package.path); } } }