diff --git a/.cirrus.yml b/.cirrus.yml index 6b3614178b11..798088eb7141 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -16,10 +16,12 @@ task: - flutter channel master - flutter upgrade - git fetch origin master - submodules_script: - - git submodule init - - git submodule update matrix: + - name: plugin_tools_tests + script: + - cd script/tool + - pub get + - CIRRUS_BUILD_ID=null pub run test - name: publishable script: - flutter channel master @@ -132,9 +134,6 @@ task: - flutter channel master - flutter upgrade - git fetch origin master - submodules_script: - - git submodule init - - git submodule update matrix: - name: build-linux+drive-examples install_script: @@ -161,9 +160,6 @@ task: - flutter channel master - flutter upgrade - git fetch origin master - submodules_script: - - git submodule init - - git submodule update create_simulator_script: - xcrun simctl list - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.iOS-14-3 | xargs xcrun simctl boot @@ -222,9 +218,6 @@ task: - flutter channel master - flutter upgrade - git fetch origin master - submodules_script: - - git submodule init - - git submodule update create_simulator_script: - xcrun simctl list - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot @@ -254,9 +247,6 @@ task: - flutter channel master - flutter upgrade - git fetch origin master - submodules_script: - - git submodule init - - git submodule update matrix: - name: build_all_plugins_app script: diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index d9fce4ad26a7..e47123959005 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -21,5 +21,10 @@ dependencies: http_multi_server: ^2.2.0 collection: 1.14.13 +dev_dependencies: + matcher: ^0.12.6 + mockito: ^4.1.1 + pedantic: 1.8.0 + environment: sdk: ">=2.3.0 <3.0.0" diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart new file mode 100644 index 000000000000..9e7a42bbb680 --- /dev/null +++ b/script/tool/test/analyze_command_test.dart @@ -0,0 +1,93 @@ +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.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + RecordingProcessRunner processRunner; + CommandRunner runner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final AnalyzeCommand analyzeCommand = AnalyzeCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('analyze_command', 'Test for analyze_command'); + runner.addCommand(analyzeCommand); + }); + + tearDown(() { + mockPackagesDir.deleteSync(recursive: true); + }); + + test('analyzes all packages', () async { + final Directory plugin1Dir = await createFakePlugin('a'); + final Directory plugin2Dir = await createFakePlugin('b'); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('pub', ['global', 'activate', 'tuneup'], + mockPackagesDir.path), + ProcessCall('flutter', ['packages', 'get'], plugin1Dir.path), + ProcessCall('flutter', ['packages', 'get'], plugin2Dir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + plugin1Dir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + plugin2Dir.path), + ])); + }); + + group('verifies analysis settings', () { + test('fails analysis_options.yaml', () async { + await createFakePlugin('foo', withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('fails .analysis_options', () async { + await createFakePlugin('foo', withExtraFiles: >[ + ['.analysis_options'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('takes an allow list', () async { + final Directory pluginDir = + await createFakePlugin('foo', withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze', '--custom-analysis', 'foo']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('pub', ['global', 'activate', 'tuneup'], + mockPackagesDir.path), + ProcessCall('flutter', ['packages', 'get'], pluginDir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + pluginDir.path), + ])); + }); + }); +} diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart new file mode 100644 index 000000000000..eaf5049dcc02 --- /dev/null +++ b/script/tool/test/build_examples_command_test.dart @@ -0,0 +1,470 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/build_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test build_example_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final BuildExamplesCommand command = BuildExamplesCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'build_examples_command', 'Test for build_example_command'); + runner.addCommand(command); + cleanupPackages(); + }); + + test('building for iOS when plugin is not set up for iOS results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ipa', '--no-macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + 'iOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for ios', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Linux when plugin is not set up for Linux results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + 'Linux is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --linux with no + // Linux implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for Linux', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'linux'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('building for macos with no implementation results in no-op', + () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['example', 'test'], + ]); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + '\macOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + test('building for macos', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['pub', 'get'], + pluginExampleDirectory.path), + ProcessCall(flutterCommand, ['build', 'macos'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Windows when plugin is not set up for Windows results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + 'Windows is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for windows', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'windows'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Android when plugin is not set up for Android results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--no-ipa']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + 'Android is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for android', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'apk'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('enable-experiment flag for Android', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + ['build', 'apk', '--enable-experiment=exp1'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('enable-experiment flag for ios', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + }); +} diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart new file mode 100644 index 000000000000..b3504c2358d9 --- /dev/null +++ b/script/tool/test/common_test.dart @@ -0,0 +1,100 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + RecordingProcessRunner processRunner; + CommandRunner runner; + List plugins; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + plugins = []; + final SamplePluginCommand samplePluginCommand = SamplePluginCommand( + plugins, + mockPackagesDir, + mockFileSystem, + processRunner: processRunner, + ); + runner = + CommandRunner('common_command', 'Test for common functionality'); + runner.addCommand(samplePluginCommand); + }); + + tearDown(() { + mockPackagesDir.deleteSync(recursive: true); + }); + + test('all plugins from file system', () async { + final Directory plugin1 = createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run(['sample']); + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('exclude plugins when plugins flag is specified', () async { + createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + 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'); + createFakePlugin('plugin2'); + await runner.run(['sample', '--exclude=plugin1,plugin2']); + expect(plugins, unorderedEquals([])); + }); + + test('exclude federated plugins when plugins flag is specified', () async { + createFakePlugin('plugin1', parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2'); + 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'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); +} + +class SamplePluginCommand extends PluginCommand { + SamplePluginCommand( + this.plugins_, + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + List plugins_; + + @override + final String name = 'sample'; + + @override + final String description = 'sample command'; + + @override + Future run() async { + await for (Directory package in getPlugins()) { + this.plugins_.add(package.path); + } + } +} diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart new file mode 100644 index 000000000000..f4bdd95c1664 --- /dev/null +++ b/script/tool/test/drive_examples_command_test.dart @@ -0,0 +1,505 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test drive_example_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final DriveExamplesCommand command = DriveExamplesCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'drive_examples_command', 'Test for drive_example_command'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('driving under folder "test"', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver"', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver" when test files are missing"', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await expectLater( + () => runCapturingPrint(runner, ['drive-examples']), + throwsA(const TypeMatcher())); + }); + + test( + 'driving under folder "test_driver" when targets are under "integration_test"', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'integration_test.dart'], + ['example', 'integration_test', 'bar_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'ignore_me.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String driverTestPath = p.join('test_driver', 'integration_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'bar_test.dart'), + ], + pluginExampleDirectory.path), + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'foo_test.dart'), + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support Linux is a no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --linux on a non-Linux + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Linux plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'linux', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport macOS is a no-op', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ]); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + test('driving on a macOS plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'macos', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport windows is a no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --windows on a non-windows + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Windows plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'windows', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support mobile is no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('enable-experiment flag', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'drive-examples', + '--enable-experiment=exp1', + ]); + + String deviceTestPath = p.join('test', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--enable-experiment=exp1', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + }); +} diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart new file mode 100644 index 000000000000..97b977619d57 --- /dev/null +++ b/script/tool/test/firebase_test_lab_test.dart @@ -0,0 +1,256 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$FirebaseTestLabCommand', () { + final List printedMessages = []; + CommandRunner runner; + RecordingProcessRunner processRunner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final FirebaseTestLabCommand command = FirebaseTestLabCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, + print: (Object message) => printedMessages.add(message.toString())); + + runner = CommandRunner( + 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); + runner.addCommand(command); + }); + + tearDown(() { + printedMessages.clear(); + }); + + test('retries gcloud set', () async { + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(1); + processRunner.processToReturn = mockProcess; + createFakePlugin('plugin', withExtraFiles: >[ + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + await expectLater( + () => runCapturingPrint(runner, ['firebase-test-lab']), + throwsA(const TypeMatcher())); + expect( + printedMessages, + contains( + "\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.")); + }); + + test('runs e2e tests', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + ]); + + expect( + printedMessages, + orderedEquals([ + '\nRUNNING FIREBASE TEST LAB TESTS for plugin', + '\nFirebase project configured.', + '\n\n', + 'All Firebase Test Lab tests successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ]), + ); + }); + + test('experimental flag', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--test-run-id', + 'testRunId', + '--enable-experiment=exp1', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ]), + ); + + cleanupPackages(); + }); + }); +} diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart new file mode 100644 index 000000000000..49d6ad4d8e20 --- /dev/null +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -0,0 +1,202 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$LintPodspecsCommand', () { + CommandRunner runner; + MockPlatform mockPlatform; + final RecordingProcessRunner processRunner = RecordingProcessRunner(); + List printedMessages; + + setUp(() { + initializeFakePackages(); + + printedMessages = []; + mockPlatform = MockPlatform(); + when(mockPlatform.isMacOS).thenReturn(true); + final LintPodspecsCommand command = LintPodspecsCommand( + mockPackagesDir, + mockFileSystem, + processRunner: processRunner, + platform: mockPlatform, + print: (Object message) => printedMessages.add(message.toString()), + ); + + runner = + CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); + runner.addCommand(command); + final MockProcess mockLintProcess = MockProcess(); + mockLintProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockLintProcess; + processRunner.recordedCalls.clear(); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('only runs on macOS', () async { + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + when(mockPlatform.isMacOS).thenReturn(false); + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + equals([]), + ); + }); + + test('runs pod lib lint on a podspec', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['ios', 'plugin1.podspec'], + ['bogus.dart'], // Ignore non-podspecs. + ]); + + processRunner.resultStdout = 'Foo'; + processRunner.resultStderr = 'Bar'; + + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--analyze', + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--analyze', + ], + mockPackagesDir.path), + ]), + ); + + expect( + printedMessages, contains('Linting and analyzing plugin1.podspec')); + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('skips podspecs with known issues', () async { + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'] + ]); + createFakePlugin('plugin2', withExtraFiles: >[ + ['plugin2.podspec'] + ]); + + await runner + .run(['podspecs', '--skip=plugin1', '--skip=plugin2']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ]), + ); + }); + + test('skips analyzer for podspecs with known warnings', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + await runner.run(['podspecs', '--no-analyze=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + ], + mockPackagesDir.path), + ]), + ); + + expect(printedMessages, contains('Linting plugin1.podspec')); + }); + + test('allow warnings for podspecs with known warnings', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + await runner.run(['podspecs', '--ignore-warnings=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--allow-warnings', + '--analyze', + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--allow-warnings', + '--analyze', + ], + mockPackagesDir.path), + ]), + ); + + expect( + printedMessages, contains('Linting and analyzing plugin1.podspec')); + }); + }); +} + +class MockPlatform extends Mock implements Platform {} diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart new file mode 100644 index 000000000000..478625283dd0 --- /dev/null +++ b/script/tool/test/list_command_test.dart @@ -0,0 +1,198 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/list_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$ListCommand', () { + CommandRunner runner; + + setUp(() { + initializeFakePackages(); + final ListCommand command = ListCommand(mockPackagesDir, mockFileSystem); + + runner = CommandRunner('list_test', 'Test for $ListCommand'); + runner.addCommand(command); + }); + + test('lists plugins', () async { + createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + + final List plugins = + await runCapturingPrint(runner, ['list', '--type=plugin']); + + expect( + plugins, + orderedEquals([ + '/packages/plugin1', + '/packages/plugin2', + ]), + ); + + cleanupPackages(); + }); + + test('lists examples', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=example']); + + expect( + examples, + orderedEquals([ + '/packages/plugin1/example', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + ]), + ); + + cleanupPackages(); + }); + + test('lists packages', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List packages = + await runCapturingPrint(runner, ['list', '--type=package']); + + expect( + packages, + unorderedEquals([ + '/packages/plugin1', + '/packages/plugin1/example', + '/packages/plugin2', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + '/packages/plugin3', + ]), + ); + + cleanupPackages(); + }); + + test('lists files', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=file']); + + expect( + examples, + unorderedEquals([ + '/packages/plugin1/pubspec.yaml', + '/packages/plugin1/example/pubspec.yaml', + '/packages/plugin2/pubspec.yaml', + '/packages/plugin2/example/example1/pubspec.yaml', + '/packages/plugin2/example/example2/pubspec.yaml', + '/packages/plugin3/pubspec.yaml', + ]), + ); + + cleanupPackages(); + }); + + test('lists plugins using federated plugin layout', () async { + createFakePlugin('plugin1'); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory federatedPlugin = + mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory clientLibrary = + federatedPlugin.childDirectory('my_plugin')..createSync(); + createFakePubspec(clientLibrary); + final Directory webLibrary = + federatedPlugin.childDirectory('my_plugin_web')..createSync(); + createFakePubspec(webLibrary); + final Directory macLibrary = + federatedPlugin.childDirectory('my_plugin_macos')..createSync(); + createFakePubspec(macLibrary); + + // Test without specifying `--type`. + final List plugins = + await runCapturingPrint(runner, ['list']); + + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + cleanupPackages(); + }); + + test('can filter plugins with the --plugins argument', () async { + createFakePlugin('plugin1'); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory federatedPlugin = + mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory clientLibrary = + federatedPlugin.childDirectory('my_plugin')..createSync(); + createFakePubspec(clientLibrary); + final Directory webLibrary = + federatedPlugin.childDirectory('my_plugin_web')..createSync(); + createFakePubspec(webLibrary); + final Directory macLibrary = + federatedPlugin.childDirectory('my_plugin_macos')..createSync(); + createFakePubspec(macLibrary); + + List plugins = await runCapturingPrint( + runner, ['list', '--plugins=plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin/my_plugin_web']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin_web', + ]), + ); + + plugins = await runCapturingPrint(runner, + ['list', '--plugins=my_plugin/my_plugin_web,plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin_web', + ]), + ); + }); + }); +} diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart new file mode 100644 index 000000000000..3e17ff8efd32 --- /dev/null +++ b/script/tool/test/mocks.dart @@ -0,0 +1,33 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:mockito/mockito.dart'; + +class MockProcess extends Mock implements io.Process { + final Completer exitCodeCompleter = Completer(); + final StreamController> stdoutController = + StreamController>(); + final StreamController> stderrController = + StreamController>(); + final MockIOSink stdinMock = MockIOSink(); + + @override + Future get exitCode => exitCodeCompleter.future; + + @override + Stream> get stdout => stdoutController.stream; + + @override + Stream> get stderr => stderrController.stream; + + @override + IOSink get stdin => stdinMock; +} + +class MockIOSink extends Mock implements IOSink { + List lines = []; + + @override + void writeln([Object obj = ""]) => lines.add(obj); +} diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart new file mode 100644 index 000000000000..ada4bf08fd72 --- /dev/null +++ b/script/tool/test/publish_plugin_command_test.dart @@ -0,0 +1,378 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:git/git.dart'; +import 'package:matcher/matcher.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + const String testPluginName = 'foo'; + final List printedMessages = []; + + Directory parentDir; + Directory pluginDir; + GitDir gitDir; + TestProcessRunner processRunner; + CommandRunner commandRunner; + MockStdin mockStdin; + + setUp(() async { + // This test uses a local file system instead of an in memory one throughout + // so that git actually works. In setup we initialize a mono repo of plugins + // with one package and commit everything to Git. + parentDir = const LocalFileSystem() + .systemTempDirectory + .createTempSync('publish_plugin_command_test-'); + initializeFakePackages(parentDir: parentDir); + pluginDir = createFakePlugin(testPluginName, withSingleExample: false); + assert(pluginDir != null && pluginDir.existsSync()); + createFakePubspec(pluginDir, includeVersion: true); + io.Process.runSync('git', ['init'], + workingDirectory: mockPackagesDir.path); + gitDir = await GitDir.fromExisting(mockPackagesDir.path); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Initial commit']); + processRunner = TestProcessRunner(); + mockStdin = MockStdin(); + commandRunner = CommandRunner('tester', '') + ..addCommand(PublishPluginCommand( + mockPackagesDir, const LocalFileSystem(), + processRunner: processRunner, + print: (Object message) => printedMessages.add(message.toString()), + stdinput: mockStdin)); + }); + + tearDown(() { + parentDir.deleteSync(recursive: true); + printedMessages.clear(); + }); + + group('Initial validation', () { + test('requires a package flag', () async { + await expectLater(() => commandRunner.run(['publish-plugin']), + throwsA(const TypeMatcher())); + + expect( + printedMessages.last, contains("Must specify a package to publish.")); + }); + + test('requires an existing flag', () async { + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', 'iamerror']), + throwsA(const TypeMatcher())); + + expect(printedMessages.last, contains('iamerror does not exist')); + }); + + test('refuses to proceed with dirty files', () async { + pluginDir.childFile('tmp').createSync(); + + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', testPluginName]), + throwsA(const TypeMatcher())); + + expect( + printedMessages.last, + contains( + "There are files in the package directory that haven't been saved in git.")); + }); + + test('fails immediately if the remote doesn\'t exist', () async { + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', testPluginName]), + throwsA(const TypeMatcher())); + + expect(processRunner.results.last.stderr, contains("No such remote")); + }); + + test("doesn't validate the remote if it's not pushing tags", () async { + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + + expect(printedMessages.last, 'Done!'); + }); + + test('can publish non-flutter package', () async { + createFakePubspec(pluginDir, includeVersion: true, isFlutter: false); + io.Process.runSync('git', ['init'], + workingDirectory: mockPackagesDir.path); + gitDir = await GitDir.fromExisting(mockPackagesDir.path); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Initial commit']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + expect(printedMessages.last, 'Done!'); + }); + }); + + group('Publishes package', () { + test('while showing all output from pub publish to the user', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + processRunner.mockPublishProcess.stdoutController.add(utf8.encode('Foo')); + processRunner.mockPublishProcess.stderrController.add(utf8.encode('Bar')); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await publishCommand; + + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('forwards input from the user to `pub publish`', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + mockStdin.controller.add(utf8.encode('user input')); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await publishCommand; + + expect(processRunner.mockPublishProcess.stdinMock.lines, + contains('user input')); + }); + + test('forwards --pub-publish-flags to pub publish', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + '--pub-publish-flags', + '--dry-run,--server=foo' + ]); + + expect(processRunner.mockPublishArgs.length, 4); + expect(processRunner.mockPublishArgs[0], 'pub'); + expect(processRunner.mockPublishArgs[1], 'publish'); + expect(processRunner.mockPublishArgs[2], '--dry-run'); + expect(processRunner.mockPublishArgs[3], '--server=foo'); + }); + + test('throws if pub publish fails', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains("Publish failed. Exiting.")); + }); + }); + + group('Tags release', () { + test('with the version and name from the pubspec.yaml', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]); + + final String tag = + (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) + .stdout; + expect(tag, isNotEmpty); + }); + + test('only if publishing succeeded', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains("Publish failed. Exiting.")); + final String tag = (await gitDir.runCommand( + ['show-ref', 'fake_package-v0.0.1'], + throwOnError: false)) + .stdout; + expect(tag, isEmpty); + }); + }); + + group('Pushes tags', () { + setUp(() async { + await gitDir.runCommand( + ['remote', 'add', 'upstream', 'http://localhost:8000']); + }); + + test('requires user confirmation', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'help'; + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains('Tag push canceled.')); + }); + + test('to upstream by default', () async { + await gitDir.runCommand(['tag', 'garbage']); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('to different remotes based on a flag', () async { + await gitDir.runCommand( + ['remote', 'add', 'origin', 'http://localhost:8001']); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--remote', + 'origin', + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'origin'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('only if tagging and pushing to remotes are both enabled', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-tag-release', + ]); + + expect(processRunner.pushTagsArgs.isEmpty, isTrue); + expect(printedMessages.last, 'Done!'); + }); + }); +} + +class TestProcessRunner extends ProcessRunner { + final List results = []; + final MockProcess mockPublishProcess = MockProcess(); + final List mockPublishArgs = []; + final MockProcessResult mockPushTagsResult = MockProcessResult(); + final List pushTagsArgs = []; + + @override + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + // Don't ever really push tags. + if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { + pushTagsArgs.addAll(args); + return mockPushTagsResult; + } + + final io.ProcessResult result = io.Process.runSync(executable, args, + workingDirectory: workingDir?.path); + results.add(result); + if (result.exitCode != 0) { + throw ToolExit(result.exitCode); + } + return result; + } + + @override + Future start(String executable, List args, + {Directory workingDirectory}) async { + /// Never actually publish anything. Start is always and only used for this + /// since it returns something we can route stdin through. + assert(executable == 'flutter' && + args.isNotEmpty && + args[0] == 'pub' && + args[1] == 'publish'); + mockPublishArgs.addAll(args); + return mockPublishProcess; + } +} + +class MockStdin extends Mock implements io.Stdin { + final StreamController> controller = StreamController>(); + String readLineOutput; + + @override + Stream transform(StreamTransformer streamTransformer) { + return controller.stream.transform(streamTransformer); + } + + @override + StreamSubscription> listen(void onData(List event), + {Function onError, void onDone(), bool cancelOnError}) { + return controller.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + String readLineSync( + {Encoding encoding = io.systemEncoding, + bool retainNewlines = false}) => + readLineOutput; +} + +class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart new file mode 100644 index 000000000000..514e4c27190a --- /dev/null +++ b/script/tool/test/test_command_test.dart @@ -0,0 +1,154 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/test_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$TestCommand', () { + CommandRunner runner; + final RecordingProcessRunner processRunner = RecordingProcessRunner(); + + setUp(() { + initializeFakePackages(); + final TestCommand command = TestCommand(mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('test_test', 'Test for $TestCommand'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + processRunner.recordedCalls.clear(); + }); + + test('runs flutter test on each plugin', () async { + final Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = + createFakePlugin('plugin2', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', ['test', '--color'], plugin1Dir.path), + ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('skips testing plugins without test directory', () async { + createFakePlugin('plugin1'); + final Directory plugin2Dir = + createFakePlugin('plugin2', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('runs pub run test on non-Flutter packages', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall( + 'pub', + ['run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('runs on Chrome for web plugins', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ], + isFlutter: true, + isWebPlugin: true, + ); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', + ['test', '--color', '--platform=chrome'], pluginDir.path), + ]), + ); + }); + + test('enable-experiment flag', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall( + 'pub', + ['run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + }); +} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart new file mode 100644 index 000000000000..ec0000d13f34 --- /dev/null +++ b/script/tool/test/util.dart @@ -0,0 +1,291 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:platform/platform.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:quiver/collection.dart'; + +FileSystem mockFileSystem = MemoryFileSystem( + style: LocalPlatform().isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix); +Directory mockPackagesDir; + +/// Creates a mock packages directory in the mock file system. +/// +/// If [parentDir] is set the mock packages dir will be creates as a child of +/// it. If not [mockFileSystem] will be used instead. +void initializeFakePackages({Directory parentDir}) { + mockPackagesDir = + (parentDir ?? mockFileSystem.currentDirectory).childDirectory('packages'); + mockPackagesDir.createSync(); +} + +/// Creates a plugin package with the given [name] in [mockPackagesDir]. +Directory createFakePlugin( + String name, { + bool withSingleExample = false, + List withExamples = const [], + List> withExtraFiles = const >[], + bool isFlutter = true, + bool isAndroidPlugin = false, + bool isIosPlugin = false, + bool isWebPlugin = false, + bool isLinuxPlugin = false, + bool isMacOsPlugin = false, + bool isWindowsPlugin = false, + String parentDirectoryName = '', +}) { + assert(!(withSingleExample && withExamples.isNotEmpty), + 'cannot pass withSingleExample and withExamples simultaneously'); + + final Directory pluginDirectory = (parentDirectoryName != '') + ? mockPackagesDir.childDirectory(parentDirectoryName).childDirectory(name) + : mockPackagesDir.childDirectory(name); + pluginDirectory.createSync(recursive: true); + + createFakePubspec( + pluginDirectory, + name: name, + isFlutter: isFlutter, + isAndroidPlugin: isAndroidPlugin, + isIosPlugin: isIosPlugin, + isWebPlugin: isWebPlugin, + isLinuxPlugin: isLinuxPlugin, + isMacOsPlugin: isMacOsPlugin, + isWindowsPlugin: isWindowsPlugin, + ); + + if (withSingleExample) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + createFakePubspec(exampleDir, + name: "${name}_example", isFlutter: isFlutter); + } else if (withExamples.isNotEmpty) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + for (String example in withExamples) { + final Directory currentExample = exampleDir.childDirectory(example) + ..createSync(); + createFakePubspec(currentExample, name: example, isFlutter: isFlutter); + } + } + + for (List file in withExtraFiles) { + final List newFilePath = [pluginDirectory.path] + ..addAll(file); + final File newFile = + mockFileSystem.file(mockFileSystem.path.joinAll(newFilePath)); + newFile.createSync(recursive: true); + } + + return pluginDirectory; +} + +/// Creates a `pubspec.yaml` file with a flutter dependency. +void createFakePubspec( + Directory parent, { + String name = 'fake_package', + bool isFlutter = true, + bool includeVersion = false, + bool isAndroidPlugin = false, + bool isIosPlugin = false, + bool isWebPlugin = false, + bool isLinuxPlugin = false, + bool isMacOsPlugin = false, + bool isWindowsPlugin = false, +}) { + parent.childFile('pubspec.yaml').createSync(); + String yaml = ''' +name: $name +flutter: + plugin: + platforms: +'''; + if (isAndroidPlugin) { + yaml += ''' + android: + package: io.flutter.plugins.fake + pluginClass: FakePlugin +'''; + } + if (isIosPlugin) { + yaml += ''' + ios: + pluginClass: FLTFakePlugin +'''; + } + if (isWebPlugin) { + yaml += ''' + web: + pluginClass: FakePlugin + fileName: ${name}_web.dart +'''; + } + if (isLinuxPlugin) { + yaml += ''' + linux: + pluginClass: FakePlugin +'''; + } + if (isMacOsPlugin) { + yaml += ''' + macos: + pluginClass: FakePlugin +'''; + } + if (isWindowsPlugin) { + yaml += ''' + windows: + pluginClass: FakePlugin +'''; + } + if (isFlutter) { + yaml += ''' +dependencies: + flutter: + sdk: flutter +'''; + } + if (includeVersion) { + yaml += ''' +version: 0.0.1 +publish_to: none # Hardcoded safeguard to prevent this from somehow being published by a broken test. +'''; + } + parent.childFile('pubspec.yaml').writeAsStringSync(yaml); +} + +/// Cleans up the mock packages directory, making it an empty directory again. +void cleanupPackages() { + mockPackagesDir.listSync().forEach((FileSystemEntity entity) { + entity.deleteSync(recursive: true); + }); +} + +/// Run the command [runner] with the given [args] and return +/// what was printed. +Future> runCapturingPrint( + CommandRunner runner, List args) async { + final List prints = []; + final ZoneSpecification spec = ZoneSpecification( + print: (_, __, ___, String message) { + prints.add(message); + }, + ); + await Zone.current + .fork(specification: spec) + .run>(() => runner.run(args)); + + return prints; +} + +/// A mock [ProcessRunner] which records process calls. +class RecordingProcessRunner extends ProcessRunner { + io.Process processToReturn; + final List recordedCalls = []; + + /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. + String resultStdout; + + /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. + String resultStderr; + + @override + Future runAndStream( + String executable, + List args, { + Directory workingDir, + bool exitOnError = false, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + return Future.value( + processToReturn == null ? 0 : await processToReturn.exitCode); + } + + /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. + @override + Future run(String executable, List args, + {Directory workingDir, + bool exitOnError = false, + stdoutEncoding = io.systemEncoding, + stderrEncoding = io.systemEncoding}) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + io.ProcessResult result; + + if (processToReturn != null) { + result = io.ProcessResult( + processToReturn.pid, + await processToReturn.exitCode, + resultStdout ?? processToReturn.stdout, + resultStderr ?? processToReturn.stderr); + } + return Future.value(result); + } + + @override + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + io.ProcessResult result; + if (processToReturn != null) { + result = io.ProcessResult( + processToReturn.pid, + await processToReturn.exitCode, + resultStdout ?? processToReturn.stdout, + resultStderr ?? processToReturn.stderr); + } + return Future.value(result); + } + + @override + Future start(String executable, List args, + {Directory workingDirectory}) async { + recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); + return Future.value(processToReturn); + } +} + +/// A recorded process call. +class ProcessCall { + const ProcessCall(this.executable, this.args, this.workingDir); + + /// The executable that was called. + final String executable; + + /// The arguments passed to [executable] in the call. + final List args; + + /// The working directory this process was called from. + final String workingDir; + + @override + bool operator ==(dynamic other) { + if (other is! ProcessCall) { + return false; + } + final ProcessCall otherCall = other; + return executable == otherCall.executable && + listsEqual(args, otherCall.args) && + workingDir == otherCall.workingDir; + } + + @override + int get hashCode => + executable?.hashCode ?? + 0 ^ args?.hashCode ?? + 0 ^ workingDir?.hashCode ?? + 0; + + @override + String toString() { + final List command = [executable]..addAll(args); + return '"${command.join(' ')}" in $workingDir'; + } +} diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart new file mode 100644 index 000000000000..b9ace3811bff --- /dev/null +++ b/script/tool/test/version_check_test.dart @@ -0,0 +1,319 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:git/git.dart'; +import 'package:mockito/mockito.dart'; +import "package:test/test.dart"; +import "package:flutter_plugin_tools/src/version_check_command.dart"; +import 'package:pub_semver/pub_semver.dart'; +import 'util.dart'; + +void testAllowedVersion( + String masterVersion, + String headVersion, { + bool allowed = true, + NextVersionType nextVersionType, +}) { + final Version master = Version.parse(masterVersion); + final Version head = Version.parse(headVersion); + final Map allowedVersions = + getAllowedNextVersions(master, head); + if (allowed) { + expect(allowedVersions, contains(head)); + if (nextVersionType != null) { + expect(allowedVersions[head], equals(nextVersionType)); + } + } else { + expect(allowedVersions, isNot(contains(head))); + } +} + +class MockGitDir extends Mock implements GitDir {} + +class MockProcessResult extends Mock implements ProcessResult {} + +void main() { + group('$VersionCheckCommand', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + List> gitDirCommands; + String gitDiffResponse; + Map gitShowResponses; + + setUp(() { + gitDirCommands = >[]; + gitDiffResponse = ''; + gitShowResponses = {}; + final MockGitDir gitDir = MockGitDir(); + when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0]); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + } else if (invocation.positionalArguments[0][0] == 'show') { + final String response = + gitShowResponses[invocation.positionalArguments[0][1]]; + when(mockProcessResult.stdout).thenReturn(response); + } + return Future.value(mockProcessResult); + }); + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final VersionCheckCommand command = VersionCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, gitDir: gitDir); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('allows valid version', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect(gitDirCommands[1].join(' '), + equals('show master:packages/plugin/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin/pubspec.yaml')); + }); + + test('denies invalid version', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', + }; + final Future> result = runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect(gitDirCommands[1].join(' '), + equals('show master:packages/plugin/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin/pubspec.yaml')); + }); + + test('gracefully handles missing pubspec.yaml', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + mockFileSystem.currentDirectory + .childDirectory('packages') + .childDirectory('plugin') + .childFile('pubspec.yaml') + .deleteSync(); + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(1)); + expect(gitDirCommands.first.join(' '), + equals('diff --name-only master HEAD')); + }); + + test('allows minor changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface'); + gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.1.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect( + gitDirCommands[1].join(' '), + equals( + 'show master:packages/plugin_platform_interface/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + }); + + test('disallows breaking changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface'); + gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 2.0.0', + }; + final Future> output = runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect( + gitDirCommands[1].join(' '), + equals( + 'show master:packages/plugin_platform_interface/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + }); + }); + + group("Pre 1.0", () { + test("nextVersion allows patch version", () { + testAllowedVersion("0.12.0", "0.12.0+1", + nextVersionType: NextVersionType.PATCH); + testAllowedVersion("0.12.0+4", "0.12.0+5", + nextVersionType: NextVersionType.PATCH); + }); + + test("nextVersion does not allow jumping patch", () { + testAllowedVersion("0.12.0", "0.12.0+2", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.0+4", allowed: false); + }); + + test("nextVersion does not allow going back", () { + testAllowedVersion("0.12.0", "0.11.0", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.0+1", allowed: false); + testAllowedVersion("0.12.0+1", "0.12.0", allowed: false); + }); + + test("nextVersion allows minor version", () { + testAllowedVersion("0.12.0", "0.12.1", + nextVersionType: NextVersionType.MINOR); + testAllowedVersion("0.12.0+4", "0.12.1", + nextVersionType: NextVersionType.MINOR); + }); + + test("nextVersion does not allow jumping minor", () { + testAllowedVersion("0.12.0", "0.12.2", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.3", allowed: false); + }); + }); + + group("Releasing 1.0", () { + test("nextVersion allows releasing 1.0", () { + testAllowedVersion("0.12.0", "1.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion("0.12.0+4", "1.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test("nextVersion does not allow jumping major", () { + testAllowedVersion("0.12.0", "2.0.0", allowed: false); + testAllowedVersion("0.12.0+4", "2.0.0", allowed: false); + }); + + test("nextVersion does not allow un-releasing", () { + testAllowedVersion("1.0.0", "0.12.0+4", allowed: false); + testAllowedVersion("1.0.0", "0.12.0", allowed: false); + }); + }); + + group("Post 1.0", () { + test("nextVersion allows patch jumps", () { + testAllowedVersion("1.0.1", "1.0.2", + nextVersionType: NextVersionType.PATCH); + testAllowedVersion("1.0.0", "1.0.1", + nextVersionType: NextVersionType.PATCH); + }); + + test("nextVersion does not allow build jumps", () { + testAllowedVersion("1.0.1", "1.0.1+1", allowed: false); + testAllowedVersion("1.0.0+5", "1.0.0+6", allowed: false); + }); + + test("nextVersion does not allow skipping patches", () { + testAllowedVersion("1.0.1", "1.0.3", allowed: false); + testAllowedVersion("1.0.0", "1.0.6", allowed: false); + }); + + test("nextVersion allows minor version jumps", () { + testAllowedVersion("1.0.1", "1.1.0", + nextVersionType: NextVersionType.MINOR); + testAllowedVersion("1.0.0", "1.1.0", + nextVersionType: NextVersionType.MINOR); + }); + + test("nextVersion does not allow skipping minor versions", () { + testAllowedVersion("1.0.1", "1.2.0", allowed: false); + testAllowedVersion("1.1.0", "1.3.0", allowed: false); + }); + + test("nextVersion allows breaking changes", () { + testAllowedVersion("1.0.1", "2.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion("1.0.0", "2.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test("nextVersion allows null safety pre prelease", () { + testAllowedVersion("1.0.1", "2.0.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0", "2.0.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0-nullsafety", "1.0.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0-nullsafety.1", "1.0.0-nullsafety.2", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0", "0.2.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0-nullsafety", "0.1.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0-nullsafety.1", "0.1.0-nullsafety.2", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0", "1.1.0-nullsafety", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.1.0-nullsafety", "1.1.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0", "0.1.1-nullsafety", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.1-nullsafety", "0.1.1-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + }); + + test("nextVersion does not allow skipping major versions", () { + testAllowedVersion("1.0.1", "3.0.0", allowed: false); + testAllowedVersion("1.1.0", "2.3.0", allowed: false); + }); + }); +} diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart new file mode 100644 index 000000000000..007c2e12188c --- /dev/null +++ b/script/tool/test/xctest_command_test.dart @@ -0,0 +1,358 @@ +// Copyright 2017 The Chromium 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:convert'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/xctest_command.dart'; +import 'package:test/test.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +final _kDeviceListMap = { + "runtimes": [ + { + "bundlePath": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime", + "buildversion": "17A577", + "runtimeRoot": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-0", + "version": "13.0", + "isAvailable": true, + "name": "iOS 13.0" + }, + { + "bundlePath": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime", + "buildversion": "17L255", + "runtimeRoot": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-4", + "version": "13.4", + "isAvailable": true, + "name": "iOS 13.4" + }, + { + "bundlePath": + "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime", + "buildversion": "17T531", + "runtimeRoot": + "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.watchOS-6-2", + "version": "6.2.1", + "isAvailable": true, + "name": "watchOS 6.2" + } + ], + "devices": { + "com.apple.CoreSimulator.SimRuntime.iOS-13-4": [ + { + "dataPath": + "/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data", + "logPath": + "/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774", + "udid": "2706BBEB-1E01-403E-A8E9-70E8E5A24774", + "isAvailable": true, + "deviceTypeIdentifier": + "com.apple.CoreSimulator.SimDeviceType.iPhone-8", + "state": "Shutdown", + "name": "iPhone 8" + }, + { + "dataPath": + "/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data", + "logPath": + "/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A", + "udid": "1E76A0FD-38AC-4537-A989-EA639D7D012A", + "isAvailable": true, + "deviceTypeIdentifier": + "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", + "state": "Shutdown", + "name": "iPhone 8 Plus" + } + ] + } +}; + +void main() { + const String _kDestination = '--ios-destination'; + const String _kTarget = '--target'; + const String _kSkip = '--skip'; + + group('test xctest_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final XCTestCommand command = XCTestCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('xctest_command', 'Test for xctest_command'); + runner.addCommand(command); + cleanupPackages(); + }); + + test('Not specifying --target throws', () async { + await expectLater( + () => runner.run(['xctest', _kDestination, 'a_destination']), + throwsA(const TypeMatcher())); + }); + + test('skip if ios is not supported', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + expect(output, contains('iOS is not supported by this plugin.')); + expect(processRunner.recordedCalls, orderedEquals([])); + + cleanupPackages(); + }); + + test('running with correct scheme and destination, did not find scheme', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = '{"project":{"targets":["bar_scheme"]}}'; + + await expectLater(() async { + final List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + expect(output, + contains('foo_scheme not configured for plugin, test failed.')); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcodebuild', + [ + '-project', + 'ios/Runner.xcodeproj', + '-list', + '-json' + ], + pluginExampleDirectory.path), + ])); + }, throwsA(const TypeMatcher())); + cleanupPackages(); + }); + + test('running with correct scheme and destination, found scheme', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + + expect(output, contains('Successfully ran xctest for plugin')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'foo_destination', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory.path), + ])); + + cleanupPackages(); + }); + + test('running with correct scheme and destination, skip 1 plugin', + () async { + createFakePlugin('plugin1', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + createFakePlugin('plugin2', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory1 = + mockPackagesDir.childDirectory('plugin1').childDirectory('example'); + createFakePubspec(pluginExampleDirectory1, isFlutter: true); + final Directory pluginExampleDirectory2 = + mockPackagesDir.childDirectory('plugin2').childDirectory('example'); + createFakePubspec(pluginExampleDirectory2, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination', + _kSkip, + 'plugin1' + ]); + + expect(output, contains('plugin1 was skipped with the --skip flag.')); + expect(output, contains('Successfully ran xctest for plugin2')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory2.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'foo_destination', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory2.path), + ])); + + cleanupPackages(); + }); + + test('Not specifying --ios-destination assigns an available simulator', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final Map schemeCommandResult = { + "project": { + "targets": ["bar_scheme", "foo_scheme"] + } + }; + // For simplicity of the test, we combine all the mock results into a single mock result, each internal command + // will get this result and they should still be able to parse them correctly. + processRunner.resultStdout = + jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); + await runner.run([ + 'xctest', + _kTarget, + 'foo_scheme', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory.path), + ])); + + cleanupPackages(); + }); + }); +}