// 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:async'; import 'dart:convert'; import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common.dart'; import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; import 'common_test.mocks.dart'; import 'util.dart'; @GenerateMocks([GitDir]) void main() { late RecordingProcessRunner processRunner; late CommandRunner runner; late FileSystem fileSystem; late Directory packagesDir; late Directory thirdPartyPackagesDir; late List plugins; late List?> gitDirCommands; late String gitDiffResponse; setUp(() { fileSystem = MemoryFileSystem(); packagesDir = fileSystem.currentDirectory.childDirectory('packages'); thirdPartyPackagesDir = packagesDir.parent .childDirectory('third_party') .childDirectory('packages'); gitDirCommands = ?>[]; gitDiffResponse = ''; final MockGitDir gitDir = MockGitDir(); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { gitDirCommands.add(invocation.positionalArguments[0] as List?); final MockProcessResult mockProcessResult = MockProcessResult(); if (invocation.positionalArguments[0][0] == 'diff') { when(mockProcessResult.stdout as String?) .thenReturn(gitDiffResponse); } return Future.value(mockProcessResult); }); initializeFakePackages(parentDir: packagesDir.parent); processRunner = RecordingProcessRunner(); plugins = []; final SamplePluginCommand samplePluginCommand = SamplePluginCommand( plugins, packagesDir, fileSystem, processRunner: processRunner, gitDir: gitDir, ); runner = CommandRunner('common_command', 'Test for common functionality'); runner.addCommand(samplePluginCommand); }); test('all plugins from file system', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run(['sample']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins includes third_party/packages', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); final Directory plugin3 = createFakePlugin('plugin3', packagesDirectory: thirdPartyPackagesDir); await runner.run(['sample']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); }); test('exclude plugins when plugins flag is specified', () async { createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); expect(plugins, unorderedEquals([plugin2.path])); }); test('exclude plugins when plugins flag isn\'t specified', () async { createFakePlugin('plugin1', packagesDirectory: packagesDir); createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run(['sample', '--exclude=plugin1,plugin2']); expect(plugins, unorderedEquals([])); }); test('exclude federated plugins when plugins flag is specified', () async { createFakePlugin('plugin1', parentDirectoryName: 'federated', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run([ 'sample', '--plugins=federated/plugin1,plugin2', '--exclude=federated/plugin1' ]); expect(plugins, unorderedEquals([plugin2.path])); }); test('exclude entire federated plugins when plugins flag is specified', () async { createFakePlugin('plugin1', parentDirectoryName: 'federated', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run([ 'sample', '--plugins=federated/plugin1,plugin2', '--exclude=federated' ]); expect(plugins, unorderedEquals([plugin2.path])); }); group('test run-on-changed-packages', () { test('all plugins should be tested if there are no changes.', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if there are no plugin related changes.', () async { gitDiffResponse = 'AUTHORS'; final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if .cirrus.yml changes.', () async { gitDiffResponse = ''' .cirrus.yml packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if .ci.yaml changes', () async { gitDiffResponse = ''' .ci.yaml packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if anything in .ci/ changes', () async { gitDiffResponse = ''' .ci/Dockerfile packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if anything in script changes.', () async { gitDiffResponse = ''' script/tool_runner.sh packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if the root analysis options change.', () async { gitDiffResponse = ''' analysis_options.yaml packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if formatting options change.', () async { gitDiffResponse = ''' .clang-format packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('Only changed plugin should be tested.', () async { gitDiffResponse = 'packages/plugin1/plugin1.dart'; final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path])); }); test('multiple files in one plugin should also test the plugin', () async { gitDiffResponse = ''' packages/plugin1/plugin1.dart packages/plugin1/ios/plugin1.m '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path])); }); test('multiple plugins changed should test all the changed plugins', () async { gitDiffResponse = ''' packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); createFakePlugin('plugin3', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test( 'multiple plugins inside the same plugin group changed should output the plugin group name', () async { gitDiffResponse = ''' packages/plugin1/plugin1/plugin1.dart packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart packages/plugin1/plugin1_web/plugin1_web.dart '''; final Directory plugin1 = createFakePlugin('plugin1', parentDirectoryName: 'plugin1', packagesDirectory: packagesDir); createFakePlugin('plugin2', packagesDirectory: packagesDir); createFakePlugin('plugin3', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); expect(plugins, unorderedEquals([plugin1.path])); }); test('--plugins flag overrides the behavior of --run-on-changed-packages', () async { gitDiffResponse = ''' packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''; final Directory plugin1 = createFakePlugin('plugin1', parentDirectoryName: 'plugin1', packagesDirectory: packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDirectory: packagesDir); createFakePlugin('plugin3', packagesDirectory: packagesDir); await runner.run([ 'sample', '--plugins=plugin1,plugin2', '--base-sha=master', '--run-on-changed-packages' ]); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('--exclude flag works with --run-on-changed-packages', () async { gitDiffResponse = ''' packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''; final Directory plugin1 = createFakePlugin('plugin1', parentDirectoryName: 'plugin1', packagesDirectory: packagesDir); createFakePlugin('plugin2', packagesDirectory: packagesDir); createFakePlugin('plugin3', packagesDirectory: packagesDir); await runner.run([ 'sample', '--exclude=plugin2,plugin3', '--base-sha=master', '--run-on-changed-packages' ]); expect(plugins, unorderedEquals([plugin1.path])); }); }); group('$GitVersionFinder', () { late List?> gitDirCommands; late String gitDiffResponse; String? mergeBaseResponse; late MockGitDir gitDir; setUp(() { gitDirCommands = ?>[]; gitDiffResponse = ''; gitDir = MockGitDir(); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { gitDirCommands.add(invocation.positionalArguments[0] as List?); final MockProcessResult mockProcessResult = MockProcessResult(); if (invocation.positionalArguments[0][0] == 'diff') { when(mockProcessResult.stdout as String?) .thenReturn(gitDiffResponse); } else if (invocation.positionalArguments[0][0] == 'merge-base') { when(mockProcessResult.stdout as String?) .thenReturn(mergeBaseResponse); } return Future.value(mockProcessResult); }); initializeFakePackages(); processRunner = RecordingProcessRunner(); }); tearDown(() { cleanupPackages(); }); test('No git diff should result no files changed', () async { final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); final List changedFiles = await finder.getChangedFiles(); expect(changedFiles, isEmpty); }); test('get correct files changed based on git diff', () async { gitDiffResponse = ''' file1/file1.cc file2/file2.cc '''; final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); final List changedFiles = await finder.getChangedFiles(); expect( changedFiles, equals(['file1/file1.cc', 'file2/file2.cc'])); }); test('get correct pubspec change based on git diff', () async { gitDiffResponse = ''' file1/pubspec.yaml file2/file2.cc '''; final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); final List changedFiles = await finder.getChangedPubSpecs(); expect(changedFiles, equals(['file1/pubspec.yaml'])); }); test('use correct base sha if not specified', () async { mergeBaseResponse = 'shaqwiueroaaidf12312jnadf123nd'; gitDiffResponse = ''' file1/pubspec.yaml file2/file2.cc '''; final GitVersionFinder finder = GitVersionFinder(gitDir, null); await finder.getChangedFiles(); verify(gitDir.runCommand( ['diff', '--name-only', mergeBaseResponse!, 'HEAD'])); }); test('use correct base sha if specified', () async { const String customBaseSha = 'aklsjdcaskf12312'; gitDiffResponse = ''' file1/pubspec.yaml file2/file2.cc '''; final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); await finder.getChangedFiles(); verify(gitDir .runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); }); }); group('$PubVersionFinder', () { test('Package does not exist.', () async { final MockClient mockClient = MockClient((http.Request request) async { return http.Response('', 404); }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = await finder.getPackageVersion(package: 'some_package'); expect(response.versions, isNull); expect(response.result, PubVersionFinderResult.noPackageFound); expect(response.httpResponse!.statusCode, 404); expect(response.httpResponse!.body, ''); }); test('HTTP error when getting versions from pub', () async { final MockClient mockClient = MockClient((http.Request request) async { return http.Response('', 400); }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = await finder.getPackageVersion(package: 'some_package'); expect(response.versions, isNull); expect(response.result, PubVersionFinderResult.fail); expect(response.httpResponse!.statusCode, 400); expect(response.httpResponse!.body, ''); }); test('Get a correct list of versions when http response is OK.', () async { const Map httpResponse = { 'name': 'some_package', 'versions': [ '0.0.1', '0.0.2', '0.0.2+2', '0.1.1', '0.0.1+1', '0.1.0', '0.2.0', '0.1.0+1', '0.0.2+1', '2.0.0', '1.2.0', '1.0.0', ], }; final MockClient mockClient = MockClient((http.Request request) async { return http.Response(json.encode(httpResponse), 200); }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = await finder.getPackageVersion(package: 'some_package'); expect(response.versions, [ Version.parse('2.0.0'), Version.parse('1.2.0'), Version.parse('1.0.0'), Version.parse('0.2.0'), Version.parse('0.1.1'), Version.parse('0.1.0+1'), Version.parse('0.1.0'), Version.parse('0.0.2+2'), Version.parse('0.0.2+1'), Version.parse('0.0.2'), Version.parse('0.0.1+1'), Version.parse('0.0.1'), ]); expect(response.result, PubVersionFinderResult.success); expect(response.httpResponse!.statusCode, 200); expect(response.httpResponse!.body, json.encode(httpResponse)); }); }); } class SamplePluginCommand extends PluginCommand { SamplePluginCommand( this._plugins, Directory packagesDir, FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), GitDir? gitDir, }) : super(packagesDir, fileSystem, processRunner: processRunner, gitDir: gitDir); final List _plugins; @override final String name = 'sample'; @override final String description = 'sample command'; @override Future run() async { await for (final Directory package in getPlugins()) { _plugins.add(package.path); } } } class MockProcessResult extends Mock implements ProcessResult {}