// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; import 'package:flutter_plugin_tools/src/common.dart'; import 'package:git/git.dart'; import 'package:matcher/matcher.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; void main() { const String testPluginName = 'foo'; final List printedMessages = []; Directory parentDir; Directory pluginDir; GitDir gitDir; TestProcessRunner processRunner; CommandRunner commandRunner; MockStdin mockStdin; setUp(() async { // This test uses a local file system instead of an in memory one throughout // so that git actually works. In setup we initialize a mono repo of plugins // with one package and commit everything to Git. parentDir = const LocalFileSystem() .systemTempDirectory .createTempSync('publish_plugin_command_test-'); initializeFakePackages(parentDir: parentDir); pluginDir = createFakePlugin(testPluginName, withSingleExample: false); assert(pluginDir != null && pluginDir.existsSync()); createFakePubspec(pluginDir, includeVersion: true); io.Process.runSync('git', ['init'], workingDirectory: mockPackagesDir.path); gitDir = await GitDir.fromExisting(mockPackagesDir.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Initial commit']); processRunner = TestProcessRunner(); mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') ..addCommand(PublishPluginCommand( mockPackagesDir, mockPackagesDir.fileSystem, processRunner: processRunner, print: (Object message) => printedMessages.add(message.toString()), stdinput: mockStdin, gitDir: await GitDir.fromExisting(mockPackagesDir.path))); }); tearDown(() { parentDir.deleteSync(recursive: true); printedMessages.clear(); }); group('Initial validation', () { test('requires a package flag', () async { await expectLater(() => commandRunner.run(['publish-plugin']), throwsA(const TypeMatcher())); expect( printedMessages.last, contains('Must specify a package to publish.')); }); test('requires an existing flag', () async { await expectLater( () => commandRunner.run([ 'publish-plugin', '--package', 'iamerror', '--no-push-tags' ]), throwsA(const TypeMatcher())); expect(printedMessages.last, contains('iamerror does not exist')); }); test('refuses to proceed with dirty files', () async { pluginDir.childFile('tmp').createSync(); await expectLater( () => commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--no-push-tags' ]), throwsA(const TypeMatcher())); expect( printedMessages, containsAllInOrder([ 'There are files in the package directory that haven\'t been saved in git. Refusing to publish these files:\n\n?? foo/tmp\n\nIf the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.', 'Failed, see above for details.', ])); }); test('fails immediately if the remote doesn\'t exist', () async { await expectLater( () => commandRunner .run(['publish-plugin', '--package', testPluginName]), throwsA(const TypeMatcher())); expect(processRunner.results.last.stderr, contains('No such remote')); }); test("doesn't validate the remote if it's not pushing tags", () async { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; await commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--no-push-tags', '--no-tag-release' ]); expect(printedMessages.last, 'Done!'); }); test('can publish non-flutter package', () async { createFakePubspec(pluginDir, includeVersion: true, isFlutter: false); io.Process.runSync('git', ['init'], workingDirectory: mockPackagesDir.path); gitDir = await GitDir.fromExisting(mockPackagesDir.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Initial commit']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; await commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--no-push-tags', '--no-tag-release' ]); expect(printedMessages.last, 'Done!'); }); }); group('Publishes package', () { test('while showing all output from pub publish to the user', () async { final Future publishCommand = commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--no-push-tags', '--no-tag-release' ]); processRunner.mockPublishStdout = 'Foo'; processRunner.mockPublishStderr = 'Bar'; processRunner.mockPublishCompleteCode = 0; await publishCommand; expect(printedMessages, contains('Foo')); expect(printedMessages, contains('Bar')); }); test('forwards input from the user to `pub publish`', () async { final Future publishCommand = commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--no-push-tags', '--no-tag-release' ]); mockStdin.mockUserInputs.add(utf8.encode('user input')); processRunner.mockPublishCompleteCode = 0; await publishCommand; expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); }); test('forwards --pub-publish-flags to pub publish', () async { processRunner.mockPublishCompleteCode = 0; await commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--no-push-tags', '--no-tag-release', '--pub-publish-flags', '--dry-run,--server=foo' ]); expect(processRunner.mockPublishArgs.length, 4); expect(processRunner.mockPublishArgs[0], 'pub'); expect(processRunner.mockPublishArgs[1], 'publish'); expect(processRunner.mockPublishArgs[2], '--dry-run'); expect(processRunner.mockPublishArgs[3], '--server=foo'); }); test('throws if pub publish fails', () async { processRunner.mockPublishCompleteCode = 128; await expectLater( () => commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--no-push-tags', '--no-tag-release', ]), throwsA(const TypeMatcher())); expect(printedMessages, contains('Publish foo failed.')); }); test('publish, dry run', () async { // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; await commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--dry-run', '--no-push-tags', '--no-tag-release', ]); expect(processRunner.pushTagsArgs, isEmpty); expect( printedMessages, containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', 'Done!' ])); }); }); group('Tags release', () { test('with the version and name from the pubspec.yaml', () async { processRunner.mockPublishCompleteCode = 0; await commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--no-push-tags', ]); final String tag = (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) .stdout as String; expect(tag, isNotEmpty); }); test('only if publishing succeeded', () async { processRunner.mockPublishCompleteCode = 128; await expectLater( () => commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--no-push-tags', ]), throwsA(const TypeMatcher())); expect(printedMessages, contains('Publish foo failed.')); final String tag = (await gitDir.runCommand( ['show-ref', 'fake_package-v0.0.1'], throwOnError: false)) .stdout as String; expect(tag, isEmpty); }); }); group('Pushes tags', () { setUp(() async { await gitDir.runCommand( ['remote', 'add', 'upstream', 'http://localhost:8000']); }); test('requires user confirmation', () async { processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'help'; await expectLater( () => commandRunner.run([ 'publish-plugin', '--package', testPluginName, ]), throwsA(const TypeMatcher())); expect(printedMessages, contains('Tag push canceled.')); }); test('to upstream by default', () async { await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; await commandRunner.run([ 'publish-plugin', '--package', testPluginName, ]); expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); expect(printedMessages.last, 'Done!'); }); test('to upstream by default, dry run', () async { await gitDir.runCommand(['tag', 'garbage']); // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; mockStdin.readLineOutput = 'y'; await commandRunner.run( ['publish-plugin', '--package', testPluginName, '--dry-run']); expect(processRunner.pushTagsArgs, isEmpty); expect( printedMessages, containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', 'Tagging release fake_package-v0.0.1...', 'Pushing tag to upstream...', 'Done!' ])); }); test('to different remotes based on a flag', () async { await gitDir.runCommand( ['remote', 'add', 'origin', 'http://localhost:8001']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; await commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--remote', 'origin', ]); expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'origin'); expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); expect(printedMessages.last, 'Done!'); }); test('only if tagging and pushing to remotes are both enabled', () async { processRunner.mockPublishCompleteCode = 0; await commandRunner.run([ 'publish-plugin', '--package', testPluginName, '--no-tag-release', ]); expect(processRunner.pushTagsArgs.isEmpty, isTrue); expect(printedMessages.last, 'Done!'); }); }); group('Auto release (all-changed flag)', () { setUp(() async { io.Process.runSync('git', ['init'], workingDirectory: mockPackagesDir.path); gitDir = await GitDir.fromExisting(mockPackagesDir.path); await gitDir.runCommand( ['remote', 'add', 'upstream', 'http://localhost:8000']); }); test('can release newly created plugins', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', withSingleExample: true, packagesDirectory: mockPackagesDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', packagesDirectory: mockPackagesDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, name: 'plugin2', includeVersion: true, isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; await commandRunner .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( printedMessages, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Packages released: plugin1, plugin2', 'Done!' ])); expect(processRunner.pushTagsArgs, isNotEmpty); expect(processRunner.pushTagsArgs[0], 'push'); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); expect(processRunner.pushTagsArgs[3], 'push'); expect(processRunner.pushTagsArgs[4], 'upstream'); expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); }); test('can release newly created plugins, while there are existing plugins', () async { // Prepare an exiting plugin and tag it final Directory pluginDir0 = createFakePlugin('plugin0', withSingleExample: true, packagesDirectory: mockPackagesDir); createFakePubspec(pluginDir0, name: 'plugin0', includeVersion: true, isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; await commandRunner .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); processRunner.pushTagsArgs.clear(); // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', withSingleExample: true, packagesDirectory: mockPackagesDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', packagesDirectory: mockPackagesDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, name: 'plugin2', includeVersion: true, isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. await commandRunner .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( printedMessages, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Packages released: plugin1, plugin2', 'Done!' ])); expect(processRunner.pushTagsArgs, isNotEmpty); expect(processRunner.pushTagsArgs[0], 'push'); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); expect(processRunner.pushTagsArgs[3], 'push'); expect(processRunner.pushTagsArgs[4], 'upstream'); expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); }); test('can release newly created plugins, dry run', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', withSingleExample: true, packagesDirectory: mockPackagesDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', packagesDirectory: mockPackagesDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, name: 'plugin2', includeVersion: true, isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; mockStdin.readLineOutput = 'y'; await commandRunner.run([ 'publish-plugin', '--all-changed', '--base-sha=HEAD~', '--dry-run' ]); expect( printedMessages, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', '=============== DRY RUN ===============', 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Tagging release plugin1-v0.0.1...', 'Pushing tag to upstream...', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Tagging release plugin2-v0.0.1...', 'Pushing tag to upstream...', 'Packages released: plugin1, plugin2', 'Done!' ])); expect(processRunner.pushTagsArgs, isEmpty); }); test('version change triggers releases.', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', withSingleExample: true, packagesDirectory: mockPackagesDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', packagesDirectory: mockPackagesDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, name: 'plugin2', includeVersion: true, isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; await commandRunner .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( printedMessages, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Packages released: plugin1, plugin2', 'Done!' ])); expect(processRunner.pushTagsArgs, isNotEmpty); expect(processRunner.pushTagsArgs[0], 'push'); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); expect(processRunner.pushTagsArgs[3], 'push'); expect(processRunner.pushTagsArgs[4], 'upstream'); expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); processRunner.pushTagsArgs.clear(); printedMessages.clear(); final List plugin1Pubspec = pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); plugin1Pubspec[plugin1Pubspec.indexWhere( (String element) => element.contains('version:'))] = 'version: 0.0.2'; pluginDir1 .childFile('pubspec.yaml') .writeAsStringSync(plugin1Pubspec.join('\n')); final List plugin2Pubspec = pluginDir2.childFile('pubspec.yaml').readAsLinesSync(); plugin2Pubspec[plugin2Pubspec.indexWhere( (String element) => element.contains('version:'))] = 'version: 0.0.2'; pluginDir2 .childFile('pubspec.yaml') .writeAsStringSync(plugin2Pubspec.join('\n')); await gitDir.runCommand(['add', '-A']); await gitDir .runCommand(['commit', '-m', 'Update versions to 0.0.2']); await commandRunner .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( printedMessages, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Packages released: plugin1, plugin2', 'Done!' ])); expect(processRunner.pushTagsArgs, isNotEmpty); expect(processRunner.pushTagsArgs[0], 'push'); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); expect(processRunner.pushTagsArgs[3], 'push'); expect(processRunner.pushTagsArgs[4], 'upstream'); expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2'); }); test( 'delete package will not trigger publish but exit the command successfully.', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', withSingleExample: true, packagesDirectory: mockPackagesDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', packagesDirectory: mockPackagesDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, name: 'plugin2', includeVersion: true, isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; await commandRunner .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( printedMessages, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Packages released: plugin1, plugin2', 'Done!' ])); expect(processRunner.pushTagsArgs, isNotEmpty); expect(processRunner.pushTagsArgs[0], 'push'); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); expect(processRunner.pushTagsArgs[3], 'push'); expect(processRunner.pushTagsArgs[4], 'upstream'); expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); processRunner.pushTagsArgs.clear(); printedMessages.clear(); final List plugin1Pubspec = pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); plugin1Pubspec[plugin1Pubspec.indexWhere( (String element) => element.contains('version:'))] = 'version: 0.0.2'; pluginDir1 .childFile('pubspec.yaml') .writeAsStringSync(plugin1Pubspec.join('\n')); pluginDir2.deleteSync(recursive: true); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand([ 'commit', '-m', 'Update plugin1 versions to 0.0.2, delete plugin2' ]); await commandRunner .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( printedMessages, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'The file at The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n', 'Packages released: plugin1', 'Done!' ])); expect(processRunner.pushTagsArgs, isNotEmpty); expect(processRunner.pushTagsArgs.length, 3); expect(processRunner.pushTagsArgs[0], 'push'); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); }); test( 'versions revert do not trigger releases. Also prints out warning message.', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', withSingleExample: true, packagesDirectory: mockPackagesDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', packagesDirectory: mockPackagesDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, isFlutter: false, version: '0.0.2'); createFakePubspec(pluginDir2, name: 'plugin2', includeVersion: true, isFlutter: false, version: '0.0.2'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; await commandRunner .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( printedMessages, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Packages released: plugin1, plugin2', 'Done!' ])); expect(processRunner.pushTagsArgs, isNotEmpty); expect(processRunner.pushTagsArgs[0], 'push'); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); expect(processRunner.pushTagsArgs[3], 'push'); expect(processRunner.pushTagsArgs[4], 'upstream'); expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2'); processRunner.pushTagsArgs.clear(); printedMessages.clear(); final List plugin1Pubspec = pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); plugin1Pubspec[plugin1Pubspec.indexWhere( (String element) => element.contains('version:'))] = 'version: 0.0.1'; pluginDir1 .childFile('pubspec.yaml') .writeAsStringSync(plugin1Pubspec.join('\n')); final List plugin2Pubspec = pluginDir2.childFile('pubspec.yaml').readAsLinesSync(); plugin2Pubspec[plugin2Pubspec.indexWhere( (String element) => element.contains('version:'))] = 'version: 0.0.1'; pluginDir2 .childFile('pubspec.yaml') .writeAsStringSync(plugin2Pubspec.join('\n')); await gitDir.runCommand(['add', '-A']); await gitDir .runCommand(['commit', '-m', 'Update versions to 0.0.1']); await commandRunner .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( printedMessages, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'Getting existing tags...', 'The new version (0.0.1) is lower than the current version (0.0.2) for plugin1.\nThis git commit is a revert, no release is tagged.', 'The new version (0.0.1) is lower than the current version (0.0.2) for plugin2.\nThis git commit is a revert, no release is tagged.', 'Done!' ])); expect(processRunner.pushTagsArgs, isEmpty); }); test('No version change does not release any plugins', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', withSingleExample: true, packagesDirectory: mockPackagesDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', packagesDirectory: mockPackagesDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, name: 'plugin2', includeVersion: true, isFlutter: false, version: '0.0.1'); io.Process.runSync('git', ['init'], workingDirectory: mockPackagesDir.path); gitDir = await GitDir.fromExisting(mockPackagesDir.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); pluginDir1.childFile('plugin1.dart').createSync(); pluginDir2.childFile('plugin2.dart').createSync(); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add dart files']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; await commandRunner .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( printedMessages, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'No version updates in this commit.', 'Done!' ])); expect(processRunner.pushTagsArgs, isEmpty); }); }); } class TestProcessRunner extends ProcessRunner { final List results = []; // Most recent returned publish process. MockProcess mockPublishProcess; final List mockPublishArgs = []; final MockProcessResult mockPushTagsResult = MockProcessResult(); final List pushTagsArgs = []; String mockPublishStdout; String mockPublishStderr; int mockPublishCompleteCode; @override Future run( String executable, List args, { Directory workingDir, bool exitOnError = false, bool logOnError = false, Encoding stdoutEncoding = io.systemEncoding, Encoding stderrEncoding = io.systemEncoding, }) async { // Don't ever really push tags. if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { pushTagsArgs.addAll(args); return mockPushTagsResult; } final io.ProcessResult result = io.Process.runSync(executable, args, workingDirectory: workingDir?.path); results.add(result); if (result.exitCode != 0) { throw ToolExit(result.exitCode); } return result; } @override Future start(String executable, List args, {Directory workingDirectory}) async { /// Never actually publish anything. Start is always and only used for this /// since it returns something we can route stdin through. assert(executable == 'flutter' && args.isNotEmpty && args[0] == 'pub' && args[1] == 'publish'); mockPublishArgs.addAll(args); mockPublishProcess = MockProcess(); if (mockPublishStdout != null) { mockPublishProcess.stdoutController.add(utf8.encode(mockPublishStdout)); } if (mockPublishStderr != null) { mockPublishProcess.stderrController.add(utf8.encode(mockPublishStderr)); } if (mockPublishCompleteCode != null) { mockPublishProcess.exitCodeCompleter.complete(mockPublishCompleteCode); } return mockPublishProcess; } } class MockStdin extends Mock implements io.Stdin { List> mockUserInputs = >[]; StreamController> _controller; String readLineOutput; @override Stream transform(StreamTransformer, S> streamTransformer) { // In the test context, only one `PublishPluginCommand` object is created for a single test case. // However, sometimes, we need to run multiple commands in a single test case. // In such situation, this `MockStdin`'s StreamController might be listened to more than once, which is not allowed. // // Create a new controller every time so this Stdin could be listened to multiple times. _controller = StreamController>(); mockUserInputs.forEach(_addUserInputsToSteam); return _controller.stream.transform(streamTransformer); } @override StreamSubscription> listen(void onData(List event), {Function onError, void onDone(), bool cancelOnError}) { return _controller.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } @override String readLineSync( {Encoding encoding = io.systemEncoding, bool retainNewlines = false}) => readLineOutput; void _addUserInputsToSteam(List input) => _controller.add(input); } class MockProcessResult extends Mock implements io.ProcessResult { MockProcessResult({int exitCode = 0}) : _exitCode = exitCode; final int _exitCode; @override int get exitCode => _exitCode; }