// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:flutter_plugin_tools/src/federation_safety_check_command.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import 'common/plugin_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; void main() { FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; setUp(() { fileSystem = MemoryFileSystem(); mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); final MockGitDir gitDir = MockGitDir(); when(gitDir.path).thenReturn(packagesDir.parent.path); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { final List arguments = invocation.positionalArguments[0]! as List; // Route git calls through the process runner, to make mock output // consistent with other processes. Attach the first argument to the // command to make targeting the mock results easier. final String gitCommand = arguments.removeAt(0); return processRunner.run('git-$gitCommand', arguments); }); processRunner = RecordingProcessRunner(); final FederationSafetyCheckCommand command = FederationSafetyCheckCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, gitDir: gitDir); runner = CommandRunner('federation_safety_check_command', 'Test for $FederationSafetyCheckCommand'); runner.addCommand(command); }); test('skips non-plugin packages', () async { final Directory package = createFakePackage('foo', packagesDir); final String changedFileOutput = [ package.childDirectory('lib').childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), ]; final List output = await runCapturingPrint(runner, ['federation-safety-check']); expect( output, containsAllInOrder([ contains('Running for foo...'), contains('Not a plugin'), contains('Skipped 1 package(s)'), ]), ); }); test('skips unfederated plugins', () async { final Directory package = createFakePlugin('foo', packagesDir); final String changedFileOutput = [ package.childDirectory('lib').childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), ]; final List output = await runCapturingPrint(runner, ['federation-safety-check']); expect( output, containsAllInOrder([ contains('Running for foo...'), contains('Not a federated plugin'), contains('Skipped 1 package(s)'), ]), ); }); test('skips interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); final Directory platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ platformInterface.childDirectory('lib').childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), ]; final List output = await runCapturingPrint(runner, ['federation-safety-check']); expect( output, containsAllInOrder([ contains('Running for foo_platform_interface...'), contains('Platform interface changes are not validated.'), contains('Skipped 1 package(s)'), ]), ); }); test('allows changes to multiple non-interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); final Directory appFacing = createFakePlugin('foo', pluginGroupDir); final Directory implementation = createFakePlugin('foo_bar', pluginGroupDir); createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ appFacing.childFile('foo.dart'), implementation.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), ]; final List output = await runCapturingPrint(runner, ['federation-safety-check']); expect( output, containsAllInOrder([ contains('Running for foo/foo...'), contains('No published changes for foo_platform_interface.'), contains('Running for foo_bar...'), contains('No published changes for foo_platform_interface.'), ]), ); }); test( 'fails on changes to interface and non-interface packages in the same plugin', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); final Directory appFacing = createFakePlugin('foo', pluginGroupDir); final Directory implementation = createFakePlugin('foo_bar', pluginGroupDir); final Directory platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ appFacing.childFile('foo.dart'), implementation.childFile('foo.dart'), platformInterface.childFile('pubspec.yaml'), platformInterface.childDirectory('lib').childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['federation-safety-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Running for foo/foo...'), contains('Dart changes are not allowed to other packages in foo in the ' 'same PR as changes to public Dart code in foo_platform_interface, ' 'as this can cause accidental breaking changes to be missed by ' 'automated checks. Please split the changes to these two packages ' 'into separate PRs.'), contains('Running for foo_bar...'), contains('Dart changes are not allowed to other packages in foo'), contains('The following packages had errors:'), contains('foo/foo:\n' ' foo_platform_interface changed.'), contains('foo_bar:\n' ' foo_platform_interface changed.'), ]), ); }); test('ignores test-only changes to interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); final Directory appFacing = createFakePlugin('foo', pluginGroupDir); final Directory implementation = createFakePlugin('foo_bar', pluginGroupDir); final Directory platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ appFacing.childFile('foo.dart'), implementation.childFile('foo.dart'), platformInterface.childFile('pubspec.yaml'), platformInterface.childDirectory('test').childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), ]; final List output = await runCapturingPrint(runner, ['federation-safety-check']); expect( output, containsAllInOrder([ contains('Running for foo/foo...'), contains('No public code changes for foo_platform_interface.'), contains('Running for foo_bar...'), contains('No public code changes for foo_platform_interface.'), ]), ); }); test('ignores unpublished changes to interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); final Directory appFacing = createFakePlugin('foo', pluginGroupDir); final Directory implementation = createFakePlugin('foo_bar', pluginGroupDir); final Directory platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ appFacing.childFile('foo.dart'), implementation.childFile('foo.dart'), platformInterface.childFile('pubspec.yaml'), platformInterface.childDirectory('lib').childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), ]; // Simulate no change to the version in the interface's pubspec.yaml. processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess( stdout: RepositoryPackage(platformInterface) .pubspecFile .readAsStringSync()), ]; final List output = await runCapturingPrint(runner, ['federation-safety-check']); expect( output, containsAllInOrder([ contains('Running for foo/foo...'), contains('No published changes for foo_platform_interface.'), contains('Running for foo_bar...'), contains('No published changes for foo_platform_interface.'), ]), ); }); test('allows things that look like mass changes, with warning', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); final Directory appFacing = createFakePlugin('foo', pluginGroupDir); final Directory implementation = createFakePlugin('foo_bar', pluginGroupDir); final Directory platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final Directory otherPlugin1 = createFakePlugin('bar', packagesDir); final Directory otherPlugin2 = createFakePlugin('baz', packagesDir); final String changedFileOutput = [ appFacing.childFile('foo.dart'), implementation.childFile('foo.dart'), platformInterface.childFile('pubspec.yaml'), platformInterface.childDirectory('lib').childFile('foo.dart'), otherPlugin1.childFile('bar.dart'), otherPlugin2.childFile('baz.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), ]; final List output = await runCapturingPrint(runner, ['federation-safety-check']); expect( output, containsAllInOrder([ contains('Running for foo/foo...'), contains( 'Ignoring potentially dangerous change, as this appears to be a mass change.'), contains('Running for foo_bar...'), contains( 'Ignoring potentially dangerous change, as this appears to be a mass change.'), contains('Ran for 2 package(s) (2 with warnings)'), ]), ); }); }