Files
packages/script/tool/test/publish_plugin_command_test.dart
stuartmorgan 74d03857f8 [flutter_plugin_tools] Remove global state from tests (#4018)
Eliminates the global test filesystem and global test packages directory, in favor of local versions. This guarantees that each test runs with a clean filesystem state, rather than relying on cleanup. It also simplifies understanding the tests, since everything is done via params and return values instead of needing to know about the magic global variables and which methods mutate them.
2021-06-07 10:04:43 -07:00

986 lines
36 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.
// @dart=2.9
import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:flutter_plugin_tools/src/common.dart';
import 'package:flutter_plugin_tools/src/publish_plugin_command.dart';
import 'package:git/git.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'mocks.dart';
import 'util.dart';
void main() {
const String testPluginName = 'foo';
List<String> printedMessages;
Directory testRoot;
Directory packagesDir;
Directory pluginDir;
GitDir gitDir;
TestProcessRunner processRunner;
CommandRunner<void> commandRunner;
MockStdin mockStdin;
// This test uses a local file system instead of an in memory one throughout
// so that git actually works. In setup we initialize a mono repo of plugins
// with one package and commit everything to Git.
const FileSystem fileSystem = LocalFileSystem();
void _createMockCredentialFile() {
final String credentialPath = PublishPluginCommand.getCredentialPath();
fileSystem.file(credentialPath)
..createSync(recursive: true)
..writeAsStringSync('some credential');
}
setUp(() async {
testRoot = fileSystem.systemTempDirectory
.createTempSync('publish_plugin_command_test-');
// The temp directory can have symbolic links, which won't match git output;
// use a fully resolved version to avoid potential path comparison issues.
testRoot = fileSystem.directory(testRoot.resolveSymbolicLinksSync());
packagesDir = createPackagesDirectory(parentDir: testRoot);
pluginDir =
createFakePlugin(testPluginName, packagesDir, withSingleExample: false);
assert(pluginDir != null && pluginDir.existsSync());
createFakePubspec(pluginDir, includeVersion: true);
io.Process.runSync('git', <String>['init'],
workingDirectory: testRoot.path);
gitDir = await GitDir.fromExisting(testRoot.path);
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Initial commit']);
processRunner = TestProcessRunner();
mockStdin = MockStdin();
printedMessages = <String>[];
commandRunner = CommandRunner<void>('tester', '')
..addCommand(PublishPluginCommand(packagesDir,
processRunner: processRunner,
print: (Object message) => printedMessages.add(message.toString()),
stdinput: mockStdin,
gitDir: gitDir));
});
tearDown(() {
testRoot.deleteSync(recursive: true);
});
group('Initial validation', () {
test('requires a package flag', () async {
await expectLater(() => commandRunner.run(<String>['publish-plugin']),
throwsA(const TypeMatcher<ToolExit>()));
expect(
printedMessages.last, contains('Must specify a package to publish.'));
});
test('requires an existing flag', () async {
await expectLater(
() => commandRunner.run(<String>[
'publish-plugin',
'--package',
'iamerror',
'--no-push-tags'
]),
throwsA(const TypeMatcher<ToolExit>()));
expect(printedMessages.last, contains('iamerror does not exist'));
});
test('refuses to proceed with dirty files', () async {
pluginDir.childFile('tmp').createSync();
await expectLater(
() => commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--no-push-tags'
]),
throwsA(const TypeMatcher<ToolExit>()));
expect(
printedMessages,
containsAllInOrder(<String>[
'There are files in the package directory that haven\'t been saved in git. Refusing to publish these files:\n\n?? packages/foo/tmp\n\nIf the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.',
'Failed, see above for details.',
]));
});
test('fails immediately if the remote doesn\'t exist', () async {
await expectLater(
() => commandRunner
.run(<String>['publish-plugin', '--package', testPluginName]),
throwsA(const TypeMatcher<ToolExit>()));
expect(processRunner.results.last.stderr, contains('No such remote'));
});
test("doesn't validate the remote if it's not pushing tags", () async {
// Immediately return 0 when running `pub publish`.
processRunner.mockPublishCompleteCode = 0;
await commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--no-push-tags',
'--no-tag-release'
]);
expect(printedMessages.last, 'Done!');
});
test('can publish non-flutter package', () async {
createFakePubspec(pluginDir, includeVersion: true, isFlutter: false);
io.Process.runSync('git', <String>['init'],
workingDirectory: testRoot.path);
gitDir = await GitDir.fromExisting(testRoot.path);
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Initial commit']);
// Immediately return 0 when running `pub publish`.
processRunner.mockPublishCompleteCode = 0;
await commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--no-push-tags',
'--no-tag-release'
]);
expect(printedMessages.last, 'Done!');
});
});
group('Publishes package', () {
test('while showing all output from pub publish to the user', () async {
final Future<void> publishCommand = commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--no-push-tags',
'--no-tag-release'
]);
processRunner.mockPublishStdout = 'Foo';
processRunner.mockPublishStderr = 'Bar';
processRunner.mockPublishCompleteCode = 0;
await publishCommand;
expect(printedMessages, contains('Foo'));
expect(printedMessages, contains('Bar'));
});
test('forwards input from the user to `pub publish`', () async {
final Future<void> publishCommand = commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--no-push-tags',
'--no-tag-release'
]);
mockStdin.mockUserInputs.add(utf8.encode('user input'));
processRunner.mockPublishCompleteCode = 0;
await publishCommand;
expect(processRunner.mockPublishProcess.stdinMock.lines,
contains('user input'));
});
test('forwards --pub-publish-flags to pub publish', () async {
processRunner.mockPublishCompleteCode = 0;
await commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--no-push-tags',
'--no-tag-release',
'--pub-publish-flags',
'--dry-run,--server=foo'
]);
expect(processRunner.mockPublishArgs.length, 4);
expect(processRunner.mockPublishArgs[0], 'pub');
expect(processRunner.mockPublishArgs[1], 'publish');
expect(processRunner.mockPublishArgs[2], '--dry-run');
expect(processRunner.mockPublishArgs[3], '--server=foo');
});
test(
'--skip-confirmation flag automatically adds --force to --pub-publish-flags',
() async {
processRunner.mockPublishCompleteCode = 0;
_createMockCredentialFile();
await commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--no-push-tags',
'--no-tag-release',
'--skip-confirmation',
'--pub-publish-flags',
'--server=foo'
]);
expect(processRunner.mockPublishArgs.length, 4);
expect(processRunner.mockPublishArgs[0], 'pub');
expect(processRunner.mockPublishArgs[1], 'publish');
expect(processRunner.mockPublishArgs[2], '--server=foo');
expect(processRunner.mockPublishArgs[3], '--force');
});
test('throws if pub publish fails', () async {
processRunner.mockPublishCompleteCode = 128;
await expectLater(
() => commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--no-push-tags',
'--no-tag-release',
]),
throwsA(const TypeMatcher<ToolExit>()));
expect(printedMessages, contains('Publish foo failed.'));
});
test('publish, dry run', () async {
// Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw.
processRunner.mockPublishCompleteCode = 1;
await commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--dry-run',
'--no-push-tags',
'--no-tag-release',
]);
expect(processRunner.pushTagsArgs, isEmpty);
expect(
printedMessages,
containsAllInOrder(<String>[
'=============== DRY RUN ===============',
'Running `pub publish ` in ${pluginDir.path}...\n',
'Done!'
]));
});
});
group('Tags release', () {
test('with the version and name from the pubspec.yaml', () async {
processRunner.mockPublishCompleteCode = 0;
await commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--no-push-tags',
]);
final String tag =
(await gitDir.runCommand(<String>['show-ref', 'fake_package-v0.0.1']))
.stdout as String;
expect(tag, isNotEmpty);
});
test('only if publishing succeeded', () async {
processRunner.mockPublishCompleteCode = 128;
await expectLater(
() => commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--no-push-tags',
]),
throwsA(const TypeMatcher<ToolExit>()));
expect(printedMessages, contains('Publish foo failed.'));
final String tag = (await gitDir.runCommand(
<String>['show-ref', 'fake_package-v0.0.1'],
throwOnError: false))
.stdout as String;
expect(tag, isEmpty);
});
});
group('Pushes tags', () {
setUp(() async {
await gitDir.runCommand(
<String>['remote', 'add', 'upstream', 'http://localhost:8000']);
});
test('requires user confirmation', () async {
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'help';
await expectLater(
() => commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
]),
throwsA(const TypeMatcher<ToolExit>()));
expect(printedMessages, contains('Tag push canceled.'));
});
test('to upstream by default', () async {
await gitDir.runCommand(<String>['tag', 'garbage']);
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'y';
await commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
]);
expect(processRunner.pushTagsArgs.isNotEmpty, isTrue);
expect(processRunner.pushTagsArgs[1], 'upstream');
expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1');
expect(printedMessages.last, 'Done!');
});
test('does not ask for user input if the --skip-confirmation flag is on',
() async {
await gitDir.runCommand(<String>['tag', 'garbage']);
processRunner.mockPublishCompleteCode = 0;
_createMockCredentialFile();
await commandRunner.run(<String>[
'publish-plugin',
'--skip-confirmation',
'--package',
testPluginName,
]);
expect(processRunner.pushTagsArgs.isNotEmpty, isTrue);
expect(processRunner.pushTagsArgs[1], 'upstream');
expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1');
expect(printedMessages.last, 'Done!');
});
test('to upstream by default, dry run', () async {
await gitDir.runCommand(<String>['tag', 'garbage']);
// Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw.
processRunner.mockPublishCompleteCode = 1;
mockStdin.readLineOutput = 'y';
await commandRunner.run(
<String>['publish-plugin', '--package', testPluginName, '--dry-run']);
expect(processRunner.pushTagsArgs, isEmpty);
expect(
printedMessages,
containsAllInOrder(<String>[
'=============== DRY RUN ===============',
'Running `pub publish ` in ${pluginDir.path}...\n',
'Tagging release fake_package-v0.0.1...',
'Pushing tag to upstream...',
'Done!'
]));
});
test('to different remotes based on a flag', () async {
await gitDir.runCommand(
<String>['remote', 'add', 'origin', 'http://localhost:8001']);
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'y';
await commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--remote',
'origin',
]);
expect(processRunner.pushTagsArgs.isNotEmpty, isTrue);
expect(processRunner.pushTagsArgs[1], 'origin');
expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1');
expect(printedMessages.last, 'Done!');
});
test('only if tagging and pushing to remotes are both enabled', () async {
processRunner.mockPublishCompleteCode = 0;
await commandRunner.run(<String>[
'publish-plugin',
'--package',
testPluginName,
'--no-tag-release',
]);
expect(processRunner.pushTagsArgs.isEmpty, isTrue);
expect(printedMessages.last, 'Done!');
});
});
group('Auto release (all-changed flag)', () {
setUp(() async {
io.Process.runSync('git', <String>['init'],
workingDirectory: testRoot.path);
gitDir = await GitDir.fromExisting(testRoot.path);
await gitDir.runCommand(
<String>['remote', 'add', 'upstream', 'http://localhost:8000']);
});
test('can release newly created plugins', () async {
// Non-federated
final Directory pluginDir1 =
createFakePlugin('plugin1', packagesDir, withSingleExample: true);
// federated
final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir,
withSingleExample: true, parentDirectoryName: 'plugin2');
createFakePubspec(pluginDir1,
name: 'plugin1',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
createFakePubspec(pluginDir2,
name: 'plugin2',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']);
// Immediately return 0 when running `pub publish`.
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'y';
await commandRunner
.run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']);
expect(
printedMessages,
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
'Done!'
]));
expect(processRunner.pushTagsArgs, isNotEmpty);
expect(processRunner.pushTagsArgs[0], 'push');
expect(processRunner.pushTagsArgs[1], 'upstream');
expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1');
expect(processRunner.pushTagsArgs[3], 'push');
expect(processRunner.pushTagsArgs[4], 'upstream');
expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1');
});
test('can release newly created plugins, while there are existing plugins',
() async {
// Prepare an exiting plugin and tag it
final Directory pluginDir0 =
createFakePlugin('plugin0', packagesDir, withSingleExample: true);
createFakePubspec(pluginDir0,
name: 'plugin0',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']);
// Immediately return 0 when running `pub publish`.
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'y';
await commandRunner
.run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']);
processRunner.pushTagsArgs.clear();
// Non-federated
final Directory pluginDir1 =
createFakePlugin('plugin1', packagesDir, withSingleExample: true);
// federated
final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir,
withSingleExample: true, parentDirectoryName: 'plugin2');
createFakePubspec(pluginDir1,
name: 'plugin1',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
createFakePubspec(pluginDir2,
name: 'plugin2',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']);
// Immediately return 0 when running `pub publish`.
await commandRunner
.run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']);
expect(
printedMessages,
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
'Done!'
]));
expect(processRunner.pushTagsArgs, isNotEmpty);
expect(processRunner.pushTagsArgs[0], 'push');
expect(processRunner.pushTagsArgs[1], 'upstream');
expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1');
expect(processRunner.pushTagsArgs[3], 'push');
expect(processRunner.pushTagsArgs[4], 'upstream');
expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1');
});
test('can release newly created plugins, dry run', () async {
// Non-federated
final Directory pluginDir1 =
createFakePlugin('plugin1', packagesDir, withSingleExample: true);
// federated
final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir,
withSingleExample: true, parentDirectoryName: 'plugin2');
createFakePubspec(pluginDir1,
name: 'plugin1',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
createFakePubspec(pluginDir2,
name: 'plugin2',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']);
// Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw.
processRunner.mockPublishCompleteCode = 1;
mockStdin.readLineOutput = 'y';
await commandRunner.run(<String>[
'publish-plugin',
'--all-changed',
'--base-sha=HEAD~',
'--dry-run'
]);
expect(
printedMessages,
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'=============== DRY RUN ===============',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Tagging release plugin1-v0.0.1...',
'Pushing tag to upstream...',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Tagging release plugin2-v0.0.1...',
'Pushing tag to upstream...',
'Packages released: plugin1, plugin2',
'Done!'
]));
expect(processRunner.pushTagsArgs, isEmpty);
});
test('version change triggers releases.', () async {
// Non-federated
final Directory pluginDir1 =
createFakePlugin('plugin1', packagesDir, withSingleExample: true);
// federated
final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir,
withSingleExample: true, parentDirectoryName: 'plugin2');
createFakePubspec(pluginDir1,
name: 'plugin1',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
createFakePubspec(pluginDir2,
name: 'plugin2',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']);
// Immediately return 0 when running `pub publish`.
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'y';
await commandRunner
.run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']);
expect(
printedMessages,
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
'Done!'
]));
expect(processRunner.pushTagsArgs, isNotEmpty);
expect(processRunner.pushTagsArgs[0], 'push');
expect(processRunner.pushTagsArgs[1], 'upstream');
expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1');
expect(processRunner.pushTagsArgs[3], 'push');
expect(processRunner.pushTagsArgs[4], 'upstream');
expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1');
processRunner.pushTagsArgs.clear();
printedMessages.clear();
final List<String> plugin1Pubspec =
pluginDir1.childFile('pubspec.yaml').readAsLinesSync();
plugin1Pubspec[plugin1Pubspec.indexWhere(
(String element) => element.contains('version:'))] = 'version: 0.0.2';
pluginDir1
.childFile('pubspec.yaml')
.writeAsStringSync(plugin1Pubspec.join('\n'));
final List<String> plugin2Pubspec =
pluginDir2.childFile('pubspec.yaml').readAsLinesSync();
plugin2Pubspec[plugin2Pubspec.indexWhere(
(String element) => element.contains('version:'))] = 'version: 0.0.2';
pluginDir2
.childFile('pubspec.yaml')
.writeAsStringSync(plugin2Pubspec.join('\n'));
await gitDir.runCommand(<String>['add', '-A']);
await gitDir
.runCommand(<String>['commit', '-m', 'Update versions to 0.0.2']);
await commandRunner
.run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']);
expect(
printedMessages,
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
'Done!'
]));
expect(processRunner.pushTagsArgs, isNotEmpty);
expect(processRunner.pushTagsArgs[0], 'push');
expect(processRunner.pushTagsArgs[1], 'upstream');
expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2');
expect(processRunner.pushTagsArgs[3], 'push');
expect(processRunner.pushTagsArgs[4], 'upstream');
expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2');
});
test(
'delete package will not trigger publish but exit the command successfully.',
() async {
// Non-federated
final Directory pluginDir1 =
createFakePlugin('plugin1', packagesDir, withSingleExample: true);
// federated
final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir,
withSingleExample: true, parentDirectoryName: 'plugin2');
createFakePubspec(pluginDir1,
name: 'plugin1',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
createFakePubspec(pluginDir2,
name: 'plugin2',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']);
// Immediately return 0 when running `pub publish`.
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'y';
await commandRunner
.run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']);
expect(
printedMessages,
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
'Done!'
]));
expect(processRunner.pushTagsArgs, isNotEmpty);
expect(processRunner.pushTagsArgs[0], 'push');
expect(processRunner.pushTagsArgs[1], 'upstream');
expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1');
expect(processRunner.pushTagsArgs[3], 'push');
expect(processRunner.pushTagsArgs[4], 'upstream');
expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1');
processRunner.pushTagsArgs.clear();
printedMessages.clear();
final List<String> plugin1Pubspec =
pluginDir1.childFile('pubspec.yaml').readAsLinesSync();
plugin1Pubspec[plugin1Pubspec.indexWhere(
(String element) => element.contains('version:'))] = 'version: 0.0.2';
pluginDir1
.childFile('pubspec.yaml')
.writeAsStringSync(plugin1Pubspec.join('\n'));
pluginDir2.deleteSync(recursive: true);
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>[
'commit',
'-m',
'Update plugin1 versions to 0.0.2, delete plugin2'
]);
await commandRunner
.run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']);
expect(
printedMessages,
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'The file at The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n',
'Packages released: plugin1',
'Done!'
]));
expect(processRunner.pushTagsArgs, isNotEmpty);
expect(processRunner.pushTagsArgs.length, 3);
expect(processRunner.pushTagsArgs[0], 'push');
expect(processRunner.pushTagsArgs[1], 'upstream');
expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2');
});
test(
'versions revert do not trigger releases. Also prints out warning message.',
() async {
// Non-federated
final Directory pluginDir1 =
createFakePlugin('plugin1', packagesDir, withSingleExample: true);
// federated
final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir,
withSingleExample: true, parentDirectoryName: 'plugin2');
createFakePubspec(pluginDir1,
name: 'plugin1',
includeVersion: true,
isFlutter: false,
version: '0.0.2');
createFakePubspec(pluginDir2,
name: 'plugin2',
includeVersion: true,
isFlutter: false,
version: '0.0.2');
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']);
// Immediately return 0 when running `pub publish`.
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'y';
await commandRunner
.run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']);
expect(
printedMessages,
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
'Done!'
]));
expect(processRunner.pushTagsArgs, isNotEmpty);
expect(processRunner.pushTagsArgs[0], 'push');
expect(processRunner.pushTagsArgs[1], 'upstream');
expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2');
expect(processRunner.pushTagsArgs[3], 'push');
expect(processRunner.pushTagsArgs[4], 'upstream');
expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2');
processRunner.pushTagsArgs.clear();
printedMessages.clear();
final List<String> plugin1Pubspec =
pluginDir1.childFile('pubspec.yaml').readAsLinesSync();
plugin1Pubspec[plugin1Pubspec.indexWhere(
(String element) => element.contains('version:'))] = 'version: 0.0.1';
pluginDir1
.childFile('pubspec.yaml')
.writeAsStringSync(plugin1Pubspec.join('\n'));
final List<String> plugin2Pubspec =
pluginDir2.childFile('pubspec.yaml').readAsLinesSync();
plugin2Pubspec[plugin2Pubspec.indexWhere(
(String element) => element.contains('version:'))] = 'version: 0.0.1';
pluginDir2
.childFile('pubspec.yaml')
.writeAsStringSync(plugin2Pubspec.join('\n'));
await gitDir.runCommand(<String>['add', '-A']);
await gitDir
.runCommand(<String>['commit', '-m', 'Update versions to 0.0.1']);
await commandRunner
.run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']);
expect(
printedMessages,
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'The new version (0.0.1) is lower than the current version (0.0.2) for plugin1.\nThis git commit is a revert, no release is tagged.',
'The new version (0.0.1) is lower than the current version (0.0.2) for plugin2.\nThis git commit is a revert, no release is tagged.',
'Done!'
]));
expect(processRunner.pushTagsArgs, isEmpty);
});
test('No version change does not release any plugins', () async {
// Non-federated
final Directory pluginDir1 =
createFakePlugin('plugin1', packagesDir, withSingleExample: true);
// federated
final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir,
withSingleExample: true, parentDirectoryName: 'plugin2');
createFakePubspec(pluginDir1,
name: 'plugin1',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
createFakePubspec(pluginDir2,
name: 'plugin2',
includeVersion: true,
isFlutter: false,
version: '0.0.1');
io.Process.runSync('git', <String>['init'],
workingDirectory: testRoot.path);
gitDir = await GitDir.fromExisting(testRoot.path);
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']);
pluginDir1.childFile('plugin1.dart').createSync();
pluginDir2.childFile('plugin2.dart').createSync();
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Add dart files']);
// Immediately return 0 when running `pub publish`.
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'y';
await commandRunner
.run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']);
expect(
printedMessages,
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'No version updates in this commit.',
'Done!'
]));
expect(processRunner.pushTagsArgs, isEmpty);
});
});
}
class TestProcessRunner extends ProcessRunner {
final List<io.ProcessResult> results = <io.ProcessResult>[];
// Most recent returned publish process.
MockProcess mockPublishProcess;
final List<String> mockPublishArgs = <String>[];
final MockProcessResult mockPushTagsResult = MockProcessResult();
final List<String> pushTagsArgs = <String>[];
String mockPublishStdout;
String mockPublishStderr;
int mockPublishCompleteCode;
@override
Future<io.ProcessResult> run(
String executable,
List<String> args, {
Directory workingDir,
bool exitOnError = false,
bool logOnError = false,
Encoding stdoutEncoding = io.systemEncoding,
Encoding stderrEncoding = io.systemEncoding,
}) async {
// Don't ever really push tags.
if (executable == 'git' && args.isNotEmpty && args[0] == 'push') {
pushTagsArgs.addAll(args);
return mockPushTagsResult;
}
final io.ProcessResult result = io.Process.runSync(executable, args,
workingDirectory: workingDir?.path);
results.add(result);
if (result.exitCode != 0) {
throw ToolExit(result.exitCode);
}
return result;
}
@override
Future<io.Process> start(String executable, List<String> args,
{Directory workingDirectory}) async {
/// Never actually publish anything. Start is always and only used for this
/// since it returns something we can route stdin through.
assert(executable == 'flutter' &&
args.isNotEmpty &&
args[0] == 'pub' &&
args[1] == 'publish');
mockPublishArgs.addAll(args);
mockPublishProcess = MockProcess();
if (mockPublishStdout != null) {
mockPublishProcess.stdoutController.add(utf8.encode(mockPublishStdout));
}
if (mockPublishStderr != null) {
mockPublishProcess.stderrController.add(utf8.encode(mockPublishStderr));
}
if (mockPublishCompleteCode != null) {
mockPublishProcess.exitCodeCompleter.complete(mockPublishCompleteCode);
}
return mockPublishProcess;
}
}
class MockStdin extends Mock implements io.Stdin {
List<List<int>> mockUserInputs = <List<int>>[];
StreamController<List<int>> _controller;
String readLineOutput;
@override
Stream<S> transform<S>(StreamTransformer<List<int>, S> streamTransformer) {
// In the test context, only one `PublishPluginCommand` object is created for a single test case.
// However, sometimes, we need to run multiple commands in a single test case.
// In such situation, this `MockStdin`'s StreamController might be listened to more than once, which is not allowed.
//
// Create a new controller every time so this Stdin could be listened to multiple times.
_controller = StreamController<List<int>>();
mockUserInputs.forEach(_addUserInputsToSteam);
return _controller.stream.transform(streamTransformer);
}
@override
StreamSubscription<List<int>> listen(void onData(List<int> event),
{Function onError, void onDone(), bool cancelOnError}) {
return _controller.stream.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
@override
String readLineSync(
{Encoding encoding = io.systemEncoding,
bool retainNewlines = false}) =>
readLineOutput;
void _addUserInputsToSteam(List<int> input) => _controller.add(input);
}
class MockProcessResult extends Mock implements io.ProcessResult {
MockProcessResult({int exitCode = 0}) : _exitCode = exitCode;
final int _exitCode;
@override
int get exitCode => _exitCode;
}