blob: 068ccb56569a9f9d851981d6993c27990aaa102f [file] [log] [blame]
// 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<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('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('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 String 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 = 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 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!'),
],
),
);
});
test('skips publish with --tag-for-auto-publish', () async {
const String 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')));
});
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);
}