Move plugin tool tests over (#3606)
diff --git a/.cirrus.yml b/.cirrus.yml
index 5a25b77..118802b 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -16,10 +16,12 @@
- 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 @@
- 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 @@
- 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 @@
- 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 @@
- 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 d9fce4a..e471239 100644
--- a/script/tool/pubspec.yaml
+++ b/script/tool/pubspec.yaml
@@ -21,5 +21,10 @@
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 0000000..9e7a42b
--- /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<Null>('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(<String>['analyze']);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall('pub', <String>['global', 'activate', 'tuneup'],
+ mockPackagesDir.path),
+ ProcessCall('flutter', <String>['packages', 'get'], plugin1Dir.path),
+ ProcessCall('flutter', <String>['packages', 'get'], plugin2Dir.path),
+ ProcessCall('pub', <String>['global', 'run', 'tuneup', 'check'],
+ plugin1Dir.path),
+ ProcessCall('pub', <String>['global', 'run', 'tuneup', 'check'],
+ plugin2Dir.path),
+ ]));
+ });
+
+ group('verifies analysis settings', () {
+ test('fails analysis_options.yaml', () async {
+ await createFakePlugin('foo', withExtraFiles: <List<String>>[
+ <String>['analysis_options.yaml']
+ ]);
+
+ await expectLater(() => runner.run(<String>['analyze']),
+ throwsA(const TypeMatcher<ToolExit>()));
+ });
+
+ test('fails .analysis_options', () async {
+ await createFakePlugin('foo', withExtraFiles: <List<String>>[
+ <String>['.analysis_options']
+ ]);
+
+ await expectLater(() => runner.run(<String>['analyze']),
+ throwsA(const TypeMatcher<ToolExit>()));
+ });
+
+ test('takes an allow list', () async {
+ final Directory pluginDir =
+ await createFakePlugin('foo', withExtraFiles: <List<String>>[
+ <String>['analysis_options.yaml']
+ ]);
+
+ final MockProcess mockProcess = MockProcess();
+ mockProcess.exitCodeCompleter.complete(0);
+ processRunner.processToReturn = mockProcess;
+ await runner.run(<String>['analyze', '--custom-analysis', 'foo']);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall('pub', <String>['global', 'activate', 'tuneup'],
+ mockPackagesDir.path),
+ ProcessCall('flutter', <String>['packages', 'get'], pluginDir.path),
+ ProcessCall('pub', <String>['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 0000000..eaf5049
--- /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<Null> 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<Null>(
+ '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: <List<String>>[
+ <String>['example', 'test'],
+ ],
+ isLinuxPlugin: false);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['build-examples', '--ipa', '--no-macos']);
+ final String packageName =
+ p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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(<ProcessCall>[]));
+ cleanupPackages();
+ });
+
+ test('building for ios', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test'],
+ ],
+ isIosPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'build-examples',
+ '--ipa',
+ '--no-macos',
+ '--enable-experiment=exp1'
+ ]);
+ final String packageName =
+ p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\nBUILDING IPA for $packageName',
+ '\n\n',
+ 'All builds successful!',
+ ]),
+ );
+
+ print(processRunner.recordedCalls);
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall(
+ flutterCommand,
+ <String>[
+ '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: <List<String>>[
+ <String>['example', 'test'],
+ ],
+ isLinuxPlugin: false);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['build-examples', '--no-ipa', '--linux']);
+ final String packageName =
+ p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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(<ProcessCall>[]));
+ cleanupPackages();
+ });
+
+ test('building for Linux', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test'],
+ ],
+ isLinuxPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['build-examples', '--no-ipa', '--linux']);
+ final String packageName =
+ p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\nBUILDING Linux for $packageName',
+ '\n\n',
+ 'All builds successful!',
+ ]),
+ );
+
+ print(processRunner.recordedCalls);
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall(flutterCommand, <String>['build', 'linux'],
+ pluginExampleDirectory.path),
+ ]));
+ cleanupPackages();
+ });
+
+ test('building for macos with no implementation results in no-op',
+ () async {
+ createFakePlugin('plugin', withExtraFiles: <List<String>>[
+ <String>['example', 'test'],
+ ]);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['build-examples', '--no-ipa', '--macos']);
+ final String packageName =
+ p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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(<ProcessCall>[]));
+ cleanupPackages();
+ });
+ test('building for macos', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test'],
+ <String>['example', 'macos', 'macos.swift'],
+ ],
+ isMacOsPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['build-examples', '--no-ipa', '--macos']);
+ final String packageName =
+ p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\nBUILDING macOS for $packageName',
+ '\n\n',
+ 'All builds successful!',
+ ]),
+ );
+
+ print(processRunner.recordedCalls);
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall(flutterCommand, <String>['pub', 'get'],
+ pluginExampleDirectory.path),
+ ProcessCall(flutterCommand, <String>['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: <List<String>>[
+ <String>['example', 'test'],
+ ],
+ isWindowsPlugin: false);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['build-examples', '--no-ipa', '--windows']);
+ final String packageName =
+ p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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(<ProcessCall>[]));
+ cleanupPackages();
+ });
+
+ test('building for windows', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test'],
+ ],
+ isWindowsPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['build-examples', '--no-ipa', '--windows']);
+ final String packageName =
+ p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\nBUILDING Windows for $packageName',
+ '\n\n',
+ 'All builds successful!',
+ ]),
+ );
+
+ print(processRunner.recordedCalls);
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall(flutterCommand, <String>['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: <List<String>>[
+ <String>['example', 'test'],
+ ],
+ isLinuxPlugin: false);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['build-examples', '--apk', '--no-ipa']);
+ final String packageName =
+ p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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(<ProcessCall>[]));
+ cleanupPackages();
+ });
+
+ test('building for android', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test'],
+ ],
+ isAndroidPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'build-examples',
+ '--apk',
+ '--no-ipa',
+ '--no-macos',
+ ]);
+ final String packageName =
+ p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\nBUILDING APK for $packageName',
+ '\n\n',
+ 'All builds successful!',
+ ]),
+ );
+
+ print(processRunner.recordedCalls);
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall(flutterCommand, <String>['build', 'apk'],
+ pluginExampleDirectory.path),
+ ]));
+ cleanupPackages();
+ });
+
+ test('enable-experiment flag for Android', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test'],
+ ],
+ isAndroidPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ await runCapturingPrint(runner, <String>[
+ 'build-examples',
+ '--apk',
+ '--no-ipa',
+ '--no-macos',
+ '--enable-experiment=exp1'
+ ]);
+
+ print(processRunner.recordedCalls);
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall(
+ flutterCommand,
+ <String>['build', 'apk', '--enable-experiment=exp1'],
+ pluginExampleDirectory.path),
+ ]));
+ cleanupPackages();
+ });
+
+ test('enable-experiment flag for ios', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test'],
+ ],
+ isIosPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ await runCapturingPrint(runner, <String>[
+ 'build-examples',
+ '--ipa',
+ '--no-macos',
+ '--enable-experiment=exp1'
+ ]);
+ print(processRunner.recordedCalls);
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall(
+ flutterCommand,
+ <String>[
+ '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 0000000..b3504c2
--- /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<String> plugins;
+
+ setUp(() {
+ initializeFakePackages();
+ processRunner = RecordingProcessRunner();
+ plugins = [];
+ final SamplePluginCommand samplePluginCommand = SamplePluginCommand(
+ plugins,
+ mockPackagesDir,
+ mockFileSystem,
+ processRunner: processRunner,
+ );
+ runner =
+ CommandRunner<Null>('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(<String>['sample']);
+ expect(plugins, unorderedEquals(<String>[plugin1.path, plugin2.path]));
+ });
+
+ test('exclude plugins when plugins flag is specified', () async {
+ createFakePlugin('plugin1');
+ final Directory plugin2 = createFakePlugin('plugin2');
+ await runner.run(
+ <String>['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']);
+ expect(plugins, unorderedEquals(<String>[plugin2.path]));
+ });
+
+ test('exclude plugins when plugins flag isn\'t specified', () async {
+ createFakePlugin('plugin1');
+ createFakePlugin('plugin2');
+ await runner.run(<String>['sample', '--exclude=plugin1,plugin2']);
+ expect(plugins, unorderedEquals(<String>[]));
+ });
+
+ test('exclude federated plugins when plugins flag is specified', () async {
+ createFakePlugin('plugin1', parentDirectoryName: 'federated');
+ final Directory plugin2 = createFakePlugin('plugin2');
+ await runner.run(<String>[
+ 'sample',
+ '--plugins=federated/plugin1,plugin2',
+ '--exclude=federated/plugin1'
+ ]);
+ expect(plugins, unorderedEquals(<String>[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(<String>[
+ 'sample',
+ '--plugins=federated/plugin1,plugin2',
+ '--exclude=federated'
+ ]);
+ expect(plugins, unorderedEquals(<String>[plugin2.path]));
+ });
+}
+
+class SamplePluginCommand extends PluginCommand {
+ SamplePluginCommand(
+ this.plugins_,
+ Directory packagesDir,
+ FileSystem fileSystem, {
+ ProcessRunner processRunner = const ProcessRunner(),
+ }) : super(packagesDir, fileSystem, processRunner: processRunner);
+
+ List<String> plugins_;
+
+ @override
+ final String name = 'sample';
+
+ @override
+ final String description = 'sample command';
+
+ @override
+ Future<Null> 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 0000000..f4bdd95
--- /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<Null> 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<Null>(
+ 'drive_examples_command', 'Test for drive_example_command');
+ runner.addCommand(command);
+ });
+
+ tearDown(() {
+ cleanupPackages();
+ });
+
+ test('driving under folder "test"', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test_driver', 'plugin_test.dart'],
+ <String>['example', 'test', 'plugin.dart'],
+ ],
+ isIosPlugin: true,
+ isAndroidPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'drive-examples',
+ ]);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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>[
+ ProcessCall(
+ flutterCommand,
+ <String>[
+ 'drive',
+ '--driver',
+ driverTestPath,
+ '--target',
+ deviceTestPath
+ ],
+ pluginExampleDirectory.path),
+ ]));
+ });
+
+ test('driving under folder "test_driver"', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test_driver', 'plugin_test.dart'],
+ <String>['example', 'test_driver', 'plugin.dart'],
+ ],
+ isAndroidPlugin: true,
+ isIosPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'drive-examples',
+ ]);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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>[
+ ProcessCall(
+ flutterCommand,
+ <String>[
+ 'drive',
+ '--driver',
+ driverTestPath,
+ '--target',
+ deviceTestPath
+ ],
+ pluginExampleDirectory.path),
+ ]));
+ });
+
+ test('driving under folder "test_driver" when test files are missing"',
+ () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['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, <String>['drive-examples']),
+ throwsA(const TypeMatcher<ToolExit>()));
+ });
+
+ test(
+ 'driving under folder "test_driver" when targets are under "integration_test"',
+ () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test_driver', 'integration_test.dart'],
+ <String>['example', 'integration_test', 'bar_test.dart'],
+ <String>['example', 'integration_test', 'foo_test.dart'],
+ <String>['example', 'integration_test', 'ignore_me.dart'],
+ ],
+ isAndroidPlugin: true,
+ isIosPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'drive-examples',
+ ]);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\n\n',
+ 'All driver tests successful!',
+ ]),
+ );
+
+ String driverTestPath = p.join('test_driver', 'integration_test.dart');
+ print(processRunner.recordedCalls);
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall(
+ flutterCommand,
+ <String>[
+ 'drive',
+ '--driver',
+ driverTestPath,
+ '--target',
+ p.join('integration_test', 'bar_test.dart'),
+ ],
+ pluginExampleDirectory.path),
+ ProcessCall(
+ flutterCommand,
+ <String>[
+ '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: <List<String>>[
+ <String>['example', 'test_driver', 'plugin_test.dart'],
+ <String>['example', 'test_driver', 'plugin.dart'],
+ ],
+ isMacOsPlugin: false);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'drive-examples',
+ '--linux',
+ ]);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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, <ProcessCall>[]);
+ });
+
+ test('driving on a Linux plugin', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test_driver', 'plugin_test.dart'],
+ <String>['example', 'test_driver', 'plugin.dart'],
+ ],
+ isLinuxPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'drive-examples',
+ '--linux',
+ ]);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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>[
+ ProcessCall(
+ flutterCommand,
+ <String>[
+ '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: <List<String>>[
+ <String>['example', 'test_driver', 'plugin_test.dart'],
+ <String>['example', 'test_driver', 'plugin.dart'],
+ ]);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'drive-examples',
+ '--macos',
+ ]);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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, <ProcessCall>[]);
+ });
+ test('driving on a macOS plugin', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test_driver', 'plugin_test.dart'],
+ <String>['example', 'test_driver', 'plugin.dart'],
+ <String>['example', 'macos', 'macos.swift'],
+ ],
+ isMacOsPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'drive-examples',
+ '--macos',
+ ]);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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>[
+ ProcessCall(
+ flutterCommand,
+ <String>[
+ '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: <List<String>>[
+ <String>['example', 'test_driver', 'plugin_test.dart'],
+ <String>['example', 'test_driver', 'plugin.dart'],
+ ],
+ isMacOsPlugin: false);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'drive-examples',
+ '--windows',
+ ]);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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, <ProcessCall>[]);
+ });
+
+ test('driving on a Windows plugin', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test_driver', 'plugin_test.dart'],
+ <String>['example', 'test_driver', 'plugin.dart'],
+ ],
+ isWindowsPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'drive-examples',
+ '--windows',
+ ]);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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>[
+ ProcessCall(
+ flutterCommand,
+ <String>[
+ 'drive',
+ '-d',
+ 'windows',
+ '--driver',
+ driverTestPath,
+ '--target',
+ deviceTestPath
+ ],
+ pluginExampleDirectory.path),
+ ]));
+ });
+
+ test('driving when plugin does not support mobile is no-op', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test_driver', 'plugin_test.dart'],
+ <String>['example', 'test_driver', 'plugin.dart'],
+ ],
+ isMacOsPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'drive-examples',
+ ]);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '\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, <ProcessCall>[]);
+ });
+
+ test('enable-experiment flag', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['example', 'test_driver', 'plugin_test.dart'],
+ <String>['example', 'test', 'plugin.dart'],
+ ],
+ isIosPlugin: true,
+ isAndroidPlugin: true);
+
+ final Directory pluginExampleDirectory =
+ mockPackagesDir.childDirectory('plugin').childDirectory('example');
+
+ createFakePubspec(pluginExampleDirectory, isFlutter: true);
+
+ await runCapturingPrint(runner, <String>[
+ '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>[
+ ProcessCall(
+ flutterCommand,
+ <String>[
+ '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 0000000..97b9776
--- /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<String> printedMessages = <String>[];
+ CommandRunner<FirebaseTestLabCommand> runner;
+ RecordingProcessRunner processRunner;
+
+ setUp(() {
+ initializeFakePackages();
+ processRunner = RecordingProcessRunner();
+ final FirebaseTestLabCommand command = FirebaseTestLabCommand(
+ mockPackagesDir, mockFileSystem,
+ processRunner: processRunner,
+ print: (Object message) => printedMessages.add(message.toString()));
+
+ runner = CommandRunner<Null>(
+ '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: <List<String>>[
+ <String>['lib/test/should_not_run_e2e.dart'],
+ <String>['example', 'test_driver', 'plugin_e2e.dart'],
+ <String>['example', 'test_driver', 'plugin_e2e_test.dart'],
+ <String>['example', 'android', 'gradlew'],
+ <String>['example', 'should_not_run_e2e.dart'],
+ <String>[
+ 'example',
+ 'android',
+ 'app',
+ 'src',
+ 'androidTest',
+ 'MainActivityTest.java'
+ ],
+ ]);
+ await expectLater(
+ () => runCapturingPrint(runner, <String>['firebase-test-lab']),
+ throwsA(const TypeMatcher<ToolExit>()));
+ expect(
+ printedMessages,
+ contains(
+ "\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway."));
+ });
+
+ test('runs e2e tests', () async {
+ createFakePlugin('plugin', withExtraFiles: <List<String>>[
+ <String>['test', 'plugin_test.dart'],
+ <String>['test', 'plugin_e2e.dart'],
+ <String>['should_not_run_e2e.dart'],
+ <String>['lib/test/should_not_run_e2e.dart'],
+ <String>['example', 'test', 'plugin_e2e.dart'],
+ <String>['example', 'test_driver', 'plugin_e2e.dart'],
+ <String>['example', 'test_driver', 'plugin_e2e_test.dart'],
+ <String>['example', 'integration_test', 'foo_test.dart'],
+ <String>['example', 'integration_test', 'should_not_run.dart'],
+ <String>['example', 'android', 'gradlew'],
+ <String>['example', 'should_not_run_e2e.dart'],
+ <String>[
+ 'example',
+ 'android',
+ 'app',
+ 'src',
+ 'androidTest',
+ 'MainActivityTest.java'
+ ],
+ ]);
+
+ final List<String> output = await runCapturingPrint(runner, <String>[
+ 'firebase-test-lab',
+ '--device',
+ 'model=flame,version=29',
+ '--device',
+ 'model=seoul,version=26',
+ '--test-run-id',
+ 'testRunId',
+ ]);
+
+ expect(
+ printedMessages,
+ orderedEquals(<String>[
+ '\nRUNNING FIREBASE TEST LAB TESTS for plugin',
+ '\nFirebase project configured.',
+ '\n\n',
+ 'All Firebase Test Lab tests successful!',
+ ]),
+ );
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ 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: <List<String>>[
+ <String>['test', 'plugin_test.dart'],
+ <String>['test', 'plugin_e2e.dart'],
+ <String>['should_not_run_e2e.dart'],
+ <String>['lib/test/should_not_run_e2e.dart'],
+ <String>['example', 'test', 'plugin_e2e.dart'],
+ <String>['example', 'test_driver', 'plugin_e2e.dart'],
+ <String>['example', 'test_driver', 'plugin_e2e_test.dart'],
+ <String>['example', 'integration_test', 'foo_test.dart'],
+ <String>['example', 'integration_test', 'should_not_run.dart'],
+ <String>['example', 'android', 'gradlew'],
+ <String>['example', 'should_not_run_e2e.dart'],
+ <String>[
+ 'example',
+ 'android',
+ 'app',
+ 'src',
+ 'androidTest',
+ 'MainActivityTest.java'
+ ],
+ ]);
+
+ await runCapturingPrint(runner, <String>[
+ 'firebase-test-lab',
+ '--device',
+ 'model=flame,version=29',
+ '--test-run-id',
+ 'testRunId',
+ '--enable-experiment=exp1',
+ ]);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ 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 0000000..49d6ad4
--- /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<Null> runner;
+ MockPlatform mockPlatform;
+ final RecordingProcessRunner processRunner = RecordingProcessRunner();
+ List<String> printedMessages;
+
+ setUp(() {
+ initializeFakePackages();
+
+ printedMessages = <String>[];
+ 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<Null>('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: <List<String>>[
+ <String>['plugin1.podspec'],
+ ]);
+
+ when(mockPlatform.isMacOS).thenReturn(false);
+ await runner.run(<String>['podspecs']);
+
+ expect(
+ processRunner.recordedCalls,
+ equals(<ProcessCall>[]),
+ );
+ });
+
+ test('runs pod lib lint on a podspec', () async {
+ Directory plugin1Dir =
+ createFakePlugin('plugin1', withExtraFiles: <List<String>>[
+ <String>['ios', 'plugin1.podspec'],
+ <String>['bogus.dart'], // Ignore non-podspecs.
+ ]);
+
+ processRunner.resultStdout = 'Foo';
+ processRunner.resultStderr = 'Bar';
+
+ await runner.run(<String>['podspecs']);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall('which', <String>['pod'], mockPackagesDir.path),
+ ProcessCall(
+ 'pod',
+ <String>[
+ 'lib',
+ 'lint',
+ p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'),
+ '--analyze',
+ '--use-libraries'
+ ],
+ mockPackagesDir.path),
+ ProcessCall(
+ 'pod',
+ <String>[
+ '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: <List<String>>[
+ <String>['plugin1.podspec']
+ ]);
+ createFakePlugin('plugin2', withExtraFiles: <List<String>>[
+ <String>['plugin2.podspec']
+ ]);
+
+ await runner
+ .run(<String>['podspecs', '--skip=plugin1', '--skip=plugin2']);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall('which', <String>['pod'], mockPackagesDir.path),
+ ]),
+ );
+ });
+
+ test('skips analyzer for podspecs with known warnings', () async {
+ Directory plugin1Dir =
+ createFakePlugin('plugin1', withExtraFiles: <List<String>>[
+ <String>['plugin1.podspec'],
+ ]);
+
+ await runner.run(<String>['podspecs', '--no-analyze=plugin1']);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall('which', <String>['pod'], mockPackagesDir.path),
+ ProcessCall(
+ 'pod',
+ <String>[
+ 'lib',
+ 'lint',
+ p.join(plugin1Dir.path, 'plugin1.podspec'),
+ '--use-libraries'
+ ],
+ mockPackagesDir.path),
+ ProcessCall(
+ 'pod',
+ <String>[
+ '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: <List<String>>[
+ <String>['plugin1.podspec'],
+ ]);
+
+ await runner.run(<String>['podspecs', '--ignore-warnings=plugin1']);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall('which', <String>['pod'], mockPackagesDir.path),
+ ProcessCall(
+ 'pod',
+ <String>[
+ 'lib',
+ 'lint',
+ p.join(plugin1Dir.path, 'plugin1.podspec'),
+ '--allow-warnings',
+ '--analyze',
+ '--use-libraries'
+ ],
+ mockPackagesDir.path),
+ ProcessCall(
+ 'pod',
+ <String>[
+ '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 0000000..4786252
--- /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<ListCommand> runner;
+
+ setUp(() {
+ initializeFakePackages();
+ final ListCommand command = ListCommand(mockPackagesDir, mockFileSystem);
+
+ runner = CommandRunner<Null>('list_test', 'Test for $ListCommand');
+ runner.addCommand(command);
+ });
+
+ test('lists plugins', () async {
+ createFakePlugin('plugin1');
+ createFakePlugin('plugin2');
+
+ final List<String> plugins =
+ await runCapturingPrint(runner, <String>['list', '--type=plugin']);
+
+ expect(
+ plugins,
+ orderedEquals(<String>[
+ '/packages/plugin1',
+ '/packages/plugin2',
+ ]),
+ );
+
+ cleanupPackages();
+ });
+
+ test('lists examples', () async {
+ createFakePlugin('plugin1', withSingleExample: true);
+ createFakePlugin('plugin2',
+ withExamples: <String>['example1', 'example2']);
+ createFakePlugin('plugin3');
+
+ final List<String> examples =
+ await runCapturingPrint(runner, <String>['list', '--type=example']);
+
+ expect(
+ examples,
+ orderedEquals(<String>[
+ '/packages/plugin1/example',
+ '/packages/plugin2/example/example1',
+ '/packages/plugin2/example/example2',
+ ]),
+ );
+
+ cleanupPackages();
+ });
+
+ test('lists packages', () async {
+ createFakePlugin('plugin1', withSingleExample: true);
+ createFakePlugin('plugin2',
+ withExamples: <String>['example1', 'example2']);
+ createFakePlugin('plugin3');
+
+ final List<String> packages =
+ await runCapturingPrint(runner, <String>['list', '--type=package']);
+
+ expect(
+ packages,
+ unorderedEquals(<String>[
+ '/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: <String>['example1', 'example2']);
+ createFakePlugin('plugin3');
+
+ final List<String> examples =
+ await runCapturingPrint(runner, <String>['list', '--type=file']);
+
+ expect(
+ examples,
+ unorderedEquals(<String>[
+ '/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<String> plugins =
+ await runCapturingPrint(runner, <String>['list']);
+
+ expect(
+ plugins,
+ unorderedEquals(<String>[
+ '/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<String> plugins = await runCapturingPrint(
+ runner, <String>['list', '--plugins=plugin1']);
+ expect(
+ plugins,
+ unorderedEquals(<String>[
+ '/packages/plugin1',
+ ]),
+ );
+
+ plugins = await runCapturingPrint(
+ runner, <String>['list', '--plugins=my_plugin']);
+ expect(
+ plugins,
+ unorderedEquals(<String>[
+ '/packages/my_plugin/my_plugin',
+ '/packages/my_plugin/my_plugin_web',
+ '/packages/my_plugin/my_plugin_macos',
+ ]),
+ );
+
+ plugins = await runCapturingPrint(
+ runner, <String>['list', '--plugins=my_plugin/my_plugin_web']);
+ expect(
+ plugins,
+ unorderedEquals(<String>[
+ '/packages/my_plugin/my_plugin_web',
+ ]),
+ );
+
+ plugins = await runCapturingPrint(runner,
+ <String>['list', '--plugins=my_plugin/my_plugin_web,plugin1']);
+ expect(
+ plugins,
+ unorderedEquals(<String>[
+ '/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 0000000..3e17ff8
--- /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<int> exitCodeCompleter = Completer<int>();
+ final StreamController<List<int>> stdoutController =
+ StreamController<List<int>>();
+ final StreamController<List<int>> stderrController =
+ StreamController<List<int>>();
+ final MockIOSink stdinMock = MockIOSink();
+
+ @override
+ Future<int> get exitCode => exitCodeCompleter.future;
+
+ @override
+ Stream<List<int>> get stdout => stdoutController.stream;
+
+ @override
+ Stream<List<int>> get stderr => stderrController.stream;
+
+ @override
+ IOSink get stdin => stdinMock;
+}
+
+class MockIOSink extends Mock implements IOSink {
+ List<String> lines = <String>[];
+
+ @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 0000000..ada4bf0
--- /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<String> printedMessages = <String>[];
+
+ Directory parentDir;
+ Directory pluginDir;
+ GitDir gitDir;
+ TestProcessRunner processRunner;
+ CommandRunner<Null> 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', <String>['init'],
+ workingDirectory: mockPackagesDir.path);
+ gitDir = await GitDir.fromExisting(mockPackagesDir.path);
+ await gitDir.runCommand(<String>['add', '-A']);
+ await gitDir.runCommand(<String>['commit', '-m', 'Initial commit']);
+ processRunner = TestProcessRunner();
+ mockStdin = MockStdin();
+ commandRunner = CommandRunner<Null>('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(<String>['publish-plugin']),
+ throwsA(const TypeMatcher<ToolExit>()));
+
+ expect(
+ printedMessages.last, contains("Must specify a package to publish."));
+ });
+
+ test('requires an existing flag', () async {
+ await expectLater(
+ () => commandRunner
+ .run(<String>['publish-plugin', '--package', 'iamerror']),
+ throwsA(const TypeMatcher<ToolExit>()));
+
+ expect(printedMessages.last, contains('iamerror does not exist'));
+ });
+
+ test('refuses to proceed with dirty files', () async {
+ pluginDir.childFile('tmp').createSync();
+
+ await expectLater(
+ () => commandRunner
+ .run(<String>['publish-plugin', '--package', testPluginName]),
+ throwsA(const TypeMatcher<ToolExit>()));
+
+ 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(<String>['publish-plugin', '--package', testPluginName]),
+ throwsA(const TypeMatcher<ToolExit>()));
+
+ 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(<String>[
+ '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', <String>['init'],
+ workingDirectory: mockPackagesDir.path);
+ gitDir = await GitDir.fromExisting(mockPackagesDir.path);
+ await gitDir.runCommand(<String>['add', '-A']);
+ await gitDir.runCommand(<String>['commit', '-m', 'Initial commit']);
+ // Immediately return 0 when running `pub publish`.
+ processRunner.mockPublishProcess.exitCodeCompleter.complete(0);
+ await commandRunner.run(<String>[
+ '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<void> publishCommand = commandRunner.run(<String>[
+ '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<void> publishCommand = commandRunner.run(<String>[
+ '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(<String>[
+ '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(<String>[
+ 'publish-plugin',
+ '--package',
+ testPluginName,
+ '--no-push-tags',
+ '--no-tag-release',
+ ]),
+ throwsA(const TypeMatcher<ToolExit>()));
+
+ 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(<String>[
+ 'publish-plugin',
+ '--package',
+ testPluginName,
+ '--no-push-tags',
+ ]);
+
+ final String tag =
+ (await gitDir.runCommand(<String>['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(<String>[
+ 'publish-plugin',
+ '--package',
+ testPluginName,
+ '--no-push-tags',
+ ]),
+ throwsA(const TypeMatcher<ToolExit>()));
+
+ expect(printedMessages, contains("Publish failed. Exiting."));
+ final String tag = (await gitDir.runCommand(
+ <String>['show-ref', 'fake_package-v0.0.1'],
+ throwOnError: false))
+ .stdout;
+ expect(tag, isEmpty);
+ });
+ });
+
+ group('Pushes tags', () {
+ setUp(() async {
+ await gitDir.runCommand(
+ <String>['remote', 'add', 'upstream', 'http://localhost:8000']);
+ });
+
+ test('requires user confirmation', () async {
+ processRunner.mockPublishProcess.exitCodeCompleter.complete(0);
+ mockStdin.readLineOutput = 'help';
+ await expectLater(
+ () => commandRunner.run(<String>[
+ 'publish-plugin',
+ '--package',
+ testPluginName,
+ ]),
+ throwsA(const TypeMatcher<ToolExit>()));
+
+ expect(printedMessages, contains('Tag push canceled.'));
+ });
+
+ test('to upstream by default', () async {
+ await gitDir.runCommand(<String>['tag', 'garbage']);
+ processRunner.mockPublishProcess.exitCodeCompleter.complete(0);
+ mockStdin.readLineOutput = 'y';
+ await commandRunner.run(<String>[
+ '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(
+ <String>['remote', 'add', 'origin', 'http://localhost:8001']);
+ processRunner.mockPublishProcess.exitCodeCompleter.complete(0);
+ mockStdin.readLineOutput = 'y';
+ await commandRunner.run(<String>[
+ '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(<String>[
+ 'publish-plugin',
+ '--package',
+ testPluginName,
+ '--no-tag-release',
+ ]);
+
+ expect(processRunner.pushTagsArgs.isEmpty, isTrue);
+ expect(printedMessages.last, 'Done!');
+ });
+ });
+}
+
+class TestProcessRunner extends ProcessRunner {
+ final List<io.ProcessResult> results = <io.ProcessResult>[];
+ final MockProcess mockPublishProcess = MockProcess();
+ final List<String> mockPublishArgs = <String>[];
+ final MockProcessResult mockPushTagsResult = MockProcessResult();
+ final List<String> pushTagsArgs = <String>[];
+
+ @override
+ Future<io.ProcessResult> runAndExitOnError(
+ String executable,
+ List<String> 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<io.Process> start(String executable, List<String> 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<List<int>> controller = StreamController<List<int>>();
+ String readLineOutput;
+
+ @override
+ Stream<S> transform<S>(StreamTransformer<dynamic, S> streamTransformer) {
+ return controller.stream.transform(streamTransformer);
+ }
+
+ @override
+ StreamSubscription<List<int>> listen(void onData(List<int> 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 0000000..514e4c2
--- /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<TestCommand> runner;
+ final RecordingProcessRunner processRunner = RecordingProcessRunner();
+
+ setUp(() {
+ initializeFakePackages();
+ final TestCommand command = TestCommand(mockPackagesDir, mockFileSystem,
+ processRunner: processRunner);
+
+ runner = CommandRunner<Null>('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: <List<String>>[
+ <String>['test', 'empty_test.dart'],
+ ]);
+ final Directory plugin2Dir =
+ createFakePlugin('plugin2', withExtraFiles: <List<String>>[
+ <String>['test', 'empty_test.dart'],
+ ]);
+
+ await runner.run(<String>['test']);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall('flutter', <String>['test', '--color'], plugin1Dir.path),
+ ProcessCall('flutter', <String>['test', '--color'], plugin2Dir.path),
+ ]),
+ );
+
+ cleanupPackages();
+ });
+
+ test('skips testing plugins without test directory', () async {
+ createFakePlugin('plugin1');
+ final Directory plugin2Dir =
+ createFakePlugin('plugin2', withExtraFiles: <List<String>>[
+ <String>['test', 'empty_test.dart'],
+ ]);
+
+ await runner.run(<String>['test']);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall('flutter', <String>['test', '--color'], plugin2Dir.path),
+ ]),
+ );
+
+ cleanupPackages();
+ });
+
+ test('runs pub run test on non-Flutter packages', () async {
+ final Directory plugin1Dir = createFakePlugin('plugin1',
+ isFlutter: true,
+ withExtraFiles: <List<String>>[
+ <String>['test', 'empty_test.dart'],
+ ]);
+ final Directory plugin2Dir = createFakePlugin('plugin2',
+ isFlutter: false,
+ withExtraFiles: <List<String>>[
+ <String>['test', 'empty_test.dart'],
+ ]);
+
+ await runner.run(<String>['test', '--enable-experiment=exp1']);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall(
+ 'flutter',
+ <String>['test', '--color', '--enable-experiment=exp1'],
+ plugin1Dir.path),
+ ProcessCall('pub', <String>['get'], plugin2Dir.path),
+ ProcessCall(
+ 'pub',
+ <String>['run', '--enable-experiment=exp1', 'test'],
+ plugin2Dir.path),
+ ]),
+ );
+
+ cleanupPackages();
+ });
+
+ test('runs on Chrome for web plugins', () async {
+ final Directory pluginDir = createFakePlugin(
+ 'plugin',
+ withExtraFiles: <List<String>>[
+ <String>['test', 'empty_test.dart'],
+ ],
+ isFlutter: true,
+ isWebPlugin: true,
+ );
+
+ await runner.run(<String>['test']);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall('flutter',
+ <String>['test', '--color', '--platform=chrome'], pluginDir.path),
+ ]),
+ );
+ });
+
+ test('enable-experiment flag', () async {
+ final Directory plugin1Dir = createFakePlugin('plugin1',
+ isFlutter: true,
+ withExtraFiles: <List<String>>[
+ <String>['test', 'empty_test.dart'],
+ ]);
+ final Directory plugin2Dir = createFakePlugin('plugin2',
+ isFlutter: false,
+ withExtraFiles: <List<String>>[
+ <String>['test', 'empty_test.dart'],
+ ]);
+
+ await runner.run(<String>['test', '--enable-experiment=exp1']);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall(
+ 'flutter',
+ <String>['test', '--color', '--enable-experiment=exp1'],
+ plugin1Dir.path),
+ ProcessCall('pub', <String>['get'], plugin2Dir.path),
+ ProcessCall(
+ 'pub',
+ <String>['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 0000000..ec0000d
--- /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<String> withExamples = const <String>[],
+ List<List<String>> withExtraFiles = const <List<String>>[],
+ 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<String> file in withExtraFiles) {
+ final List<String> newFilePath = <String>[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<List<String>> runCapturingPrint(
+ CommandRunner<PluginCommand> runner, List<String> args) async {
+ final List<String> prints = <String>[];
+ final ZoneSpecification spec = ZoneSpecification(
+ print: (_, __, ___, String message) {
+ prints.add(message);
+ },
+ );
+ await Zone.current
+ .fork(specification: spec)
+ .run<Future<void>>(() => runner.run(args));
+
+ return prints;
+}
+
+/// A mock [ProcessRunner] which records process calls.
+class RecordingProcessRunner extends ProcessRunner {
+ io.Process processToReturn;
+ final List<ProcessCall> recordedCalls = <ProcessCall>[];
+
+ /// 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<int> runAndStream(
+ String executable,
+ List<String> args, {
+ Directory workingDir,
+ bool exitOnError = false,
+ }) async {
+ recordedCalls.add(ProcessCall(executable, args, workingDir?.path));
+ return Future<int>.value(
+ processToReturn == null ? 0 : await processToReturn.exitCode);
+ }
+
+ /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr].
+ @override
+ Future<io.ProcessResult> run(String executable, List<String> 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<io.ProcessResult>.value(result);
+ }
+
+ @override
+ Future<io.ProcessResult> runAndExitOnError(
+ String executable,
+ List<String> 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<io.ProcessResult>.value(result);
+ }
+
+ @override
+ Future<io.Process> start(String executable, List<String> args,
+ {Directory workingDirectory}) async {
+ recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path));
+ return Future<io.Process>.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<String> 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<String> command = <String>[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 0000000..b9ace38
--- /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<Version, NextVersionType> 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<VersionCheckCommand> runner;
+ RecordingProcessRunner processRunner;
+ List<List<String>> gitDirCommands;
+ String gitDiffResponse;
+ Map<String, String> gitShowResponses;
+
+ setUp(() {
+ gitDirCommands = <List<String>>[];
+ gitDiffResponse = '';
+ gitShowResponses = <String, String>{};
+ 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<String>(mockProcessResult.stdout).thenReturn(gitDiffResponse);
+ } else if (invocation.positionalArguments[0][0] == 'show') {
+ final String response =
+ gitShowResponses[invocation.positionalArguments[0][1]];
+ when<String>(mockProcessResult.stdout).thenReturn(response);
+ }
+ return Future<ProcessResult>.value(mockProcessResult);
+ });
+ initializeFakePackages();
+ processRunner = RecordingProcessRunner();
+ final VersionCheckCommand command = VersionCheckCommand(
+ mockPackagesDir, mockFileSystem,
+ processRunner: processRunner, gitDir: gitDir);
+
+ runner = CommandRunner<Null>(
+ 'version_check_command', 'Test for $VersionCheckCommand');
+ runner.addCommand(command);
+ });
+
+ tearDown(() {
+ cleanupPackages();
+ });
+
+ test('allows valid version', () async {
+ createFakePlugin('plugin');
+ gitDiffResponse = "packages/plugin/pubspec.yaml";
+ gitShowResponses = <String, String>{
+ 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0',
+ 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0',
+ };
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['version-check', '--base_sha=master']);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '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 = <String, String>{
+ 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1',
+ 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0',
+ };
+ final Future<List<String>> result = runCapturingPrint(
+ runner, <String>['version-check', '--base_sha=master']);
+
+ await expectLater(
+ result,
+ throwsA(const TypeMatcher<Error>()),
+ );
+ 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<String> output = await runCapturingPrint(
+ runner, <String>['version-check', '--base_sha=master']);
+
+ expect(
+ output,
+ orderedEquals(<String>[
+ '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 = <String, String>{
+ 'master:packages/plugin_platform_interface/pubspec.yaml':
+ 'version: 1.0.0',
+ 'HEAD:packages/plugin_platform_interface/pubspec.yaml':
+ 'version: 1.1.0',
+ };
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['version-check', '--base_sha=master']);
+ expect(
+ output,
+ orderedEquals(<String>[
+ '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 = <String, String>{
+ 'master:packages/plugin_platform_interface/pubspec.yaml':
+ 'version: 1.0.0',
+ 'HEAD:packages/plugin_platform_interface/pubspec.yaml':
+ 'version: 2.0.0',
+ };
+ final Future<List<String>> output = runCapturingPrint(
+ runner, <String>['version-check', '--base_sha=master']);
+ await expectLater(
+ output,
+ throwsA(const TypeMatcher<Error>()),
+ );
+ 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 0000000..007c2e1
--- /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<Null> runner;
+ RecordingProcessRunner processRunner;
+
+ setUp(() {
+ initializeFakePackages();
+ processRunner = RecordingProcessRunner();
+ final XCTestCommand command = XCTestCommand(
+ mockPackagesDir, mockFileSystem,
+ processRunner: processRunner);
+
+ runner = CommandRunner<Null>('xctest_command', 'Test for xctest_command');
+ runner.addCommand(command);
+ cleanupPackages();
+ });
+
+ test('Not specifying --target throws', () async {
+ await expectLater(
+ () => runner.run(<String>['xctest', _kDestination, 'a_destination']),
+ throwsA(const TypeMatcher<ToolExit>()));
+ });
+
+ test('skip if ios is not supported', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['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<String> output = await runCapturingPrint(runner, <String>[
+ 'xctest',
+ _kTarget,
+ 'foo_scheme',
+ _kDestination,
+ 'foo_destination'
+ ]);
+ expect(output, contains('iOS is not supported by this plugin.'));
+ expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[]));
+
+ cleanupPackages();
+ });
+
+ test('running with correct scheme and destination, did not find scheme',
+ () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['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<String> output = await runCapturingPrint(runner, <String>[
+ 'xctest',
+ _kTarget,
+ 'foo_scheme',
+ _kDestination,
+ 'foo_destination'
+ ]);
+ expect(output,
+ contains('foo_scheme not configured for plugin, test failed.'));
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall('xcrun', <String>['simctl', 'list', '--json'], null),
+ ProcessCall(
+ 'xcodebuild',
+ <String>[
+ '-project',
+ 'ios/Runner.xcodeproj',
+ '-list',
+ '-json'
+ ],
+ pluginExampleDirectory.path),
+ ]));
+ }, throwsA(const TypeMatcher<ToolExit>()));
+ cleanupPackages();
+ });
+
+ test('running with correct scheme and destination, found scheme', () async {
+ createFakePlugin('plugin',
+ withExtraFiles: <List<String>>[
+ <String>['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<String> output = await runCapturingPrint(runner, <String>[
+ 'xctest',
+ _kTarget,
+ 'foo_scheme',
+ _kDestination,
+ 'foo_destination'
+ ]);
+
+ expect(output, contains('Successfully ran xctest for plugin'));
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall(
+ 'xcodebuild',
+ <String>['-project', 'ios/Runner.xcodeproj', '-list', '-json'],
+ pluginExampleDirectory.path),
+ ProcessCall(
+ 'xcodebuild',
+ <String>[
+ '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: <List<String>>[
+ <String>['example', 'test'],
+ ],
+ isIosPlugin: true);
+ createFakePlugin('plugin2',
+ withExtraFiles: <List<String>>[
+ <String>['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<String> output = await runCapturingPrint(runner, <String>[
+ '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>[
+ ProcessCall(
+ 'xcodebuild',
+ <String>['-project', 'ios/Runner.xcodeproj', '-list', '-json'],
+ pluginExampleDirectory2.path),
+ ProcessCall(
+ 'xcodebuild',
+ <String>[
+ '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: <List<String>>[
+ <String>['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<String, dynamic> 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(<String>[
+ 'xctest',
+ _kTarget,
+ 'foo_scheme',
+ ]);
+
+ expect(
+ processRunner.recordedCalls,
+ orderedEquals(<ProcessCall>[
+ ProcessCall('xcrun', <String>['simctl', 'list', '--json'], null),
+ ProcessCall(
+ 'xcodebuild',
+ <String>['-project', 'ios/Runner.xcodeproj', '-list', '-json'],
+ pluginExampleDirectory.path),
+ ProcessCall(
+ 'xcodebuild',
+ <String>[
+ '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();
+ });
+ });
+}