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