Files
packages/script/tool/test/publish_command_test.dart
stuartmorgan 5b7d732939 [tool] Use new pub cache location for publish (#3962)
As of Dart 3.0 (Flutter 3.10), the location that the tool was using for
the pub cache wasn't being read, breaking autopublish in CI. This
updates to the new location, and adds tests for it.

Should fix the fact that the `release` step hangs on every package
change now.
2023-05-11 06:11:46 -04:00

974 lines
32 KiB
Dart

// 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:platform/platform.dart';
import 'package:test/test.dart';
import 'common/package_command_test.mocks.dart';
import 'mocks.dart';
import 'util.dart';
void main() {
final String flutterCommand = getFlutterCommand(const LocalPlatform());
late MockPlatform platform;
late Directory packagesDir;
late MockGitDir gitDir;
late TestProcessRunner processRunner;
late PublishCommand command;
late CommandRunner<void> commandRunner;
late MockStdin mockStdin;
late FileSystem fileSystem;
// Map of package name to mock response.
late Map<String, Map<String, dynamic>> 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 = <String, Map<String, dynamic>>{};
final MockClient 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);
});
gitDir = MockGitDir();
when(gitDir.path).thenReturn(packagesDir.parent.path);
when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError')))
.thenAnswer((Invocation invocation) {
final List<String> arguments =
invocation.positionalArguments[0]! as List<String>;
// 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<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('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[flutterCommand] =
<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('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(
flutterCommand,
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(
flutterCommand,
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(
flutterCommand,
const <String>['pub', 'publish', '--server=bar', '--force'],
plugin1.path),
ProcessCall(
flutterCommand,
const <String>['pub', 'publish', '--server=bar', '--force'],
plugin2.path),
]));
});
test('throws if pub publish fails', () async {
createFakePlugin('foo', packagesDir, examples: <String>[]);
processRunner.mockProcessesForExecutable[flutterCommand] =
<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 String 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!'),
],
),
);
});
});
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[flutterCommand] =
<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))));
});
});
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('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('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 - \x1B[32mpublished\x1B[0m'),
contains('plugin2/plugin2 - \x1B[32mpublished\x1B[0m'),
]));
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')));
});
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 == getFlutterCommand(const LocalPlatform()) &&
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);
}