| // 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/common/core.dart'; |
| import 'package:flutter_plugin_tools/src/common/process_runner.dart'; |
| import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; |
| import 'package:git/git.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:test/test.dart'; |
| |
| import 'mocks.dart'; |
| import 'util.dart'; |
| |
| void main() { |
| const String testPluginName = 'foo'; |
| late List<String> printedMessages; |
| |
| late Directory testRoot; |
| late Directory packagesDir; |
| late Directory pluginDir; |
| late GitDir gitDir; |
| late TestProcessRunner processRunner; |
| late CommandRunner<void> commandRunner; |
| late MockStdin mockStdin; |
| // 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. |
| const FileSystem fileSystem = LocalFileSystem(); |
| |
| void _createMockCredentialFile() { |
| final String credentialPath = PublishPluginCommand.getCredentialPath(); |
| fileSystem.file(credentialPath) |
| ..createSync(recursive: true) |
| ..writeAsStringSync('some credential'); |
| } |
| |
| setUp(() async { |
| testRoot = fileSystem.systemTempDirectory |
| .createTempSync('publish_plugin_command_test-'); |
| // The temp directory can have symbolic links, which won't match git output; |
| // use a fully resolved version to avoid potential path comparison issues. |
| testRoot = fileSystem.directory(testRoot.resolveSymbolicLinksSync()); |
| packagesDir = createPackagesDirectory(parentDir: testRoot); |
| pluginDir = |
| createFakePlugin(testPluginName, packagesDir, withSingleExample: false); |
| assert(pluginDir != null && pluginDir.existsSync()); |
| createFakePubspec(pluginDir, version: '0.0.1'); |
| io.Process.runSync('git', <String>['init'], |
| workingDirectory: testRoot.path); |
| gitDir = await GitDir.fromExisting(testRoot.path); |
| await gitDir.runCommand(<String>['add', '-A']); |
| await gitDir.runCommand(<String>['commit', '-m', 'Initial commit']); |
| processRunner = TestProcessRunner(); |
| mockStdin = MockStdin(); |
| printedMessages = <String>[]; |
| commandRunner = CommandRunner<void>('tester', '') |
| ..addCommand(PublishPluginCommand(packagesDir, |
| processRunner: processRunner, |
| print: (Object? message) => printedMessages.add(message.toString()), |
| stdinput: mockStdin, |
| gitDir: gitDir)); |
| }); |
| |
| tearDown(() { |
| testRoot.deleteSync(recursive: true); |
| }); |
| |
| 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', |
| '--no-push-tags' |
| ]), |
| 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, |
| '--no-push-tags' |
| ]), |
| throwsA(const TypeMatcher<ToolExit>())); |
| |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| 'There are files in the package directory that haven\'t been saved in git. Refusing to publish these files:\n\n?? packages/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(<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.mockPublishCompleteCode = 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, version: '0.0.1', isFlutter: false); |
| io.Process.runSync('git', <String>['init'], |
| workingDirectory: testRoot.path); |
| gitDir = await GitDir.fromExisting(testRoot.path); |
| await gitDir.runCommand(<String>['add', '-A']); |
| await gitDir.runCommand(<String>['commit', '-m', 'Initial commit']); |
| // Immediately return 0 when running `pub publish`. |
| processRunner.mockPublishCompleteCode = 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.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<void> publishCommand = commandRunner.run(<String>[ |
| '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(<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( |
| '--skip-confirmation flag automatically adds --force to --pub-publish-flags', |
| () async { |
| processRunner.mockPublishCompleteCode = 0; |
| _createMockCredentialFile(); |
| await commandRunner.run(<String>[ |
| 'publish-plugin', |
| '--package', |
| testPluginName, |
| '--no-push-tags', |
| '--no-tag-release', |
| '--skip-confirmation', |
| '--pub-publish-flags', |
| '--server=foo' |
| ]); |
| |
| expect(processRunner.mockPublishArgs.length, 4); |
| expect(processRunner.mockPublishArgs[0], 'pub'); |
| expect(processRunner.mockPublishArgs[1], 'publish'); |
| expect(processRunner.mockPublishArgs[2], '--server=foo'); |
| expect(processRunner.mockPublishArgs[3], '--force'); |
| }); |
| |
| test('throws if pub publish fails', () async { |
| processRunner.mockPublishCompleteCode = 128; |
| await expectLater( |
| () => commandRunner.run(<String>[ |
| 'publish-plugin', |
| '--package', |
| testPluginName, |
| '--no-push-tags', |
| '--no-tag-release', |
| ]), |
| throwsA(const TypeMatcher<ToolExit>())); |
| |
| 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(<String>[ |
| 'publish-plugin', |
| '--package', |
| testPluginName, |
| '--dry-run', |
| '--no-push-tags', |
| '--no-tag-release', |
| ]); |
| |
| expect(processRunner.pushTagsArgs, isEmpty); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| '=============== 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(<String>[ |
| 'publish-plugin', |
| '--package', |
| testPluginName, |
| '--no-push-tags', |
| ]); |
| |
| final String? tag = |
| (await gitDir.runCommand(<String>['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(<String>[ |
| 'publish-plugin', |
| '--package', |
| testPluginName, |
| '--no-push-tags', |
| ]), |
| throwsA(const TypeMatcher<ToolExit>())); |
| |
| expect(printedMessages, contains('Publish foo failed.')); |
| final String? tag = (await gitDir.runCommand( |
| <String>['show-ref', 'fake_package-v0.0.1'], |
| throwOnError: false)) |
| .stdout as String?; |
| expect(tag, isEmpty); |
| }); |
| }); |
| |
| group('Pushes tags', () { |
| setUp(() async { |
| await gitDir.runCommand( |
| <String>['remote', 'add', 'upstream', 'http://localhost:8000']); |
| }); |
| |
| test('requires user confirmation', () async { |
| processRunner.mockPublishCompleteCode = 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.mockPublishCompleteCode = 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('does not ask for user input if the --skip-confirmation flag is on', |
| () async { |
| await gitDir.runCommand(<String>['tag', 'garbage']); |
| processRunner.mockPublishCompleteCode = 0; |
| _createMockCredentialFile(); |
| await commandRunner.run(<String>[ |
| 'publish-plugin', |
| '--skip-confirmation', |
| '--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(<String>['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( |
| <String>['publish-plugin', '--package', testPluginName, '--dry-run']); |
| |
| expect(processRunner.pushTagsArgs, isEmpty); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| '=============== 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( |
| <String>['remote', 'add', 'origin', 'http://localhost:8001']); |
| processRunner.mockPublishCompleteCode = 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.mockPublishCompleteCode = 0; |
| await commandRunner.run(<String>[ |
| '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', <String>['init'], |
| workingDirectory: testRoot.path); |
| gitDir = await GitDir.fromExisting(testRoot.path); |
| await gitDir.runCommand( |
| <String>['remote', 'add', 'upstream', 'http://localhost:8000']); |
| }); |
| |
| test('can release newly created plugins', () async { |
| // Non-federated |
| final Directory pluginDir1 = |
| createFakePlugin('plugin1', packagesDir, withSingleExample: true); |
| // federated |
| final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, |
| withSingleExample: true, parentDirectoryName: 'plugin2'); |
| createFakePubspec(pluginDir1, |
| name: 'plugin1', isFlutter: false, version: '0.0.1'); |
| createFakePubspec(pluginDir2, |
| name: 'plugin2', isFlutter: false, version: '0.0.1'); |
| await gitDir.runCommand(<String>['add', '-A']); |
| await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']); |
| // Immediately return 0 when running `pub publish`. |
| processRunner.mockPublishCompleteCode = 0; |
| mockStdin.readLineOutput = 'y'; |
| await commandRunner |
| .run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| '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', packagesDir, withSingleExample: true); |
| createFakePubspec(pluginDir0, |
| name: 'plugin0', isFlutter: false, version: '0.0.1'); |
| await gitDir.runCommand(<String>['add', '-A']); |
| await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']); |
| // Immediately return 0 when running `pub publish`. |
| processRunner.mockPublishCompleteCode = 0; |
| mockStdin.readLineOutput = 'y'; |
| await commandRunner |
| .run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']); |
| processRunner.pushTagsArgs.clear(); |
| |
| // Non-federated |
| final Directory pluginDir1 = |
| createFakePlugin('plugin1', packagesDir, withSingleExample: true); |
| // federated |
| final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, |
| withSingleExample: true, parentDirectoryName: 'plugin2'); |
| createFakePubspec(pluginDir1, |
| name: 'plugin1', isFlutter: false, version: '0.0.1'); |
| createFakePubspec(pluginDir2, |
| name: 'plugin2', isFlutter: false, version: '0.0.1'); |
| await gitDir.runCommand(<String>['add', '-A']); |
| await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']); |
| // Immediately return 0 when running `pub publish`. |
| await commandRunner |
| .run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| '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', packagesDir, withSingleExample: true); |
| // federated |
| final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, |
| withSingleExample: true, parentDirectoryName: 'plugin2'); |
| createFakePubspec(pluginDir1, |
| name: 'plugin1', isFlutter: false, version: '0.0.1'); |
| createFakePubspec(pluginDir2, |
| name: 'plugin2', isFlutter: false, version: '0.0.1'); |
| await gitDir.runCommand(<String>['add', '-A']); |
| await gitDir.runCommand(<String>['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(<String>[ |
| 'publish-plugin', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| '--dry-run' |
| ]); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| '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', packagesDir, withSingleExample: true); |
| // federated |
| final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, |
| withSingleExample: true, parentDirectoryName: 'plugin2'); |
| createFakePubspec(pluginDir1, |
| name: 'plugin1', isFlutter: false, version: '0.0.1'); |
| createFakePubspec(pluginDir2, |
| name: 'plugin2', isFlutter: false, version: '0.0.1'); |
| await gitDir.runCommand(<String>['add', '-A']); |
| await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']); |
| // Immediately return 0 when running `pub publish`. |
| processRunner.mockPublishCompleteCode = 0; |
| mockStdin.readLineOutput = 'y'; |
| await commandRunner |
| .run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| '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<String> 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<String> 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(<String>['add', '-A']); |
| await gitDir |
| .runCommand(<String>['commit', '-m', 'Update versions to 0.0.2']); |
| |
| await commandRunner |
| .run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| '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', packagesDir, withSingleExample: true); |
| // federated |
| final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, |
| withSingleExample: true, parentDirectoryName: 'plugin2'); |
| createFakePubspec(pluginDir1, |
| name: 'plugin1', isFlutter: false, version: '0.0.1'); |
| createFakePubspec(pluginDir2, |
| name: 'plugin2', isFlutter: false, version: '0.0.1'); |
| await gitDir.runCommand(<String>['add', '-A']); |
| await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']); |
| // Immediately return 0 when running `pub publish`. |
| processRunner.mockPublishCompleteCode = 0; |
| mockStdin.readLineOutput = 'y'; |
| await commandRunner |
| .run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| '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<String> 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(<String>['add', '-A']); |
| await gitDir.runCommand(<String>[ |
| 'commit', |
| '-m', |
| 'Update plugin1 versions to 0.0.2, delete plugin2' |
| ]); |
| |
| await commandRunner |
| .run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| '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', packagesDir, withSingleExample: true); |
| // federated |
| final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, |
| withSingleExample: true, parentDirectoryName: 'plugin2'); |
| createFakePubspec(pluginDir1, |
| name: 'plugin1', isFlutter: false, version: '0.0.2'); |
| createFakePubspec(pluginDir2, |
| name: 'plugin2', isFlutter: false, version: '0.0.2'); |
| await gitDir.runCommand(<String>['add', '-A']); |
| await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']); |
| // Immediately return 0 when running `pub publish`. |
| processRunner.mockPublishCompleteCode = 0; |
| mockStdin.readLineOutput = 'y'; |
| await commandRunner |
| .run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| '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<String> 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<String> 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(<String>['add', '-A']); |
| await gitDir |
| .runCommand(<String>['commit', '-m', 'Update versions to 0.0.1']); |
| |
| await commandRunner |
| .run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| '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', packagesDir, withSingleExample: true); |
| // federated |
| final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, |
| withSingleExample: true, parentDirectoryName: 'plugin2'); |
| createFakePubspec(pluginDir1, |
| name: 'plugin1', isFlutter: false, version: '0.0.1'); |
| createFakePubspec(pluginDir2, |
| name: 'plugin2', isFlutter: false, version: '0.0.1'); |
| |
| io.Process.runSync('git', <String>['init'], |
| workingDirectory: testRoot.path); |
| gitDir = await GitDir.fromExisting(testRoot.path); |
| await gitDir.runCommand(<String>['add', '-A']); |
| await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']); |
| |
| pluginDir1.childFile('plugin1.dart').createSync(); |
| pluginDir2.childFile('plugin2.dart').createSync(); |
| await gitDir.runCommand(<String>['add', '-A']); |
| await gitDir.runCommand(<String>['commit', '-m', 'Add dart files']); |
| |
| // Immediately return 0 when running `pub publish`. |
| processRunner.mockPublishCompleteCode = 0; |
| mockStdin.readLineOutput = 'y'; |
| await commandRunner |
| .run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']); |
| expect( |
| printedMessages, |
| containsAllInOrder(<String>[ |
| 'Checking local repo...', |
| 'Local repo is ready!', |
| 'No version updates in this commit.', |
| 'Done!' |
| ])); |
| expect(processRunner.pushTagsArgs, isEmpty); |
| }); |
| }); |
| } |
| |
| class TestProcessRunner extends ProcessRunner { |
| final List<io.ProcessResult> results = <io.ProcessResult>[]; |
| // Most recent returned publish process. |
| late MockProcess mockPublishProcess; |
| final List<String> mockPublishArgs = <String>[]; |
| final MockProcessResult mockPushTagsResult = MockProcessResult(); |
| final List<String> pushTagsArgs = <String>[]; |
| |
| String? mockPublishStdout; |
| String? mockPublishStderr; |
| int? mockPublishCompleteCode; |
| |
| @override |
| Future<io.ProcessResult> run( |
| String executable, |
| List<String> 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<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); |
| 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<List<int>> mockUserInputs = <List<int>>[]; |
| late StreamController<List<int>> _controller; |
| String? readLineOutput; |
| |
| @override |
| Stream<S> transform<S>(StreamTransformer<List<int>, 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<List<int>>(); |
| mockUserInputs.forEach(_addUserInputsToSteam); |
| 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; |
| |
| void _addUserInputsToSteam(List<int> 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; |
| } |