| // Copyright 2013 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'package:args/command_runner.dart'; | |
| import 'package:file/file.dart'; | |
| import 'package:flutter_plugin_tools/src/analyze_command.dart'; | |
| import 'package:flutter_plugin_tools/src/common/core.dart'; | |
| import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; | |
| import 'package:git/git.dart'; | |
| import 'package:platform/platform.dart'; | |
| import 'package:test/test.dart'; | |
| import 'mocks.dart'; | |
| import 'util.dart'; | |
| void main() { | |
| late NativePlatform mockPlatform; | |
| late Directory packagesDir; | |
| late RecordingProcessRunner processRunner; | |
| late RecordingProcessRunner gitProcessRunner; | |
| late CommandRunner<void> runner; | |
| setUp(() { | |
| mockPlatform = createMockPlatform(); | |
| final GitDir gitDir; | |
| (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) = configureBaseCommandMocks( | |
| platform: mockPlatform, | |
| ); | |
| final analyzeCommand = AnalyzeCommand( | |
| packagesDir, | |
| processRunner: processRunner, | |
| gitDir: gitDir, | |
| platform: mockPlatform, | |
| ); | |
| runner = CommandRunner<void>('analyze_command', 'Test for analyze_command'); | |
| runner.addCommand(analyzeCommand); | |
| }); | |
| test('throws if no analysis options are included', () async { | |
| createFakePackage('a', packagesDir); | |
| await expectLater( | |
| () => runCapturingPrint(runner, <String>['analyze', '--no-dart']), | |
| throwsA(isA<ToolExit>()), | |
| ); | |
| }); | |
| group('result aggregation', () { | |
| test('repeorts failure if any analysis fails', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| extraFiles: <String>['example/android/gradlew'], | |
| platformSupport: <String, PlatformDetails>{ | |
| platformAndroid: const PlatformDetails(PlatformSupport.inline), | |
| platformIOS: const PlatformDetails(PlatformSupport.inline), | |
| platformMacOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| plugin.platformDirectory(FlutterPlatform.android).createSync(recursive: true); | |
| // Simulate Android analysis failure only. | |
| final String gradlewPath = plugin | |
| .getExamples() | |
| .first | |
| .platformDirectory(FlutterPlatform.android) | |
| .childFile('gradlew') | |
| .path; | |
| processRunner.mockProcessesForExecutable[gradlewPath] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1)), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze', '--android', '--ios', '--macos'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect(output, containsAllInOrder(<Matcher>[contains('The following packages had errors:')])); | |
| }); | |
| test('reports skip if everything is skipped', () async { | |
| createFakePlugin('plugin', packagesDir); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--android', | |
| '--ios', | |
| '--macos', | |
| ]); | |
| expect(output, containsAllInOrder(<Matcher>[contains('SKIPPING:')])); | |
| expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[])); | |
| }); | |
| test('reports success for a mixture of skip and success', () async { | |
| createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformIOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--android', | |
| '--ios', | |
| '--macos', | |
| ]); | |
| expect(output, containsAllInOrder(<Matcher>[contains('No issues found')])); | |
| expect(output, isNot(containsAllInOrder(<Matcher>[contains('SKIPPING:')]))); | |
| }); | |
| }); | |
| group('dart analyze', () { | |
| test('analyzes all packages', () async { | |
| // Create a non-Flutter Dart package and a Flutter plugin to make sure | |
| // the right command is used for each. | |
| final RepositoryPackage package1 = createFakePackage('a', packagesDir); | |
| final RepositoryPackage plugin2 = createFakePlugin('b', packagesDir); | |
| await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('dart', const <String>['pub', 'get'], package1.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package1.path), | |
| ProcessCall('flutter', const <String>['pub', 'get'], plugin2.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], plugin2.path), | |
| ]), | |
| ); | |
| }); | |
| test('skips pub get for examples', () async { | |
| final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); | |
| await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], plugin1.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], plugin1.path), | |
| ]), | |
| ); | |
| }); | |
| test('runs pub get for non-example subpackages', () async { | |
| final RepositoryPackage mainPackage = createFakePackage('a', packagesDir); | |
| final Directory otherPackagesDir = mainPackage.directory.childDirectory('other_packages'); | |
| final RepositoryPackage subpackage1 = createFakePackage('subpackage1', otherPackagesDir); | |
| final RepositoryPackage subpackage2 = createFakePackage('subpackage2', otherPackagesDir); | |
| await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('dart', const <String>['pub', 'get'], mainPackage.path), | |
| ProcessCall('dart', const <String>['pub', 'get'], subpackage1.path), | |
| ProcessCall('dart', const <String>['pub', 'get'], subpackage2.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], mainPackage.path), | |
| ]), | |
| ); | |
| }); | |
| test('runs flutter pub get for non-Flutter packages with Flutter examples', () async { | |
| final RepositoryPackage mainPackage = createFakePackage('a', packagesDir, examples: []); | |
| createFakePackage('example', mainPackage.directory, examples: [], isFlutter: true); | |
| await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], mainPackage.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], mainPackage.path), | |
| ]), | |
| ); | |
| }); | |
| test('passes lib/ directory with --lib-only', () async { | |
| final RepositoryPackage package = createFakePackage('a_package', packagesDir); | |
| await runCapturingPrint(runner, <String>['analyze', '--lib-only']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('dart', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos', 'lib'], package.path), | |
| ]), | |
| ); | |
| }); | |
| test('skips when missing lib/ directory with --lib-only', () async { | |
| final RepositoryPackage package = createFakePackage('a_package', packagesDir); | |
| package.libDirectory.deleteSync(); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--lib-only', | |
| ]); | |
| expect(processRunner.recordedCalls, isEmpty); | |
| expect(output, containsAllInOrder(<Matcher>[contains('SKIPPING: No lib/ directory')])); | |
| }); | |
| test('does not run pub get for non-example subpackages with --lib-only', () async { | |
| final RepositoryPackage mainPackage = createFakePackage('a', packagesDir); | |
| final Directory otherPackagesDir = mainPackage.directory.childDirectory('other_packages'); | |
| createFakePackage('subpackage1', otherPackagesDir); | |
| createFakePackage('subpackage2', otherPackagesDir); | |
| await runCapturingPrint(runner, <String>['analyze', '--lib-only']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('dart', const <String>['pub', 'get'], mainPackage.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos', 'lib'], mainPackage.path), | |
| ]), | |
| ); | |
| }); | |
| test("don't elide a non-contained example package", () async { | |
| final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); | |
| final RepositoryPackage plugin2 = createFakePlugin('example', packagesDir); | |
| await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], plugin1.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], plugin1.path), | |
| ProcessCall('flutter', const <String>['pub', 'get'], plugin2.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], plugin2.path), | |
| ]), | |
| ); | |
| }); | |
| test('uses a separate analysis sdk', () async { | |
| final RepositoryPackage plugin = createFakePlugin('a', packagesDir); | |
| await runCapturingPrint(runner, <String>['analyze', '--analysis-sdk', 'foo/bar/baz']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], plugin.path), | |
| ProcessCall('foo/bar/baz/bin/dart', const <String>[ | |
| 'analyze', | |
| '--fatal-infos', | |
| ], plugin.path), | |
| ]), | |
| ); | |
| }); | |
| test('downgrades first when requested', () async { | |
| final RepositoryPackage package = createFakePackage('a', packagesDir); | |
| await runCapturingPrint(runner, <String>['analyze', '--downgrade']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('dart', const <String>['pub', 'downgrade'], package.path), | |
| ProcessCall('dart', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path), | |
| ]), | |
| ); | |
| }); | |
| test('downgrades using flutter for Flutter packages', () async { | |
| final RepositoryPackage package = createFakePackage('a', packagesDir, isFlutter: true); | |
| await runCapturingPrint(runner, <String>['analyze', '--downgrade']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'downgrade'], package.path), | |
| ProcessCall('flutter', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path), | |
| ]), | |
| ); | |
| }); | |
| test('downgrades using flutter for non-Flutter packages with Flutter examples', () async { | |
| final RepositoryPackage package = createFakePackage('a', packagesDir, examples: []); | |
| createFakePackage('example', package.directory, examples: [], isFlutter: true); | |
| await runCapturingPrint(runner, <String>['analyze', '--downgrade']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'downgrade'], package.path), | |
| ProcessCall('flutter', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path), | |
| ]), | |
| ); | |
| }); | |
| group('verifies analysis settings', () { | |
| test('fails for unexpected analysis_options.yaml', () async { | |
| createFakePlugin('foo', packagesDir, extraFiles: <String>['analysis_options.yaml']); | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains( | |
| 'Found an unexpected analysis_options.yaml at /packages/foo/analysis_options.yaml', | |
| ), | |
| contains( | |
| ' foo:\n' | |
| ' Unexpected local analysis options', | |
| ), | |
| ]), | |
| ); | |
| }); | |
| test('fails on .analysis_options', () async { | |
| createFakePlugin('foo', packagesDir, extraFiles: <String>['.analysis_options']); | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains( | |
| 'Found an unexpected analysis_options.yaml at /packages/foo/.analysis_options', | |
| ), | |
| contains( | |
| ' foo:\n' | |
| ' Unexpected local analysis options', | |
| ), | |
| ]), | |
| ); | |
| }); | |
| test('ignores analysis options in the plugin .symlinks directory', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'foo', | |
| packagesDir, | |
| extraFiles: <String>['analysis_options.yaml'], | |
| ); | |
| plugin.ciConfigFile.writeAsStringSync('allow_custom_analysis_options: true'); | |
| final RepositoryPackage includingPackage = createFakePlugin('bar', packagesDir); | |
| // Simulate the local state of having built 'bar' if it includes 'foo'. | |
| includingPackage.directory | |
| .childDirectory('example') | |
| .childDirectory('ios') | |
| .childLink('.symlinks') | |
| .createSync(plugin.directory.path, recursive: true); | |
| await runCapturingPrint(runner, <String>['analyze']); | |
| }); | |
| test('allows analysis options when configured', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'foo', | |
| packagesDir, | |
| extraFiles: <String>['analysis_options.yaml'], | |
| ); | |
| plugin.ciConfigFile.writeAsStringSync('allow_custom_analysis_options: true'); | |
| await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], plugin.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], plugin.path), | |
| ]), | |
| ); | |
| }); | |
| }); | |
| group('cognitive_complexity', () { | |
| test('runs cognitive_complexity if present in dev_dependencies', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| isFlutter: true, | |
| ); | |
| _writeFakePubspecWithLinter(package, inDevDependencies: true); | |
| package.libDirectory.childFile('lib.dart').createSync(); | |
| _mockCallsForFlutterAnalyze( | |
| processRunner, | |
| extraDartCalls: [ | |
| FakeProcessInfo(MockProcess(), <String>['run', 'cognitive_complexity']), | |
| ], | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path), | |
| ProcessCall('dart', const <String>[ | |
| 'run', | |
| 'cognitive_complexity', | |
| 'lib/lib.dart', | |
| ], package.path), | |
| ]), | |
| ); | |
| expect(output, contains('Running cognitive_complexity analysis...')); | |
| }); | |
| test('does not run cognitive_complexity when --downgrade is specified', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| isFlutter: true, | |
| ); | |
| _writeFakePubspecWithLinter(package, inDevDependencies: true); | |
| package.libDirectory.childFile('lib.dart').createSync(); | |
| processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(), <String>['pub', 'downgrade']), | |
| FakeProcessInfo(MockProcess(), <String>['pub', 'get']), | |
| ]; | |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(), <String>['analyze', '--fatal-infos']), | |
| ]; | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--downgrade', | |
| ]); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'downgrade'], package.path), | |
| ProcessCall('flutter', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path), | |
| ]), | |
| ); | |
| final String combinedOutput = output.join('\n').toLowerCase(); | |
| expect(combinedOutput, isNot(contains('cognitive_complexity'))); | |
| }); | |
| test('does not run cognitive_complexity if present in dependencies', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| isFlutter: true, | |
| ); | |
| _writeFakePubspecWithLinter(package, inDependencies: true); | |
| package.libDirectory.childFile('lib.dart').createSync(); | |
| _mockCallsForFlutterAnalyze(processRunner); | |
| final List<String> output = await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path), | |
| ]), | |
| ); | |
| final String combinedOutput = output.join('\n').toLowerCase(); | |
| expect(combinedOutput, isNot(contains('cognitive_complexity'))); | |
| }); | |
| test('does not run cognitive_complexity if not present', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| isFlutter: true, | |
| ); | |
| package.libDirectory.childFile('lib.dart').createSync(); | |
| _mockCallsForFlutterAnalyze(processRunner); | |
| final List<String> output = await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path), | |
| ]), | |
| ); | |
| final String combinedOutput = output.join('\n').toLowerCase(); | |
| expect(combinedOutput, isNot(contains('cognitive_complexity'))); | |
| }); | |
| test('runs cognitive_complexity using dart for pure Dart packages', () async { | |
| final RepositoryPackage package = createFakePackage('a_package', packagesDir); | |
| _writeFakePubspecWithLinter(package, inDevDependencies: true, includeFlutter: false); | |
| package.libDirectory.childFile('lib.dart').createSync(); | |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(), <String>['pub', 'get']), | |
| FakeProcessInfo(MockProcess(), <String>['analyze']), | |
| FakeProcessInfo(MockProcess(), <String>['run', 'cognitive_complexity']), | |
| ]; | |
| final List<String> output = await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('dart', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path), | |
| ProcessCall('dart', const <String>[ | |
| 'run', | |
| 'cognitive_complexity', | |
| 'lib/lib.dart', | |
| ], package.path), | |
| ]), | |
| ); | |
| expect(output, contains('Running cognitive_complexity analysis...')); | |
| }); | |
| test('skips cognitive_complexity if lib/ does not exist', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| isFlutter: true, | |
| ); | |
| _writeFakePubspecWithLinter(package, inDevDependencies: true); | |
| package.libDirectory.deleteSync(recursive: true); | |
| _mockCallsForFlutterAnalyze(processRunner); | |
| final List<String> output = await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path), | |
| ]), | |
| ); | |
| final String combinedOutput = output.join('\n').toLowerCase(); | |
| expect(combinedOutput, isNot(contains('cognitive_complexity'))); | |
| }); | |
| test('skips cognitive_complexity if lib/ only contains generated files', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| isFlutter: true, | |
| ); | |
| _writeFakePubspecWithLinter(package, inDevDependencies: true); | |
| package.libDirectory.childFile('foo.g.dart').createSync(); | |
| package.libDirectory.childFile('bar.mocks.dart').createSync(); | |
| package.libDirectory.childFile('baz.gen.dart').createSync(); | |
| _mockCallsForFlutterAnalyze(processRunner); | |
| final List<String> output = await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path), | |
| ]), | |
| ); | |
| final String combinedOutput = output.join('\n').toLowerCase(); | |
| expect(combinedOutput, isNot(contains('cognitive_complexity'))); | |
| }); | |
| test('fails if cognitive_complexity analysis fails', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| isFlutter: true, | |
| ); | |
| _writeFakePubspecWithLinter(package, inDevDependencies: true); | |
| package.libDirectory.childFile('lib.dart').createSync(); | |
| _mockCallsForFlutterAnalyze( | |
| processRunner, | |
| extraDartCalls: [ | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['run', 'cognitive_complexity']), | |
| ], | |
| ); | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains( | |
| 'Metrics violations found. See the package\'s local "analysis_options.yaml" for configured thresholds.', | |
| ), | |
| ]), | |
| ); | |
| }); | |
| test('fails with threshold if analysis_options.yaml defines one', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| isFlutter: true, | |
| ); | |
| _writeFakePubspecWithLinter(package, inDevDependencies: true); | |
| package.libDirectory.childFile('lib.dart').createSync(); | |
| package.ciConfigFile.writeAsStringSync('allow_custom_analysis_options: true'); | |
| package.directory.childFile('analysis_options.yaml').writeAsStringSync(''' | |
| cognitive_complexity: | |
| fail-threshold: 15 | |
| '''); | |
| _mockCallsForFlutterAnalyze( | |
| processRunner, | |
| extraDartCalls: [ | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['run', 'cognitive_complexity']), | |
| ], | |
| ); | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path), | |
| ProcessCall('dart', const <String>[ | |
| 'run', | |
| 'cognitive_complexity', | |
| '--threshold', | |
| '15', | |
| '--fail-threshold', | |
| '15', | |
| 'lib/lib.dart', | |
| ], package.path), | |
| ]), | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains( | |
| 'Metrics violations found (configured threshold: 15). See the package\'s local "analysis_options.yaml" for configured thresholds.', | |
| ), | |
| ]), | |
| ); | |
| }); | |
| }); | |
| group('skills analysis via ci_config.yaml', () { | |
| test('fails if package configuration has no dart files in .agents/skills', () async { | |
| final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); | |
| plugin.ciConfigFile.writeAsStringSync('analyze_skills: true'); | |
| // Note: we purposely do not create any .dart files in .agents/skills | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains( | |
| 'Configured to analyze skills for foo, but no Dart code was found in .agents/skills.', | |
| ), | |
| ]), | |
| ); | |
| }); | |
| test( | |
| 'analyzes .agents/skills when dart files are present and analyze_skills is true', | |
| () async { | |
| final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); | |
| plugin.ciConfigFile.writeAsStringSync('analyze_skills: true'); | |
| plugin.directory | |
| .childDirectory('.agents') | |
| .childDirectory('skills') | |
| .childFile('test.dart') | |
| .createSync(recursive: true); | |
| await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], plugin.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], plugin.path), | |
| ProcessCall('dart', const <String>[ | |
| 'analyze', | |
| '--fatal-infos', | |
| '.agents/skills', | |
| ], plugin.path), | |
| ]), | |
| ); | |
| }, | |
| ); | |
| test('does not analyze .agents/skills when analyze_skills is false/omitted', () async { | |
| final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); | |
| plugin.directory | |
| .childDirectory('.agents') | |
| .childDirectory('skills') | |
| .childFile('test.dart') | |
| .createSync(recursive: true); | |
| await runCapturingPrint(runner, <String>['analyze']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], plugin.path), | |
| ProcessCall('dart', const <String>['analyze', '--fatal-infos'], plugin.path), | |
| ]), | |
| ); | |
| }); | |
| test('fails with both main and skills failures listed if both fail', () async { | |
| final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); | |
| plugin.ciConfigFile.writeAsStringSync('analyze_skills: true'); | |
| plugin.directory | |
| .childDirectory('.agents') | |
| .childDirectory('skills') | |
| .childFile('test.dart') | |
| .createSync(recursive: true); | |
| // Mock both dart analyze runs to fail (exit code 1) | |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['analyze']), // main package | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['analyze']), // skills package | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| final String joinedOutput = output.join('\n'); | |
| expect(joinedOutput, contains('The following packages had errors:')); | |
| expect( | |
| joinedOutput, | |
| contains(' foo:\n Main package analysis failed\n Skills analysis failed'), | |
| ); | |
| }); | |
| test( | |
| 'fails with package analysis, skills analysis, and custom linter violations listed if all fail', | |
| () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| isFlutter: true, | |
| ); | |
| _writeFakePubspecWithLinter(package, inDevDependencies: true); | |
| package.libDirectory.childFile('lib.dart').createSync(); | |
| package.ciConfigFile.writeAsStringSync('analyze_skills: true'); | |
| package.directory | |
| .childDirectory('.agents') | |
| .childDirectory('skills') | |
| .childFile('test.dart') | |
| .createSync(recursive: true); | |
| processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(), <String>['pub', 'get']), | |
| ]; | |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['analyze']), // main package | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['analyze']), // skills package | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>[ | |
| 'run', | |
| 'cognitive_complexity', | |
| ]), // custom linter | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| final String joinedOutput = output.join('\n'); | |
| expect(joinedOutput, contains('The following packages had errors:')); | |
| expect( | |
| joinedOutput, | |
| contains( | |
| ' a_package:\n' | |
| ' Main package analysis failed\n' | |
| ' Skills analysis failed\n' | |
| ' Metrics violations found. See the package\'s local "analysis_options.yaml" for configured thresholds.', | |
| ), | |
| ); | |
| }, | |
| ); | |
| }); | |
| test('skips if requested if "pub get" fails in the resolver', () async { | |
| final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); | |
| final failingPubGet = FakeProcessInfo( | |
| MockProcess( | |
| exitCode: 1, | |
| stderr: | |
| 'So, because foo depends on both thing_one ^1.0.0 and ' | |
| 'thing_two from path, version solving failed.', | |
| ), | |
| <String>['pub', 'get'], | |
| ); | |
| processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[ | |
| failingPubGet, | |
| // The command re-runs failures when --skip-if-resolver-fails is passed | |
| // to check the output, so provide the same failing outcome. | |
| failingPubGet, | |
| ]; | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--skip-if-resolving-fails', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('Skipping package due to pub resolution failure.')]), | |
| ); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], plugin.path), | |
| ProcessCall('flutter', const <String>['pub', 'get'], plugin.path), | |
| ]), | |
| ); | |
| }); | |
| test('fails if "pub get" fails', () async { | |
| createFakePlugin('foo', packagesDir); | |
| processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'get']), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect(output, containsAllInOrder(<Matcher>[contains('Unable to get dependencies')])); | |
| }); | |
| test('fails if "pub downgrade" fails', () async { | |
| createFakePlugin('foo', packagesDir); | |
| processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'downgrade']), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze', '--downgrade'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('Unable to resolve downgraded dependencies')]), | |
| ); | |
| }); | |
| test('fails if "analyze" fails', () async { | |
| createFakePlugin('foo', packagesDir); | |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['analyze']), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('The following packages had errors:'), | |
| contains(' foo'), | |
| ]), | |
| ); | |
| }); | |
| // Ensure that the command used to analyze flutter/plugins in the Dart repo: | |
| // https://github.com/dart-lang/sdk/blob/main/tools/bots/flutter/analyze_flutter_plugins.sh | |
| // continues to work. | |
| // | |
| // DO NOT remove or modify this test without a coordination plan in place to | |
| // modify the script above, as it is run from source, but out-of-repo. | |
| // Contact stuartmorgan or devoncarew for assistance. | |
| test('Dart repo analyze command works', () async { | |
| final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); | |
| final File allowFile = packagesDir.childFile('custom.yaml'); | |
| // Intentionally do not create the file; this ensures that removing the legacy file from this | |
| // repository won't break the other callers. | |
| await runCapturingPrint(runner, <String>[ | |
| // DO NOT change this call; see comment above. | |
| 'analyze', | |
| '--analysis-sdk', | |
| 'foo/bar/baz', | |
| '--custom-analysis', | |
| allowFile.path, | |
| ]); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['pub', 'get'], plugin.path), | |
| ProcessCall('foo/bar/baz/bin/dart', const <String>[ | |
| 'analyze', | |
| '--fatal-infos', | |
| ], plugin.path), | |
| ]), | |
| ); | |
| }); | |
| group('file filtering', () { | |
| test('runs command for changes to Dart source', () async { | |
| createFakePackage('package_a', packagesDir); | |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ | |
| FakeProcessInfo( | |
| MockProcess( | |
| stdout: ''' | |
| packages/package_a/foo.dart | |
| ''', | |
| ), | |
| ), | |
| ]; | |
| final List<String> output = await runCapturingPrint(runner, <String>['analyze']); | |
| expect(output, containsAllInOrder(<Matcher>[contains('Running for package_a')])); | |
| }); | |
| const files = <String>[ | |
| 'foo.java', | |
| 'foo.kt', | |
| 'foo.m', | |
| 'foo.swift', | |
| 'foo.c', | |
| 'foo.cc', | |
| 'foo.cpp', | |
| 'foo.h', | |
| ]; | |
| for (final file in files) { | |
| test('skips command for changes to non-Dart source $file', () async { | |
| createFakePackage('package_a', packagesDir); | |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ | |
| FakeProcessInfo( | |
| MockProcess( | |
| stdout: | |
| ''' | |
| packages/package_a/$file | |
| ''', | |
| ), | |
| ), | |
| ]; | |
| final List<String> output = await runCapturingPrint(runner, <String>['analyze']); | |
| expect(output, isNot(containsAllInOrder(<Matcher>[contains('Running for package_a')]))); | |
| expect(output, containsAllInOrder(<Matcher>[contains('SKIPPING ALL PACKAGES')])); | |
| }); | |
| } | |
| test('skips commands if all files should be ignored', () async { | |
| createFakePackage('package_a', packagesDir); | |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ | |
| FakeProcessInfo( | |
| MockProcess( | |
| stdout: ''' | |
| README.md | |
| SUGGESTED_REVIEWERS.md | |
| packages/package_a/CHANGELOG.md | |
| ''', | |
| ), | |
| ), | |
| ]; | |
| final List<String> output = await runCapturingPrint(runner, <String>['analyze']); | |
| expect(output, isNot(containsAllInOrder(<Matcher>[contains('Running for package_a')]))); | |
| expect(output, containsAllInOrder(<Matcher>[contains('SKIPPING ALL PACKAGES')])); | |
| }); | |
| }); | |
| }); | |
| group('gradle lint', () { | |
| test('runs gradle lint', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| extraFiles: <String>['example/android/gradlew'], | |
| platformSupport: <String, PlatformDetails>{ | |
| platformAndroid: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| plugin.platformDirectory(FlutterPlatform.android).createSync(recursive: true); | |
| final Directory androidDir = plugin.getExamples().first.platformDirectory( | |
| FlutterPlatform.android, | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--android', | |
| '--no-dart', | |
| ]); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall(androidDir.childFile('gradlew').path, const <String>[ | |
| 'plugin1:lintDebug', | |
| ], androidDir.path), | |
| ]), | |
| ); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('Running for plugin1'), | |
| contains('No issues found!'), | |
| ]), | |
| ); | |
| }); | |
| test('runs on all examples', () async { | |
| final examples = <String>['example1', 'example2']; | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| examples: examples, | |
| extraFiles: <String>[ | |
| 'example/example1/android/gradlew', | |
| 'example/example2/android/gradlew', | |
| ], | |
| platformSupport: <String, PlatformDetails>{ | |
| platformAndroid: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| plugin.platformDirectory(FlutterPlatform.android).createSync(recursive: true); | |
| final Iterable<Directory> exampleAndroidDirs = plugin.getExamples().map( | |
| (RepositoryPackage example) => example.platformDirectory(FlutterPlatform.android), | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--android', | |
| '--no-dart', | |
| ]); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| for (final Directory directory in exampleAndroidDirs) | |
| ProcessCall(directory.childFile('gradlew').path, const <String>[ | |
| 'plugin1:lintDebug', | |
| ], directory.path), | |
| ]), | |
| ); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('Running for plugin1'), | |
| contains('No issues found!'), | |
| ]), | |
| ); | |
| }); | |
| test('runs --config-only build if gradlew is missing', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformAndroid: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| plugin.platformDirectory(FlutterPlatform.android).createSync(recursive: true); | |
| final Directory androidDir = plugin.getExamples().first.platformDirectory( | |
| FlutterPlatform.android, | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--android', | |
| '--no-dart', | |
| ]); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall(getFlutterCommand(mockPlatform), const <String>[ | |
| 'build', | |
| 'apk', | |
| '--config-only', | |
| ], plugin.getExamples().first.directory.path), | |
| ProcessCall(androidDir.childFile('gradlew').path, const <String>[ | |
| 'plugin1:lintDebug', | |
| ], androidDir.path), | |
| ]), | |
| ); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('Running for plugin1'), | |
| contains('No issues found!'), | |
| ]), | |
| ); | |
| }); | |
| test('fails if gradlew generation fails', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformAndroid: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| plugin.platformDirectory(FlutterPlatform.android).createSync(recursive: true); | |
| processRunner.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1)), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze', '--android', '--no-dart'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect(output, containsAllInOrder(<Matcher>[contains('Unable to configure Gradle project')])); | |
| }); | |
| test('fails if linting finds issues', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| extraFiles: <String>['example/android/gradlew'], | |
| platformSupport: <String, PlatformDetails>{ | |
| platformAndroid: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| plugin.platformDirectory(FlutterPlatform.android).createSync(recursive: true); | |
| final String gradlewPath = plugin | |
| .getExamples() | |
| .first | |
| .platformDirectory(FlutterPlatform.android) | |
| .childFile('gradlew') | |
| .path; | |
| processRunner.mockProcessesForExecutable[gradlewPath] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1)), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze', '--android', '--no-dart'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect(output, containsAllInOrder(<Matcher>[contains('The following packages had errors:')])); | |
| }); | |
| test('skips non-Android plugins', () async { | |
| createFakePlugin('plugin1', packagesDir); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--android', | |
| '--no-dart', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('SKIPPING: Package does not contain native Android plugin code'), | |
| ]), | |
| ); | |
| }); | |
| test('skips non-inline plugins', () async { | |
| createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformAndroid: const PlatformDetails(PlatformSupport.federated), | |
| }, | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--android', | |
| '--no-dart', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('SKIPPING: Package does not contain native Android plugin code'), | |
| ]), | |
| ); | |
| }); | |
| test('skips Dart-only plugins', () async { | |
| createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformAndroid: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--android', | |
| '--no-dart', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('SKIPPING: Package does not contain native Android plugin code'), | |
| ]), | |
| ); | |
| }); | |
| group('file filtering', () { | |
| const files = <String>['foo.java', 'foo.kt']; | |
| for (final file in files) { | |
| test('runs command for changes to $file', () async { | |
| createFakePackage('package_a', packagesDir); | |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ | |
| FakeProcessInfo( | |
| MockProcess( | |
| stdout: | |
| ''' | |
| packages/package_a/$file | |
| ''', | |
| ), | |
| ), | |
| ]; | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--android', | |
| '--no-dart', | |
| ]); | |
| expect(output, containsAllInOrder(<Matcher>[contains('Running for package_a')])); | |
| }); | |
| } | |
| test('skips commands if all files should be ignored', () async { | |
| createFakePackage('package_a', packagesDir); | |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ | |
| FakeProcessInfo( | |
| MockProcess( | |
| stdout: ''' | |
| README.md | |
| SUGGESTED_REVIEWERS.md | |
| packages/package_a/CHANGELOG.md | |
| packages/package_a/lib/foo.dart | |
| ''', | |
| ), | |
| ), | |
| ]; | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--android', | |
| '--no-dart', | |
| ]); | |
| expect(output, isNot(containsAllInOrder(<Matcher>[contains('Running for package_a')]))); | |
| expect(output, containsAllInOrder(<Matcher>[contains('SKIPPING ALL PACKAGES')])); | |
| }); | |
| }); | |
| }); | |
| group('Xcode analyze', () { | |
| group('iOS', () { | |
| test('skip if iOS is not supported', () async { | |
| createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformMacOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--ios', | |
| ]); | |
| expect(output, contains(contains('Package does not contain native iOS plugin code'))); | |
| expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[])); | |
| }); | |
| test('skip if iOS is implemented in a federated package', () async { | |
| createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformIOS: const PlatformDetails(PlatformSupport.federated), | |
| }, | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--ios', | |
| ]); | |
| expect(output, contains(contains('Package does not contain native iOS plugin code'))); | |
| expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[])); | |
| }); | |
| test('runs for iOS plugin', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformIOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| final Directory pluginExampleDirectory = getExampleDir(plugin); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--ios', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('Running for plugin'), | |
| contains('plugin/example (iOS) passed analysis.'), | |
| ]), | |
| ); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>[ | |
| 'build', | |
| 'ios', | |
| '--debug', | |
| '--config-only', | |
| ], pluginExampleDirectory.path), | |
| ProcessCall('xcrun', const <String>[ | |
| 'xcodebuild', | |
| 'clean', | |
| 'analyze', | |
| '-workspace', | |
| 'ios/Runner.xcworkspace', | |
| '-scheme', | |
| 'Runner', | |
| '-configuration', | |
| 'Debug', | |
| '-destination', | |
| 'generic/platform=iOS Simulator', | |
| 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', | |
| ], pluginExampleDirectory.path), | |
| ]), | |
| ); | |
| }); | |
| test('passes min iOS deployment version when requested', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformIOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| final Directory pluginExampleDirectory = getExampleDir(plugin); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--ios', | |
| '--ios-min-version=14.0', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('Running for plugin'), | |
| contains('plugin/example (iOS) passed analysis.'), | |
| ]), | |
| ); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>[ | |
| 'build', | |
| 'ios', | |
| '--debug', | |
| '--config-only', | |
| ], pluginExampleDirectory.path), | |
| ProcessCall('xcrun', const <String>[ | |
| 'xcodebuild', | |
| 'clean', | |
| 'analyze', | |
| '-workspace', | |
| 'ios/Runner.xcworkspace', | |
| '-scheme', | |
| 'Runner', | |
| '-configuration', | |
| 'Debug', | |
| '-destination', | |
| 'generic/platform=iOS Simulator', | |
| 'IPHONEOS_DEPLOYMENT_TARGET=14.0', | |
| 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', | |
| ], pluginExampleDirectory.path), | |
| ]), | |
| ); | |
| }); | |
| test('fails if xcrun fails', () async { | |
| createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformIOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| processRunner.mockProcessesForExecutable['xcrun'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1)), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze', '--no-dart', '--ios'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('The following packages had errors:'), | |
| contains(' plugin'), | |
| ]), | |
| ); | |
| }); | |
| }); | |
| group('macOS', () { | |
| test('skip if macOS is not supported', () async { | |
| createFakePlugin('plugin', packagesDir); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--macos', | |
| ]); | |
| expect(output, contains(contains('Package does not contain native macOS plugin code'))); | |
| expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[])); | |
| }); | |
| test('skip if macOS is implemented in a federated package', () async { | |
| createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformMacOS: const PlatformDetails(PlatformSupport.federated), | |
| }, | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--macos', | |
| ]); | |
| expect(output, contains(contains('Package does not contain native macOS plugin code'))); | |
| expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[])); | |
| }); | |
| test('runs for macOS plugin', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformMacOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| final Directory pluginExampleDirectory = getExampleDir(plugin); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--macos', | |
| ]); | |
| expect(output, contains(contains('plugin/example (macOS) passed analysis.'))); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>[ | |
| 'build', | |
| 'macos', | |
| '--debug', | |
| '--config-only', | |
| ], pluginExampleDirectory.path), | |
| ProcessCall('xcrun', const <String>[ | |
| 'xcodebuild', | |
| 'clean', | |
| 'analyze', | |
| '-workspace', | |
| 'macos/Runner.xcworkspace', | |
| '-scheme', | |
| 'Runner', | |
| '-configuration', | |
| 'Debug', | |
| 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', | |
| ], pluginExampleDirectory.path), | |
| ]), | |
| ); | |
| }); | |
| test('passes min macOS deployment version when requested', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformMacOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| final Directory pluginExampleDirectory = getExampleDir(plugin); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--macos', | |
| '--macos-min-version=12.0', | |
| ]); | |
| expect(output, contains(contains('plugin/example (macOS) passed analysis.'))); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>[ | |
| 'build', | |
| 'macos', | |
| '--debug', | |
| '--config-only', | |
| ], pluginExampleDirectory.path), | |
| ProcessCall('xcrun', const <String>[ | |
| 'xcodebuild', | |
| 'clean', | |
| 'analyze', | |
| '-workspace', | |
| 'macos/Runner.xcworkspace', | |
| '-scheme', | |
| 'Runner', | |
| '-configuration', | |
| 'Debug', | |
| 'MACOSX_DEPLOYMENT_TARGET=12.0', | |
| 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', | |
| ], pluginExampleDirectory.path), | |
| ]), | |
| ); | |
| }); | |
| test('fails if xcrun fails', () async { | |
| createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformMacOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| processRunner.mockProcessesForExecutable['xcrun'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1)), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['analyze', '--no-dart', '--macos'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('The following packages had errors:'), | |
| contains(' plugin'), | |
| ]), | |
| ); | |
| }); | |
| }); | |
| group('combined', () { | |
| test('runs both iOS and macOS when supported', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformIOS: const PlatformDetails(PlatformSupport.inline), | |
| platformMacOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| final Directory pluginExampleDirectory = getExampleDir(plugin); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--ios', | |
| '--macos', | |
| ]); | |
| expect( | |
| output, | |
| containsAll(<Matcher>[ | |
| contains('plugin/example (iOS) passed analysis.'), | |
| contains('plugin/example (macOS) passed analysis.'), | |
| ]), | |
| ); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>[ | |
| 'build', | |
| 'ios', | |
| '--debug', | |
| '--config-only', | |
| ], pluginExampleDirectory.path), | |
| ProcessCall('xcrun', const <String>[ | |
| 'xcodebuild', | |
| 'clean', | |
| 'analyze', | |
| '-workspace', | |
| 'ios/Runner.xcworkspace', | |
| '-scheme', | |
| 'Runner', | |
| '-configuration', | |
| 'Debug', | |
| '-destination', | |
| 'generic/platform=iOS Simulator', | |
| 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', | |
| ], pluginExampleDirectory.path), | |
| ProcessCall('flutter', const <String>[ | |
| 'build', | |
| 'macos', | |
| '--debug', | |
| '--config-only', | |
| ], pluginExampleDirectory.path), | |
| ProcessCall('xcrun', const <String>[ | |
| 'xcodebuild', | |
| 'clean', | |
| 'analyze', | |
| '-workspace', | |
| 'macos/Runner.xcworkspace', | |
| '-scheme', | |
| 'Runner', | |
| '-configuration', | |
| 'Debug', | |
| 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', | |
| ], pluginExampleDirectory.path), | |
| ]), | |
| ); | |
| }); | |
| test('runs only macOS for a macOS plugin', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformMacOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| final Directory pluginExampleDirectory = getExampleDir(plugin); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--ios', | |
| '--macos', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('plugin/example (macOS) passed analysis.')]), | |
| ); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>[ | |
| 'build', | |
| 'macos', | |
| '--debug', | |
| '--config-only', | |
| ], pluginExampleDirectory.path), | |
| ProcessCall('xcrun', const <String>[ | |
| 'xcodebuild', | |
| 'clean', | |
| 'analyze', | |
| '-workspace', | |
| 'macos/Runner.xcworkspace', | |
| '-scheme', | |
| 'Runner', | |
| '-configuration', | |
| 'Debug', | |
| 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', | |
| ], pluginExampleDirectory.path), | |
| ]), | |
| ); | |
| }); | |
| test('runs only iOS for a iOS plugin', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin', | |
| packagesDir, | |
| platformSupport: <String, PlatformDetails>{ | |
| platformIOS: const PlatformDetails(PlatformSupport.inline), | |
| }, | |
| ); | |
| final Directory pluginExampleDirectory = getExampleDir(plugin); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--ios', | |
| '--macos', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('plugin/example (iOS) passed analysis.')]), | |
| ); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>[ | |
| 'build', | |
| 'ios', | |
| '--debug', | |
| '--config-only', | |
| ], pluginExampleDirectory.path), | |
| ProcessCall('xcrun', const <String>[ | |
| 'xcodebuild', | |
| 'clean', | |
| 'analyze', | |
| '-workspace', | |
| 'ios/Runner.xcworkspace', | |
| '-scheme', | |
| 'Runner', | |
| '-configuration', | |
| 'Debug', | |
| '-destination', | |
| 'generic/platform=iOS Simulator', | |
| 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', | |
| ], pluginExampleDirectory.path), | |
| ]), | |
| ); | |
| }); | |
| }); | |
| group('file filtering', () { | |
| const files = <String>['foo.m', 'foo.swift', 'foo.cc', 'foo.cpp', 'foo.h']; | |
| for (final file in files) { | |
| test('runs command for changes to $file', () async { | |
| createFakePackage('package_a', packagesDir); | |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ | |
| FakeProcessInfo( | |
| MockProcess( | |
| stdout: | |
| ''' | |
| packages/package_a/$file | |
| ''', | |
| ), | |
| ), | |
| ]; | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--ios', | |
| ]); | |
| expect(output, containsAllInOrder(<Matcher>[contains('Running for package_a')])); | |
| }); | |
| } | |
| test('skips commands if all files should be ignored', () async { | |
| createFakePackage('package_a', packagesDir); | |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ | |
| FakeProcessInfo( | |
| MockProcess( | |
| stdout: ''' | |
| .gemini/config.yaml | |
| AGENTS.md | |
| README.md | |
| SUGGESTED_REVIEWERS.md | |
| packages/package_a/CHANGELOG.md | |
| packages/package_a/lib/foo.dart | |
| ''', | |
| ), | |
| ), | |
| ]; | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'analyze', | |
| '--no-dart', | |
| '--ios', | |
| ]); | |
| expect(output, isNot(containsAllInOrder(<Matcher>[contains('Running for package_a')]))); | |
| expect(output, containsAllInOrder(<Matcher>[contains('SKIPPING ALL PACKAGES')])); | |
| }); | |
| }); | |
| }); | |
| } | |
| void _writeFakePubspecWithLinter( | |
| RepositoryPackage package, { | |
| bool inDevDependencies = false, | |
| bool inDependencies = false, | |
| bool includeFlutter = true, | |
| }) { | |
| package.pubspecFile.writeAsStringSync(''' | |
| name: ${package.directory.basename} | |
| version: 0.0.1 | |
| environment: | |
| sdk: ">=2.14.0 <4.0.0" | |
| ${includeFlutter ? 'flutter: ">=2.5.0"' : ''} | |
| dependencies: | |
| ${includeFlutter ? 'flutter:\n sdk: flutter' : ''} | |
| ${inDependencies ? ' cognitive_complexity: 0.2.0' : ''} | |
| ${inDevDependencies ? 'dev_dependencies:\n cognitive_complexity: 0.2.0' : ''} | |
| '''); | |
| } | |
| void _mockCallsForFlutterAnalyze( | |
| RecordingProcessRunner processRunner, { | |
| List<FakeProcessInfo> extraFlutterCalls = const <FakeProcessInfo>[], | |
| List<FakeProcessInfo> extraDartCalls = const <FakeProcessInfo>[], | |
| }) { | |
| processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(), const <String>['pub', 'get']), | |
| ...extraFlutterCalls, | |
| ]; | |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(), const <String>['analyze']), | |
| ...extraDartCalls, | |
| ]; | |
| } |