// 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/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/publish_command.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; void main() { late MockPlatform platform; late Directory packagesDir; late MockGitDir gitDir; late TestProcessRunner processRunner; late PublishCommand command; late CommandRunner commandRunner; late MockStdin mockStdin; late FileSystem fileSystem; // Map of package name to mock response. late Map> mockHttpResponses; void createMockCredentialFile() { fileSystem.file(command.credentialsPath) ..createSync(recursive: true) ..writeAsStringSync('some credential'); } setUp(() async { platform = MockPlatform(isLinux: true); platform.environment['HOME'] = '/home'; fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = TestProcessRunner(); mockHttpResponses = >{}; final MockClient mockClient = MockClient((http.Request request) async { final String packageName = request.url.pathSegments.last.replaceAll('.json', ''); final Map? 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); }); gitDir = MockGitDir(); when(gitDir.path).thenReturn(packagesDir.parent.path); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { final List arguments = invocation.positionalArguments[0]! as List; // Route git calls through the process runner, to make mock output // consistent with outer processes. Attach the first argument to the // command to make targeting the mock results easier. final String gitCommand = arguments.removeAt(0); return processRunner.run('git-$gitCommand', arguments); }); mockStdin = MockStdin(); command = PublishCommand( packagesDir, platform: platform, processRunner: processRunner, stdinput: mockStdin, gitDir: gitDir, httpClient: mockClient, ); commandRunner = CommandRunner('tester', '')..addCommand(command); }); group('Initial validation', () { test('refuses to proceed with dirty files', () async { final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); processRunner.mockProcessesForExecutable['git-status'] = [ FakeProcessInfo(MockProcess( stdout: '?? ${plugin.directory.childFile('tmp').path}\n')) ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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: []); processRunner.mockProcessesForExecutable['git-remote'] = [ FakeProcessInfo(MockProcess(exitCode: 1)), ]; Error? commandError; final List output = await runCapturingPrint( commandRunner, ['publish', '--packages=foo'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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: []); package.prePublishScript.createSync(recursive: true); final List output = await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', ]); expect( output, containsAllInOrder([ contains('Running pre-publish hook tool/pre_publish.dart...'), ]), ); expect( processRunner.recordedCalls, containsAllInOrder([ ProcessCall( 'dart', const [ 'pub', 'get', ], package.directory.path), ProcessCall( 'dart', const [ '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: []); package.prePublishScript.createSync(recursive: true); processRunner.mockProcessesForExecutable['dart'] = [ FakeProcessInfo(MockProcess(exitCode: 1), ['run']), // run tool/pre_publish.dart ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Pre-publish script failed.'), ]), ); expect( processRunner.recordedCalls, containsAllInOrder([ ProcessCall( getFlutterCommand(platform), const [ 'pub', 'get', ], package.directory.path), ProcessCall( 'dart', const [ '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: []); createFakePlugin('plugin2', packagesDir, examples: []); processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo( MockProcess( stdout: 'Foo', stderr: 'Bar', stdoutEncoding: utf8, stderrEncoding: utf8), ['pub', 'publish']), // publish for plugin1 FakeProcessInfo( MockProcess( stdout: 'Baz', stdoutEncoding: utf8, stderrEncoding: utf8), ['pub', 'publish']), // publish for plugin2 ]; final List output = await runCapturingPrint( commandRunner, ['publish', '--packages=plugin1,plugin2']); expect( output, containsAllInOrder([ 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: []); mockStdin.mockUserInputs.add(utf8.encode('user input')); await runCapturingPrint( commandRunner, ['publish', '--packages=foo']); expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); }); test('forwards --pub-publish-flags to pub publish', () async { final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', '--pub-publish-flags', '--dry-run,--server=bar' ]); expect( processRunner.recordedCalls, contains(ProcessCall( 'flutter', const ['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: []); await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', '--skip-confirmation', '--pub-publish-flags', '--server=bar' ]); expect( processRunner.recordedCalls, contains(ProcessCall( 'flutter', const ['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: []); final RepositoryPackage plugin2 = createFakePlugin('plugin_b', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish', '--packages=plugin_a,plugin_b', '--skip-confirmation', '--pub-publish-flags', '--server=bar' ]); expect( processRunner.recordedCalls, containsAllInOrder([ ProcessCall( 'flutter', const ['pub', 'publish', '--server=bar', '--force'], plugin1.path), ProcessCall( 'flutter', const ['pub', 'publish', '--server=bar', '--force'], plugin2.path), ])); }); test('creates credential file from envirnoment variable if necessary', () async { createFakePlugin('foo', packagesDir, examples: []); const String credentials = 'some credential'; platform.environment['PUB_CREDENTIALS'] = credentials; await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', '--skip-confirmation', '--pub-publish-flags', '--server=bar' ]); final File credentialFile = fileSystem.file(command.credentialsPath); expect(credentialFile.existsSync(), true); expect(credentialFile.readAsStringSync(), credentials); }); test('throws if pub publish fails', () async { createFakePlugin('foo', packagesDir, examples: []); processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo(MockProcess(exitCode: 128), ['pub', 'publish']) ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Publishing foo failed.'), ])); }); test('publish, dry run', () async { final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); final List output = await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', '--dry-run', ]); expect( processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); expect( output, containsAllInOrder([ 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 String packageName = 'a_package'; createFakePackage(packageName, packagesDir); final List output = await runCapturingPrint(commandRunner, [ 'publish', '--packages=$packageName', ]); expect( output, containsAllInOrder( [ contains('Running `pub publish ` in /packages/a_package...'), contains('Package published!'), ], ), ); }); test('skips publish with --tag-for-auto-publish', () async { const String packageName = 'a_package'; createFakePackage(packageName, packagesDir); final List output = await runCapturingPrint(commandRunner, [ '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( [ contains('Tagged a_package successfully!'), ], ), ); }); }); group('Tags release', () { test('with the version and name from the pubspec.yaml', () async { createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', ]); expect(processRunner.recordedCalls, contains(const ProcessCall('git-tag', ['foo-v0.0.1'], null))); }); test('only if publishing succeeded', () async { createFakePlugin('foo', packagesDir, examples: []); processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo(MockProcess(exitCode: 128), ['pub', 'publish']), ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Publishing foo failed.'), ])); expect( processRunner.recordedCalls, isNot(contains( const ProcessCall('git-tag', ['foo-v0.0.1'], null)))); }); test('when passed --tag-for-auto-publish', () async { createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', '--tag-for-auto-publish', ]); expect(processRunner.recordedCalls, contains(const ProcessCall('git-tag', ['foo-v0.0.1'], null))); }); }); group('Pushes tags', () { test('to upstream by default', () async { createFakePlugin('foo', packagesDir, examples: []); mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', ]); expect( processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ 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: []); final List output = await runCapturingPrint(commandRunner, [ 'publish', '--skip-confirmation', '--packages=foo', ]); expect( processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ contains('Published foo successfully!'), ])); }); test('when passed --tag-for-auto-publish', () async { createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', '--skip-confirmation', '--tag-for-auto-publish', ]); expect( processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'foo-v0.0.1'], null))); }); test('to upstream by default, dry run', () async { final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint( commandRunner, ['publish', '--packages=foo', '--dry-run']); expect( processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); expect( output, containsAllInOrder([ 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: []); mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, [ 'publish', '--packages=foo', '--remote', 'origin', ]); expect( processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['origin', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ contains('Published foo successfully!'), ])); }); }); group('--already-tagged', () { test('passes when HEAD has the expected tag', () async { createFakePlugin('foo', packagesDir, examples: []); processRunner.mockProcessesForExecutable['git-tag'] = [ FakeProcessInfo(MockProcess()), // Skip the initializeRun call. FakeProcessInfo(MockProcess(stdout: 'foo-v0.0.1\n'), ['--points-at', 'HEAD']) ]; await runCapturingPrint(commandRunner, ['publish', '--packages=foo', '--already-tagged']); }); test('fails if HEAD does not have the expected tag', () async { createFakePlugin('foo', packagesDir, examples: []); Error? commandError; final List output = await runCapturingPrint(commandRunner, ['publish', '--packages=foo', '--already-tagged'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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: []); processRunner.mockProcessesForExecutable['git-tag'] = [ FakeProcessInfo(MockProcess()), // Skip the initializeRun call. FakeProcessInfo(MockProcess(stdout: 'foo-v0.0.1\n'), ['--points-at', 'HEAD']) ]; await runCapturingPrint(commandRunner, ['publish', '--packages=foo', '--already-tagged']); expect( processRunner.recordedCalls, isNot(contains( const ProcessCall('git-tag', ['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'] = { 'name': 'plugin1', 'versions': [], }; mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': [], }; // Non-federated final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); // federated final RepositoryPackage plugin2 = createFakePlugin( 'plugin2', packagesDir.childDirectory('plugin2'), ); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess( stdout: '${plugin1.pubspecFile.path}\n' '${plugin2.pubspecFile.path}\n')) ]; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, containsAllInOrder([ 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', ['upstream', 'plugin1-v0.0.1'], null))); expect( processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); test('can release newly created plugins, while there are existing plugins', () async { mockHttpResponses['plugin0'] = { 'name': 'plugin0', 'versions': ['0.0.1'], }; mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': [], }; mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': [], }; // 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(MockProcess(stdout: 'plugin0-v0.0.1\n')) ]; processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess( stdout: '${plugin1.pubspecFile.path}\n' '${plugin2.pubspecFile.path}\n')) ]; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, containsAllInOrder([ 'Running `pub publish ` in ${plugin1.path}...\n', 'Running `pub publish ` in ${plugin2.path}...\n', ])); expect( processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); expect( processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); test('can release newly created plugins, dry run', () async { mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': [], }; mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': [], }; // Non-federated final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); // federated final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess( stdout: '${plugin1.pubspecFile.path}\n' '${plugin2.pubspecFile.path}\n')) ]; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint( commandRunner, [ 'publish', '--all-changed', '--base-sha=HEAD~', '--dry-run' ]); expect( output, containsAllInOrder([ 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'] = { 'name': 'plugin1', 'versions': ['0.0.1'], }; mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['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(MockProcess( stdout: '${plugin1.pubspecFile.path}\n' '${plugin2.pubspecFile.path}\n')) ]; mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output2, containsAllInOrder([ 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', ['upstream', 'plugin1-v0.0.2'], null))); expect( processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.2'], null))); }); test( 'delete package will not trigger publish but exit the command successfully!', () async { mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': ['0.0.1'], }; mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['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(MockProcess( stdout: '${plugin1.pubspecFile.path}\n' '${plugin2.pubspecFile.path}\n')) ]; mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output2, containsAllInOrder([ 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', ['upstream', 'plugin1-v0.0.2'], null))); }); test('Existing versions do not trigger release, also prints out message.', () async { mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': ['0.0.2'], }; mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['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(MockProcess( stdout: '${plugin1.pubspecFile.path}\n' '${plugin2.pubspecFile.path}\n')) ]; processRunner.mockProcessesForExecutable['git-tag'] = [ FakeProcessInfo(MockProcess( stdout: 'plugin1-v0.0.2\n' 'plugin2-v0.0.2\n')) ]; final List output = await runCapturingPrint(commandRunner, ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, containsAllInOrder([ 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'] = { 'name': 'plugin1', 'versions': ['0.0.2'], }; mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['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(MockProcess( stdout: '${plugin1.pubspecFile.path}\n' '${plugin2.pubspecFile.path}\n')) ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, ['publish', '--all-changed', '--base-sha=HEAD~'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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(MockProcess( stdout: '${plugin1.libDirectory.childFile('plugin1.dart').path}\n' '${plugin2.libDirectory.childFile('plugin2.dart').path}\n')) ]; final List output = await runCapturingPrint(commandRunner, ['publish', '--all-changed', '--base-sha=HEAD~']); expect(output, containsAllInOrder(['Ran for 0 package(s)'])); expect( processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); test('Do not release flutter_plugin_tools', () async { mockHttpResponses['plugin1'] = { 'name': 'flutter_plugin_tools', 'versions': [], }; final RepositoryPackage flutterPluginTools = createFakePlugin('flutter_plugin_tools', packagesDir); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess(stdout: flutterPluginTools.pubspecFile.path)) ]; final List output = await runCapturingPrint(commandRunner, ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, containsAllInOrder([ 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 start(String executable, List 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> mockUserInputs = >[]; final StreamController> _controller = StreamController>(); String? readLineOutput; @override Stream transform(StreamTransformer, S> streamTransformer) { mockUserInputs.forEach(_addUserInputsToSteam); return _controller.stream.transform(streamTransformer); } @override StreamSubscription> listen(void Function(List 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 input) => _controller.add(input); }