| // Copyright 2013 The Flutter Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io' as io; |
| |
| import 'package:args/command_runner.dart'; |
| import 'package:file/file.dart'; |
| import 'package:flutter_plugin_tools/src/common/core.dart'; |
| import 'package:flutter_plugin_tools/src/publish_command.dart'; |
| import 'package:git/git.dart'; |
| import 'package:http/http.dart' as http; |
| import 'package:http/testing.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:test/test.dart'; |
| |
| import 'mocks.dart'; |
| import 'util.dart'; |
| |
| void main() { |
| late MockPlatform platform; |
| late Directory packagesDir; |
| late TestProcessRunner processRunner; |
| late PublishCommand command; |
| late CommandRunner<void> commandRunner; |
| late MockStdin mockStdin; |
| // Map of package name to mock response. |
| late Map<String, Map<String, dynamic>> mockHttpResponses; |
| |
| void createMockCredentialFile() { |
| packagesDir.fileSystem.file(command.credentialsPath) |
| ..createSync(recursive: true) |
| ..writeAsStringSync('some credential'); |
| } |
| |
| setUp(() async { |
| platform = MockPlatform(isLinux: true); |
| processRunner = TestProcessRunner(); |
| final GitDir gitDir; |
| (:packagesDir, processRunner: _, gitProcessRunner: _, :gitDir) = configureBaseCommandMocks( |
| platform: platform, |
| customProcessRunner: processRunner, |
| customGitProcessRunner: processRunner, |
| ); |
| platform.environment['HOME'] = '/home'; |
| |
| mockHttpResponses = <String, Map<String, dynamic>>{}; |
| final mockClient = MockClient((http.Request request) async { |
| final String packageName = request.url.pathSegments.last.replaceAll('.json', ''); |
| final Map<String, dynamic>? response = mockHttpResponses[packageName]; |
| if (response != null) { |
| return http.Response(json.encode(response), 200); |
| } |
| // Default to simulating the plugin never having been published. |
| return http.Response('', 404); |
| }); |
| |
| mockStdin = MockStdin(); |
| command = PublishCommand( |
| packagesDir, |
| platform: platform, |
| processRunner: processRunner, |
| stdinput: mockStdin, |
| gitDir: gitDir, |
| httpClient: mockClient, |
| ); |
| commandRunner = CommandRunner<void>('tester', '')..addCommand(command); |
| }); |
| |
| group('Initial validation', () { |
| test('refuses to proceed with dirty files', () async { |
| final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| processRunner.mockProcessesForExecutable['git-status'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: '?? ${plugin.directory.childFile('tmp').path}\n')), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| commandRunner, |
| <String>['publish', '--packages=foo'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| "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\n' |
| 'If the directory should be clean, you can run `git clean -xdf && ' |
| 'git reset --hard HEAD` to wipe all local changes.', |
| ), |
| contains( |
| 'foo:\n' |
| ' uncommitted changes', |
| ), |
| ]), |
| ); |
| }); |
| |
| test("fails immediately if the remote doesn't exist", () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| processRunner.mockProcessesForExecutable['git-remote'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(exitCode: 1)), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| commandRunner, |
| <String>['publish', '--packages=foo'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Unable to find URL for remote upstream; cannot push tags'), |
| ]), |
| ); |
| }); |
| }); |
| |
| group('pre-publish script', () { |
| test('runs if present', () async { |
| final RepositoryPackage package = createFakePackage('foo', packagesDir, examples: <String>[]); |
| package.prePublishScript.createSync(recursive: true); |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running pre-publish hook tool/pre_publish.dart...'), |
| ]), |
| ); |
| expect( |
| processRunner.recordedCalls, |
| containsAllInOrder(<ProcessCall>[ |
| ProcessCall('dart', const <String>['pub', 'get'], package.directory.path), |
| ProcessCall('dart', const <String>[ |
| 'run', |
| 'tool/pre_publish.dart', |
| ], package.directory.path), |
| ]), |
| ); |
| }); |
| |
| test('causes command failure if it fails', () async { |
| final RepositoryPackage package = createFakePackage( |
| 'foo', |
| packagesDir, |
| isFlutter: true, |
| examples: <String>[], |
| ); |
| package.prePublishScript.createSync(recursive: true); |
| |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['run']), // run tool/pre_publish.dart |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| commandRunner, |
| <String>['publish', '--packages=foo'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect(output, containsAllInOrder(<Matcher>[contains('Pre-publish script failed.')])); |
| expect( |
| processRunner.recordedCalls, |
| containsAllInOrder(<ProcessCall>[ |
| ProcessCall(getFlutterCommand(platform), const <String>[ |
| 'pub', |
| 'get', |
| ], package.directory.path), |
| ProcessCall('dart', const <String>[ |
| 'run', |
| 'tool/pre_publish.dart', |
| ], package.directory.path), |
| ]), |
| ); |
| }); |
| }); |
| |
| group('Publishes package', () { |
| test('while showing all output from pub publish to the user', () async { |
| createFakePlugin('plugin1', packagesDir, examples: <String>[]); |
| createFakePlugin('plugin2', packagesDir, examples: <String>[]); |
| |
| processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess(stdout: 'Foo', stderr: 'Bar', stdoutEncoding: utf8, stderrEncoding: utf8), |
| <String>['pub', 'publish'], |
| ), // publish for plugin1 |
| FakeProcessInfo( |
| MockProcess(stdout: 'Baz', stdoutEncoding: utf8, stderrEncoding: utf8), |
| <String>['pub', 'publish'], |
| ), // publish for plugin2 |
| ]; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=plugin1,plugin2', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running `pub publish ` in /packages/plugin1...'), |
| contains('Foo'), |
| contains('Bar'), |
| contains('Package published!'), |
| contains('Running `pub publish ` in /packages/plugin2...'), |
| contains('Baz'), |
| contains('Package published!'), |
| ]), |
| ); |
| }); |
| |
| test('forwards input from the user to `pub publish`', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| mockStdin.mockUserInputs.add(utf8.encode('user input')); |
| |
| await runCapturingPrint(commandRunner, <String>['publish', '--packages=foo']); |
| |
| expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); |
| }); |
| |
| test('uses dart rather than flutter for non-Flutter packages', () async { |
| final RepositoryPackage package = createFakePackage('foo', packagesDir, examples: <String>[]); |
| |
| await runCapturingPrint(commandRunner, <String>['publish', '--packages=foo']); |
| |
| expect( |
| processRunner.recordedCalls, |
| contains(ProcessCall('dart', const <String>['pub', 'publish'], package.directory.path)), |
| ); |
| }); |
| |
| test('forwards --pub-publish-flags to pub publish', () async { |
| final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| '--pub-publish-flags', |
| '--dry-run,--server=bar', |
| ]); |
| |
| expect( |
| processRunner.recordedCalls, |
| contains( |
| ProcessCall('flutter', const <String>[ |
| 'pub', |
| 'publish', |
| '--dry-run', |
| '--server=bar', |
| ], plugin.path), |
| ), |
| ); |
| }); |
| |
| test('--skip-confirmation flag automatically adds --force to --pub-publish-flags', () async { |
| createMockCredentialFile(); |
| final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| '--skip-confirmation', |
| '--pub-publish-flags', |
| '--server=bar', |
| ]); |
| |
| expect( |
| processRunner.recordedCalls, |
| contains( |
| ProcessCall('flutter', const <String>[ |
| 'pub', |
| 'publish', |
| '--server=bar', |
| '--force', |
| ], plugin.path), |
| ), |
| ); |
| }); |
| |
| test('--force is only added once, regardless of plugin count', () async { |
| createMockCredentialFile(); |
| final RepositoryPackage plugin1 = createFakePlugin( |
| 'plugin_a', |
| packagesDir, |
| examples: <String>[], |
| ); |
| final RepositoryPackage plugin2 = createFakePlugin( |
| 'plugin_b', |
| packagesDir, |
| examples: <String>[], |
| ); |
| |
| await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=plugin_a,plugin_b', |
| '--skip-confirmation', |
| '--pub-publish-flags', |
| '--server=bar', |
| ]); |
| |
| expect( |
| processRunner.recordedCalls, |
| containsAllInOrder(<ProcessCall>[ |
| ProcessCall('flutter', const <String>[ |
| 'pub', |
| 'publish', |
| '--server=bar', |
| '--force', |
| ], plugin1.path), |
| ProcessCall('flutter', const <String>[ |
| 'pub', |
| 'publish', |
| '--server=bar', |
| '--force', |
| ], plugin2.path), |
| ]), |
| ); |
| }); |
| |
| test('creates credential file from envirnoment variable if necessary', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| const credentials = 'some credential'; |
| platform.environment['PUB_CREDENTIALS'] = credentials; |
| |
| await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| '--skip-confirmation', |
| '--pub-publish-flags', |
| '--server=bar', |
| ]); |
| |
| final File credentialFile = packagesDir.fileSystem.file(command.credentialsPath); |
| expect(credentialFile.existsSync(), true); |
| expect(credentialFile.readAsStringSync(), credentials); |
| }); |
| |
| test('throws if pub publish fails', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(exitCode: 128), <String>['pub', 'publish']), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| commandRunner, |
| <String>['publish', '--packages=foo'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect(output, containsAllInOrder(<Matcher>[contains('Publishing foo failed.')])); |
| }); |
| |
| test('publish, dry run', () async { |
| final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| '--dry-run', |
| ]); |
| |
| expect( |
| processRunner.recordedCalls.map((ProcessCall call) => call.executable), |
| isNot(contains('git-push')), |
| ); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('=============== DRY RUN ==============='), |
| contains('Running for foo'), |
| contains('Running `pub publish ` in ${plugin.path}...'), |
| contains('Tagging release foo-v0.0.1...'), |
| contains('Pushing tag to upstream...'), |
| contains('Published foo successfully!'), |
| ]), |
| ); |
| }); |
| |
| test('can publish non-flutter package', () async { |
| const packageName = 'a_package'; |
| createFakePackage(packageName, packagesDir); |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=$packageName', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running `pub publish ` in /packages/a_package...'), |
| contains('Package published!'), |
| ]), |
| ); |
| }); |
| |
| test('skips publish with --tag-for-auto-publish', () async { |
| const packageName = 'a_package'; |
| createFakePackage(packageName, packagesDir); |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=$packageName', |
| '--tag-for-auto-publish', |
| ]); |
| |
| // There should be no variant of any command containing "publish". |
| expect( |
| processRunner.recordedCalls.map((ProcessCall call) => call.toString()), |
| isNot(contains(contains('publish'))), |
| ); |
| // The output should indicate that it was tagged, not published. |
| expect(output, containsAllInOrder(<Matcher>[contains('Tagged a_package successfully!')])); |
| }); |
| }); |
| |
| group('Tags release', () { |
| test('with the version and name from the pubspec.yaml', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| await runCapturingPrint(commandRunner, <String>['publish', '--packages=foo']); |
| |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-tag', <String>['foo-v0.0.1'], null)), |
| ); |
| }); |
| |
| test('only if publishing succeeded', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(exitCode: 128), <String>['pub', 'publish']), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| commandRunner, |
| <String>['publish', '--packages=foo'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect(output, containsAllInOrder(<Matcher>[contains('Publishing foo failed.')])); |
| expect( |
| processRunner.recordedCalls, |
| isNot(contains(const ProcessCall('git-tag', <String>['foo-v0.0.1'], null))), |
| ); |
| }); |
| |
| test('when passed --tag-for-auto-publish', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| '--tag-for-auto-publish', |
| ]); |
| |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-tag', <String>['foo-v0.0.1'], null)), |
| ); |
| }); |
| }); |
| |
| group('Pushes tags', () { |
| test('to upstream by default', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| mockStdin.readLineOutput = 'y'; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| ]); |
| |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-push', <String>['upstream', 'foo-v0.0.1'], null)), |
| ); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Pushing tag to upstream...'), |
| contains('Published foo successfully!'), |
| ]), |
| ); |
| }); |
| |
| test('does not ask for user input if the --skip-confirmation flag is on', () async { |
| createMockCredentialFile(); |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--skip-confirmation', |
| '--packages=foo', |
| ]); |
| |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-push', <String>['upstream', 'foo-v0.0.1'], null)), |
| ); |
| expect(output, containsAllInOrder(<Matcher>[contains('Published foo successfully!')])); |
| }); |
| |
| test('when passed --tag-for-auto-publish', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| '--skip-confirmation', |
| '--tag-for-auto-publish', |
| ]); |
| |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-push', <String>['upstream', 'foo-v0.0.1'], null)), |
| ); |
| }); |
| |
| test('to upstream by default, dry run', () async { |
| final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| mockStdin.readLineOutput = 'y'; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| '--dry-run', |
| ]); |
| |
| expect( |
| processRunner.recordedCalls.map((ProcessCall call) => call.executable), |
| isNot(contains('git-push')), |
| ); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('=============== DRY RUN ==============='), |
| contains('Running `pub publish ` in ${plugin.path}...'), |
| contains('Tagging release foo-v0.0.1...'), |
| contains('Pushing tag to upstream...'), |
| contains('Published foo successfully!'), |
| ]), |
| ); |
| }); |
| |
| test('to different remotes based on a flag', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| mockStdin.readLineOutput = 'y'; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| '--remote', |
| 'origin', |
| ]); |
| |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-push', <String>['origin', 'foo-v0.0.1'], null)), |
| ); |
| expect(output, containsAllInOrder(<Matcher>[contains('Published foo successfully!')])); |
| }); |
| }); |
| |
| group('--already-tagged', () { |
| test('passes when HEAD has the expected tag', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| processRunner.mockProcessesForExecutable['git-tag'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess()), // Skip the initializeRun call. |
| FakeProcessInfo(MockProcess(stdout: 'foo-v0.0.1\n'), <String>['--points-at', 'HEAD']), |
| ]; |
| |
| await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| '--already-tagged', |
| ]); |
| }); |
| |
| test('fails if HEAD does not have the expected tag', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| commandRunner, |
| <String>['publish', '--packages=foo', '--already-tagged'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('The current checkout is not already tagged "foo-v0.0.1"'), |
| contains('missing tag'), |
| ]), |
| ); |
| }); |
| |
| test('does not create or push tags', () async { |
| createFakePlugin('foo', packagesDir, examples: <String>[]); |
| |
| processRunner.mockProcessesForExecutable['git-tag'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess()), // Skip the initializeRun call. |
| FakeProcessInfo(MockProcess(stdout: 'foo-v0.0.1\n'), <String>['--points-at', 'HEAD']), |
| ]; |
| |
| await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--packages=foo', |
| '--already-tagged', |
| ]); |
| |
| expect( |
| processRunner.recordedCalls, |
| isNot(contains(const ProcessCall('git-tag', <String>['foo-v0.0.1'], null))), |
| ); |
| expect( |
| processRunner.recordedCalls.map((ProcessCall call) => call.executable), |
| isNot(contains('git-push')), |
| ); |
| }); |
| }); |
| |
| group('Auto release (all-changed flag)', () { |
| test('can release newly created plugins', () async { |
| mockHttpResponses['plugin1'] = <String, dynamic>{'name': 'plugin1', 'versions': <String>[]}; |
| |
| mockHttpResponses['plugin2'] = <String, dynamic>{'name': 'plugin2', 'versions': <String>[]}; |
| |
| // Non-federated |
| final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); |
| // federated |
| final RepositoryPackage plugin2 = createFakePlugin( |
| 'plugin2', |
| packagesDir.childDirectory('plugin2'), |
| ); |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: |
| '${plugin1.pubspecFile.path}\n' |
| '${plugin2.pubspecFile.path}\n', |
| ), |
| ), |
| ]; |
| mockStdin.readLineOutput = 'y'; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Publishing all packages that have changed relative to "HEAD~"'), |
| contains('Running `pub publish ` in ${plugin1.path}...'), |
| contains('Running `pub publish ` in ${plugin2.path}...'), |
| contains('plugin1 - published'), |
| contains('plugin2/plugin2 - published'), |
| ]), |
| ); |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-push', <String>['upstream', 'plugin1-v0.0.1'], null)), |
| ); |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-push', <String>['upstream', 'plugin2-v0.0.1'], null)), |
| ); |
| }); |
| |
| test('can release newly created plugins, while there are existing plugins', () async { |
| mockHttpResponses['plugin0'] = <String, dynamic>{ |
| 'name': 'plugin0', |
| 'versions': <String>['0.0.1'], |
| }; |
| |
| mockHttpResponses['plugin1'] = <String, dynamic>{'name': 'plugin1', 'versions': <String>[]}; |
| |
| mockHttpResponses['plugin2'] = <String, dynamic>{'name': 'plugin2', 'versions': <String>[]}; |
| |
| // The existing plugin. |
| createFakePlugin('plugin0', packagesDir); |
| // Non-federated |
| final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); |
| // federated |
| final RepositoryPackage plugin2 = createFakePlugin( |
| 'plugin2', |
| packagesDir.childDirectory('plugin2'), |
| ); |
| |
| // Git results for plugin0 having been released already, and plugin1 and |
| // plugin2 being new. |
| processRunner.mockProcessesForExecutable['git-tag'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'plugin0-v0.0.1\n')), |
| ]; |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: |
| '${plugin1.pubspecFile.path}\n' |
| '${plugin2.pubspecFile.path}\n', |
| ), |
| ), |
| ]; |
| |
| mockStdin.readLineOutput = 'y'; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<String>[ |
| 'Running `pub publish ` in ${plugin1.path}...\n', |
| 'Running `pub publish ` in ${plugin2.path}...\n', |
| ]), |
| ); |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-push', <String>['upstream', 'plugin1-v0.0.1'], null)), |
| ); |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-push', <String>['upstream', 'plugin2-v0.0.1'], null)), |
| ); |
| }); |
| |
| test('can release newly created plugins, dry run', () async { |
| mockHttpResponses['plugin1'] = <String, dynamic>{'name': 'plugin1', 'versions': <String>[]}; |
| |
| mockHttpResponses['plugin2'] = <String, dynamic>{'name': 'plugin2', 'versions': <String>[]}; |
| |
| // Non-federated |
| final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); |
| // federated |
| final RepositoryPackage plugin2 = createFakePlugin( |
| 'plugin2', |
| packagesDir.childDirectory('plugin2'), |
| ); |
| |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: |
| '${plugin1.pubspecFile.path}\n' |
| '${plugin2.pubspecFile.path}\n', |
| ), |
| ), |
| ]; |
| mockStdin.readLineOutput = 'y'; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| '--dry-run', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('=============== DRY RUN ==============='), |
| contains('Running `pub publish ` in ${plugin1.path}...'), |
| contains('Tagging release plugin1-v0.0.1...'), |
| contains('Pushing tag to upstream...'), |
| contains('Published plugin1 successfully!'), |
| contains('Running `pub publish ` in ${plugin2.path}...'), |
| contains('Tagging release plugin2-v0.0.1...'), |
| contains('Pushing tag to upstream...'), |
| contains('Published plugin2 successfully!'), |
| ]), |
| ); |
| expect( |
| processRunner.recordedCalls.map((ProcessCall call) => call.executable), |
| isNot(contains('git-push')), |
| ); |
| }); |
| |
| test('version change triggers releases.', () async { |
| mockHttpResponses['plugin1'] = <String, dynamic>{ |
| 'name': 'plugin1', |
| 'versions': <String>['0.0.1'], |
| }; |
| |
| mockHttpResponses['plugin2'] = <String, dynamic>{ |
| 'name': 'plugin2', |
| 'versions': <String>['0.0.1'], |
| }; |
| |
| // Non-federated |
| final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); |
| // federated |
| final RepositoryPackage plugin2 = createFakePlugin( |
| 'plugin2', |
| packagesDir.childDirectory('plugin2'), |
| version: '0.0.2', |
| ); |
| |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: |
| '${plugin1.pubspecFile.path}\n' |
| '${plugin2.pubspecFile.path}\n', |
| ), |
| ), |
| ]; |
| |
| mockStdin.readLineOutput = 'y'; |
| |
| final List<String> output2 = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| ]); |
| expect( |
| output2, |
| containsAllInOrder(<Matcher>[ |
| contains('Running `pub publish ` in ${plugin1.path}...'), |
| contains('Published plugin1 successfully!'), |
| contains('Running `pub publish ` in ${plugin2.path}...'), |
| contains('Published plugin2 successfully!'), |
| ]), |
| ); |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-push', <String>['upstream', 'plugin1-v0.0.2'], null)), |
| ); |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-push', <String>['upstream', 'plugin2-v0.0.2'], null)), |
| ); |
| }); |
| |
| test('delete package will not trigger publish but exit the command successfully!', () async { |
| mockHttpResponses['plugin1'] = <String, dynamic>{ |
| 'name': 'plugin1', |
| 'versions': <String>['0.0.1'], |
| }; |
| |
| mockHttpResponses['plugin2'] = <String, dynamic>{ |
| 'name': 'plugin2', |
| 'versions': <String>['0.0.1'], |
| }; |
| |
| // Non-federated |
| final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); |
| // federated |
| final RepositoryPackage plugin2 = createFakePlugin( |
| 'plugin2', |
| packagesDir.childDirectory('plugin2'), |
| ); |
| plugin2.directory.deleteSync(recursive: true); |
| |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: |
| '${plugin1.pubspecFile.path}\n' |
| '${plugin2.pubspecFile.path}\n', |
| ), |
| ), |
| ]; |
| |
| mockStdin.readLineOutput = 'y'; |
| |
| final List<String> output2 = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| ]); |
| expect( |
| output2, |
| containsAllInOrder(<Matcher>[ |
| contains('Running `pub publish ` in ${plugin1.path}...'), |
| contains('Published plugin1 successfully!'), |
| contains( |
| 'The pubspec file for plugin2/plugin2 does not exist, so no publishing will happen.\nSafe to ignore if the package is deleted in this commit.\n', |
| ), |
| contains('SKIPPING: package deleted'), |
| contains('skipped (with warning)'), |
| ]), |
| ); |
| expect( |
| processRunner.recordedCalls, |
| contains(const ProcessCall('git-push', <String>['upstream', 'plugin1-v0.0.2'], null)), |
| ); |
| }); |
| |
| test('Existing versions do not trigger release, also prints out message.', () async { |
| mockHttpResponses['plugin1'] = <String, dynamic>{ |
| 'name': 'plugin1', |
| 'versions': <String>['0.0.2'], |
| }; |
| |
| mockHttpResponses['plugin2'] = <String, dynamic>{ |
| 'name': 'plugin2', |
| 'versions': <String>['0.0.2'], |
| }; |
| |
| // Non-federated |
| final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); |
| // federated |
| final RepositoryPackage plugin2 = createFakePlugin( |
| 'plugin2', |
| packagesDir.childDirectory('plugin2'), |
| version: '0.0.2', |
| ); |
| |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: |
| '${plugin1.pubspecFile.path}\n' |
| '${plugin2.pubspecFile.path}\n', |
| ), |
| ), |
| ]; |
| processRunner.mockProcessesForExecutable['git-tag'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: |
| 'plugin1-v0.0.2\n' |
| 'plugin2-v0.0.2\n', |
| ), |
| ), |
| ]; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('plugin1 0.0.2 has already been published'), |
| contains('SKIPPING: already published'), |
| contains('plugin2 0.0.2 has already been published'), |
| contains('SKIPPING: already published'), |
| ]), |
| ); |
| |
| expect( |
| processRunner.recordedCalls.map((ProcessCall call) => call.executable), |
| isNot(contains('git-push')), |
| ); |
| }); |
| |
| test('Existing versions do not trigger release, but fail if the tags do not exist.', () async { |
| mockHttpResponses['plugin1'] = <String, dynamic>{ |
| 'name': 'plugin1', |
| 'versions': <String>['0.0.2'], |
| }; |
| |
| mockHttpResponses['plugin2'] = <String, dynamic>{ |
| 'name': 'plugin2', |
| 'versions': <String>['0.0.2'], |
| }; |
| |
| // Non-federated |
| final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); |
| // federated |
| final RepositoryPackage plugin2 = createFakePlugin( |
| 'plugin2', |
| packagesDir.childDirectory('plugin2'), |
| version: '0.0.2', |
| ); |
| |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: |
| '${plugin1.pubspecFile.path}\n' |
| '${plugin2.pubspecFile.path}\n', |
| ), |
| ), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| commandRunner, |
| <String>['publish', '--all-changed', '--base-sha=HEAD~'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'plugin1 0.0.2 has already been published, ' |
| 'however the git release tag (plugin1-v0.0.2) was not found.', |
| ), |
| contains( |
| 'plugin2 0.0.2 has already been published, ' |
| 'however the git release tag (plugin2-v0.0.2) was not found.', |
| ), |
| ]), |
| ); |
| expect( |
| processRunner.recordedCalls.map((ProcessCall call) => call.executable), |
| isNot(contains('git-push')), |
| ); |
| }); |
| |
| test('No version change does not release any plugins', () async { |
| // Non-federated |
| final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); |
| // federated |
| final RepositoryPackage plugin2 = createFakePlugin( |
| 'plugin2', |
| packagesDir.childDirectory('plugin2'), |
| ); |
| |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: |
| '${plugin1.libDirectory.childFile('plugin1.dart').path}\n' |
| '${plugin2.libDirectory.childFile('plugin2.dart').path}\n', |
| ), |
| ), |
| ]; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| ]); |
| |
| expect(output, containsAllInOrder(<String>['Ran for 0 package(s)'])); |
| expect( |
| processRunner.recordedCalls.map((ProcessCall call) => call.executable), |
| isNot(contains('git-push')), |
| ); |
| }); |
| |
| group('--batch-release-branch flag', () { |
| test('filters packages based on the existence of ci_config.yaml', () async { |
| // Mock pub.dev responses. |
| mockHttpResponses['package1'] = <String, dynamic>{ |
| 'name': 'package1', |
| 'versions': <String>['0.0.1'], |
| }; |
| mockHttpResponses['package2'] = <String, dynamic>{ |
| 'name': 'package2', |
| 'versions': <String>['0.0.1'], |
| }; |
| |
| // Mock packages. |
| final RepositoryPackage package1 = createFakePackage( |
| 'package1', |
| packagesDir, |
| version: '0.0.2', |
| ); |
| createFakeCiConfig(package: package1, batchRelease: true); |
| |
| final RepositoryPackage package2 = createFakePackage( |
| 'package2', |
| packagesDir, |
| version: '0.0.2', |
| ); |
| |
| expect(package1.ciConfigFile.existsSync(), true); |
| expect(package2.ciConfigFile.existsSync(), false); |
| |
| // Mock git diff to show both packages have changed. |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess(stdout: '${package1.pubspecFile.path}\n${package2.pubspecFile.path}'), |
| ), |
| ]; |
| |
| mockStdin.readLineOutput = 'y'; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| '--batch-release-branch=release-package1-1.0.0', |
| ]); |
| // Package1 is published in batch realease, pacakge2 is not. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running `pub publish ` in ${package1.path}...'), |
| contains('Published package1 successfully!'), |
| ]), |
| ); |
| |
| expect(output, isNot(contains(contains('Running `pub publish ` in ${package2.path}...!')))); |
| expect(output, isNot(contains(contains('Published package2 successfully!')))); |
| }); |
| |
| test('filters packages based on the batch release flag value in ci_config.yaml', () async { |
| // Mock pub.dev responses. |
| mockHttpResponses['package1'] = <String, dynamic>{ |
| 'name': 'package1', |
| 'versions': <String>['0.0.1'], |
| }; |
| mockHttpResponses['package2'] = <String, dynamic>{ |
| 'name': 'package2', |
| 'versions': <String>['0.0.1'], |
| }; |
| |
| // Mock packages. |
| final RepositoryPackage package1 = createFakePackage( |
| 'package1', |
| packagesDir, |
| version: '0.0.2', |
| ); |
| createFakeCiConfig(package: package1, batchRelease: true); |
| final RepositoryPackage package2 = createFakePackage( |
| 'package2', |
| packagesDir, |
| version: '0.0.2', |
| ); |
| createFakeCiConfig(package: package2, batchRelease: false); |
| |
| // Mock git diff to show both packages have changed. |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess(stdout: '${package1.pubspecFile.path}\n${package2.pubspecFile.path}'), |
| ), |
| ]; |
| |
| mockStdin.readLineOutput = 'y'; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| '--batch-release-branch=release-package1-1.0.0', |
| ]); |
| // Package1 is published in batch realease, pacakge2 is not. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running `pub publish ` in ${package1.path}...'), |
| contains('Published package1 successfully!'), |
| ]), |
| ); |
| |
| expect(output, isNot(contains(contains('Running `pub publish ` in ${package2.path}...!')))); |
| expect(output, isNot(contains(contains('Published package2 successfully!')))); |
| }); |
| |
| test( |
| 'when --batch-release-branch flag value is empty, batch release packages are filtered out', |
| () async { |
| // Mock pub.dev responses. |
| mockHttpResponses['package1'] = <String, dynamic>{ |
| 'name': 'package1', |
| 'versions': <String>['0.0.1'], |
| }; |
| mockHttpResponses['package2'] = <String, dynamic>{ |
| 'name': 'package2', |
| 'versions': <String>['0.0.1'], |
| }; |
| |
| // Mock packages. |
| final RepositoryPackage package1 = createFakePackage( |
| 'package1', |
| packagesDir, |
| version: '0.0.2', |
| ); |
| |
| final RepositoryPackage package2 = createFakePackage( |
| 'package2', |
| packagesDir, |
| version: '0.0.2', |
| ); |
| createFakeCiConfig(package: package2, batchRelease: true); |
| |
| // Mock git diff to show both packages have changed. |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess(stdout: '${package1.pubspecFile.path}\n${package2.pubspecFile.path}'), |
| ), |
| ]; |
| |
| mockStdin.readLineOutput = 'y'; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| ]); |
| // Package1 is published in batch realease, pacakge2 is not. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running `pub publish ` in ${package1.path}...'), |
| contains('Published package1 successfully!'), |
| ]), |
| ); |
| |
| expect( |
| output, |
| isNot(contains(contains('Running `pub publish ` in ${package2.path}...!'))), |
| ); |
| expect(output, isNot(contains(contains('Published package2 successfully!')))); |
| }, |
| ); |
| test(' throw tool exit when could not parse ci config file', () async { |
| // Mock pub.dev responses. |
| mockHttpResponses['package1'] = <String, dynamic>{ |
| 'name': 'package1', |
| 'versions': <String>['0.0.1'], |
| }; |
| |
| // Mock packages. |
| final RepositoryPackage package1 = createFakePackage( |
| 'package1', |
| packagesDir, |
| version: '0.0.2', |
| ); |
| createFakeCiConfig(package: package1, batchRelease: false); |
| package1.ciConfigFile.writeAsStringSync('wrong format of ci config file'); |
| |
| // Mock git diff to show both packages have changed. |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: '${package1.pubspecFile.path}\n')), |
| ]; |
| |
| mockStdin.readLineOutput = 'y'; |
| |
| Error? commandError; |
| // Package1 is published in batch realease, pacakge2 is not. |
| final List<String> output = await runCapturingPrint( |
| commandRunner, |
| <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| '--batch-release-branch=release-package1-1.0.0', |
| ], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'Could not parse ci_config.yaml for package1: FormatException: Root of ci_config.yaml must be a map.', |
| ), |
| ]), |
| ); |
| }); |
| }); |
| |
| test('Do not release flutter_plugin_tools', () async { |
| mockHttpResponses['plugin1'] = <String, dynamic>{ |
| 'name': 'flutter_plugin_tools', |
| 'versions': <String>[], |
| }; |
| |
| final RepositoryPackage flutterPluginTools = createFakePlugin( |
| 'flutter_plugin_tools', |
| packagesDir, |
| ); |
| processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: flutterPluginTools.pubspecFile.path)), |
| ]; |
| |
| final List<String> output = await runCapturingPrint(commandRunner, <String>[ |
| 'publish', |
| '--all-changed', |
| '--base-sha=HEAD~', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('SKIPPING: publishing flutter_plugin_tools via the tool is not supported'), |
| ]), |
| ); |
| expect(output.contains('Running `pub publish ` in ${flutterPluginTools.path}...'), isFalse); |
| expect( |
| processRunner.recordedCalls.map((ProcessCall call) => call.executable), |
| isNot(contains('git-push')), |
| ); |
| }); |
| }); |
| |
| group('credential location', () { |
| test('Linux with XDG', () async { |
| platform = MockPlatform(isLinux: true); |
| platform.environment['XDG_CONFIG_HOME'] = '/xdghome/config'; |
| command = PublishCommand(packagesDir, platform: platform); |
| |
| expect(command.credentialsPath, '/xdghome/config/dart/pub-credentials.json'); |
| }); |
| |
| test('Linux without XDG', () async { |
| platform = MockPlatform(isLinux: true); |
| platform.environment['HOME'] = '/home'; |
| command = PublishCommand(packagesDir, platform: platform); |
| |
| expect(command.credentialsPath, '/home/.config/dart/pub-credentials.json'); |
| }); |
| |
| test('macOS', () async { |
| platform = MockPlatform(isMacOS: true); |
| platform.environment['HOME'] = '/Users/someuser'; |
| command = PublishCommand(packagesDir, platform: platform); |
| |
| expect( |
| command.credentialsPath, |
| '/Users/someuser/Library/Application Support/dart/pub-credentials.json', |
| ); |
| }); |
| |
| test('Windows', () async { |
| platform = MockPlatform(isWindows: true); |
| platform.environment['APPDATA'] = r'C:\Users\SomeUser\AppData'; |
| command = PublishCommand(packagesDir, platform: platform); |
| |
| expect(command.credentialsPath, r'C:\Users\SomeUser\AppData\dart\pub-credentials.json'); |
| }); |
| }); |
| } |
| |
| /// An extension of [RecordingProcessRunner] that stores 'flutter pub publish' |
| /// calls so that their input streams can be checked in tests. |
| class TestProcessRunner extends RecordingProcessRunner { |
| // Most recent returned publish process. |
| late MockProcess mockPublishProcess; |
| |
| @override |
| Future<io.Process> start( |
| String executable, |
| List<String> args, { |
| Directory? workingDirectory, |
| }) async { |
| final io.Process process = await super.start( |
| executable, |
| args, |
| workingDirectory: workingDirectory, |
| ); |
| if (executable == 'flutter' && args.isNotEmpty && args[0] == 'pub' && args[1] == 'publish') { |
| mockPublishProcess = process as MockProcess; |
| } |
| return process; |
| } |
| } |
| |
| class MockStdin extends Mock implements io.Stdin { |
| List<List<int>> mockUserInputs = <List<int>>[]; |
| final StreamController<List<int>> _controller = StreamController<List<int>>(); |
| String? readLineOutput; |
| |
| @override |
| Stream<S> transform<S>(StreamTransformer<List<int>, S> streamTransformer) { |
| mockUserInputs.forEach(_addUserInputsToSteam); |
| return _controller.stream.transform(streamTransformer); |
| } |
| |
| @override |
| StreamSubscription<List<int>> listen( |
| void Function(List<int> event)? onData, { |
| Function? onError, |
| void Function()? 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); |
| } |