Files
packages/script/tool/test/publish_check_command_test.dart
stuartmorgan 8c287e99d6 [tool] Move changed file detection to base command class (#8730)
Consolidates the code to find all changed file paths into the `PackageLoopingCommand` class that is the base of almost all of the repo tooling commands. This in a preparatory PR for a future change to allow each command to define a list of files or file patterns that definitively *don't* affect that test, so that CI can be smarter about what tests to run (e.g., not running expensive integration tests for README changes).

A side effect of this change is that tests of almost all commands now need a mock `GitDir` instance. This would add a lot of copy/pasted boilerplate to the test setup, and there is already too much of that, so instead this refactors common test setup:
- Creating a memory file system
- Populating it with a packages directory
- Creating a RecordingProcessRunner to mock out process calls
- Creating a mock GitDir that forwards to a RecordingProcessRunner

into a helper method (using records and destructuring to easily return multiple values). While some tests don't need all of these steps, those that don't can easily ignore parts of it, and it will make it much easier to update tests in the future if they need them, and it makes the setup much more consistent which makes it easier to reason about test setup in general.

Prep for https://github.com/flutter/flutter/issues/136394
2025-03-25 21:29:17 +00:00

693 lines
22 KiB
Dart

// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
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_check_command.dart';
import 'package:git/git.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:test/test.dart';
import 'mocks.dart';
import 'util.dart';
void main() {
group('PublishCheckCommand tests', () {
late MockPlatform mockPlatform;
late Directory packagesDir;
late RecordingProcessRunner processRunner;
// Separate process runner for the mock gitDir to make asserting the
// expected command-specific calls easier.
late GitDir gitDir;
late CommandRunner<void> runner;
CommandRunner<void> configureRunner({MockClient? httpClient}) {
final PublishCheckCommand publishCheckCommand = PublishCheckCommand(
packagesDir,
processRunner: processRunner,
platform: mockPlatform,
gitDir: gitDir,
httpClient: httpClient,
);
runner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
runner.addCommand(publishCheckCommand);
return runner;
}
setUp(() {
mockPlatform = MockPlatform();
(:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform);
runner = configureRunner();
});
test('publish check all packages', () async {
final RepositoryPackage plugin1 = createFakePlugin(
'plugin_tools_test_package_a',
packagesDir,
examples: <String>[],
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin_tools_test_package_b',
packagesDir,
examples: <String>[],
);
await runCapturingPrint(runner, <String>['publish-check']);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'flutter',
const <String>['pub', 'publish', '--', '--dry-run'],
plugin1.path),
ProcessCall(
'flutter',
const <String>['pub', 'publish', '--', '--dry-run'],
plugin2.path),
]));
});
test('publish prepares dependencies of examples (when present)', () async {
final RepositoryPackage plugin1 = createFakePlugin(
'plugin_tools_test_package_a',
packagesDir,
examples: <String>['example1', 'example2'],
);
final RepositoryPackage plugin2 = createFakePlugin(
'plugin_tools_test_package_b',
packagesDir,
examples: <String>[],
);
await runCapturingPrint(runner, <String>['publish-check']);
// For plugin1, these are the expected pub get calls that will happen
final Iterable<ProcessCall> pubGetCalls =
plugin1.getExamples().map((RepositoryPackage example) {
return ProcessCall(
getFlutterCommand(mockPlatform),
const <String>['pub', 'get'],
example.path,
);
});
expect(pubGetCalls, hasLength(2));
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
// plugin1 has 2 examples, so there's some 'dart pub get' calls.
...pubGetCalls,
ProcessCall(
'flutter',
const <String>['pub', 'publish', '--', '--dry-run'],
plugin1.path),
// plugin2 has no examples, so there's no extra 'dart pub get' calls.
ProcessCall(
'flutter',
const <String>['pub', 'publish', '--', '--dry-run'],
plugin2.path),
]),
);
});
test('fail on negative test', () async {
createFakePlugin('plugin_tools_test_package_a', packagesDir);
processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(), <String>['pub', 'get']),
FakeProcessInfo(MockProcess(exitCode: 1, stdout: 'Some error from pub'),
<String>['pub', 'publish'])
];
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['publish-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Some error from pub'),
contains('Unable to publish plugin_tools_test_package_a'),
]),
);
});
test('fail on bad pubspec', () async {
final RepositoryPackage package = createFakePlugin('c', packagesDir);
await package.pubspecFile.writeAsString('bad-yaml');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['publish-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('No valid pubspec found.'),
]),
);
});
test('fails if AUTHORS is missing', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.authorsFile.deleteSync();
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['publish-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'No AUTHORS file found. Packages must include an AUTHORS file.'),
]),
);
});
test('does not require AUTHORS for third-party', () async {
final RepositoryPackage package = createFakePackage(
'a_package',
packagesDir.parent
.childDirectory('third_party')
.childDirectory('packages'));
package.authorsFile.deleteSync();
final List<String> output =
await runCapturingPrint(runner, <String>['publish-check']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for a_package'),
]),
);
});
test('pass on prerelease if --allow-pre-release flag is on', () async {
createFakePlugin('d', packagesDir);
final MockProcess process = MockProcess(
exitCode: 1,
stdout: 'Package has 1 warning.\n'
'Packages with an SDK constraint on a pre-release of the Dart '
'SDK should themselves be published as a pre-release version.');
processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(), <String>['pub', 'get']),
FakeProcessInfo(process, <String>['pub', 'publish']),
];
expect(
runCapturingPrint(
runner, <String>['publish-check', '--allow-pre-release']),
completes);
});
test('fail on prerelease if --allow-pre-release flag is off', () async {
createFakePlugin('d', packagesDir);
final MockProcess process = MockProcess(
exitCode: 1,
stdout: 'Package has 1 warning.\n'
'Packages with an SDK constraint on a pre-release of the Dart '
'SDK should themselves be published as a pre-release version.');
processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(), <String>['pub', 'get']),
FakeProcessInfo(process, <String>['pub', 'publish']),
];
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['publish-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Packages with an SDK constraint on a pre-release of the Dart SDK'),
contains('Unable to publish d'),
]),
);
});
test('Success message on stderr is not printed as an error', () async {
createFakePlugin('d', packagesDir);
processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(), <String>['pub', 'get']),
FakeProcessInfo(MockProcess(stdout: 'Package has 0 warnings.'),
<String>['pub', 'publish']),
];
final List<String> output =
await runCapturingPrint(runner, <String>['publish-check']);
expect(output, isNot(contains(contains('ERROR:'))));
});
test(
'runs validation even for packages that are already published and reports failure',
() async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, version: '0.1.0');
final MockClient mockClient = MockClient((http.Request request) async {
if (request.url.pathSegments.last == 'a_package.json') {
return http.Response(
json.encode(<String, dynamic>{
'name': 'a_package',
'versions': <String>[
'0.0.1',
'0.1.0',
],
}),
200);
}
return http.Response('', 500);
});
runner = configureRunner(httpClient: mockClient);
processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(exitCode: 1, stdout: 'Some error from pub'),
<String>['pub', 'publish'])
];
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['publish-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Unable to publish a_package'),
]),
);
expect(
processRunner.recordedCalls,
contains(
ProcessCall(
'flutter',
const <String>['pub', 'publish', '--', '--dry-run'],
package.path),
));
});
test('skips packages that are marked as not for publishing', () async {
createFakePackage('a_package', packagesDir,
version: '0.1.0', publishTo: 'none');
final List<String> output =
await runCapturingPrint(runner, <String>['publish-check']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING: Package is marked as unpublishable.'),
]),
);
expect(processRunner.recordedCalls, isEmpty);
});
test(
'runs validation even for packages that are already published and reports success',
() async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, version: '0.1.0');
final MockClient mockClient = MockClient((http.Request request) async {
if (request.url.pathSegments.last == 'a_package.json') {
return http.Response(
json.encode(<String, dynamic>{
'name': 'a_package',
'versions': <String>[
'0.0.1',
'0.1.0',
],
}),
200);
}
return http.Response('', 500);
});
runner = configureRunner(httpClient: mockClient);
final List<String> output =
await runCapturingPrint(runner, <String>['publish-check']);
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Package a_package version: 0.1.0 has already been published on pub.'),
]),
);
expect(
processRunner.recordedCalls,
contains(
ProcessCall(
'flutter',
const <String>['pub', 'publish', '--', '--dry-run'],
package.path),
));
});
group('pre-publish script', () {
test('runs if present', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
package.prePublishScript.createSync(recursive: true);
final List<String> output = await runCapturingPrint(runner, <String>[
'publish-check',
]);
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('runs before publish --dry-run', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
package.prePublishScript.createSync(recursive: true);
final List<String> output = await runCapturingPrint(runner, <String>[
'publish-check',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running pre-publish hook tool/pre_publish.dart...'),
]),
);
expect(
processRunner.recordedCalls,
containsAllInOrder(<ProcessCall>[
ProcessCall(
'dart',
const <String>[
'run',
'tool/pre_publish.dart',
],
package.directory.path),
ProcessCall(
'flutter',
const <String>[
'pub',
'publish',
'--',
'--dry-run',
],
package.directory.path),
]));
});
test('causes command failure if it fails', () async {
final RepositoryPackage package = createFakePackage(
'a_package', 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(runner, <String>[
'publish-check',
], 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(mockPlatform),
const <String>[
'pub',
'get',
],
package.directory.path),
ProcessCall(
'dart',
const <String>[
'run',
'tool/pre_publish.dart',
],
package.directory.path),
]));
});
});
test(
'--machine: Log JSON with status:no-publish and correct human message, if there are no packages need to be published. ',
() async {
const Map<String, dynamic> httpResponseA = <String, dynamic>{
'name': 'a',
'versions': <String>[
'0.0.1',
'0.1.0',
],
};
const Map<String, dynamic> httpResponseB = <String, dynamic>{
'name': 'b',
'versions': <String>[
'0.0.1',
'0.1.0',
'0.2.0',
],
};
final MockClient mockClient = MockClient((http.Request request) async {
if (request.url.pathSegments.last == 'no_publish_a.json') {
return http.Response(json.encode(httpResponseA), 200);
} else if (request.url.pathSegments.last == 'no_publish_b.json') {
return http.Response(json.encode(httpResponseB), 200);
}
return http.Response('', 500);
});
runner = configureRunner(httpClient: mockClient);
createFakePlugin('no_publish_a', packagesDir, version: '0.1.0');
createFakePlugin('no_publish_b', packagesDir, version: '0.2.0');
final List<String> output = await runCapturingPrint(
runner, <String>['publish-check', '--machine']);
expect(output.first, r'''
{
"status": "no-publish",
"humanMessage": [
"\n============================================================\n|| Running for no_publish_a\n============================================================\n",
"Running pub publish --dry-run:",
"Package no_publish_a version: 0.1.0 has already been published on pub.",
"\n============================================================\n|| Running for no_publish_b\n============================================================\n",
"Running pub publish --dry-run:",
"Package no_publish_b version: 0.2.0 has already been published on pub.",
"\n",
"------------------------------------------------------------",
"Run overview:",
" no_publish_a - ran",
" no_publish_b - ran",
"",
"Ran for 2 package(s)",
"\n",
"No issues found!"
]
}''');
});
test(
'--machine: Log JSON with status:needs-publish and correct human message, if there is at least 1 plugin needs to be published.',
() async {
const Map<String, dynamic> httpResponseA = <String, dynamic>{
'name': 'a',
'versions': <String>[
'0.0.1',
'0.1.0',
],
};
const Map<String, dynamic> httpResponseB = <String, dynamic>{
'name': 'b',
'versions': <String>[
'0.0.1',
'0.1.0',
],
};
final MockClient mockClient = MockClient((http.Request request) async {
if (request.url.pathSegments.last == 'no_publish_a.json') {
return http.Response(json.encode(httpResponseA), 200);
} else if (request.url.pathSegments.last == 'no_publish_b.json') {
return http.Response(json.encode(httpResponseB), 200);
}
return http.Response('', 500);
});
runner = configureRunner(httpClient: mockClient);
createFakePlugin('no_publish_a', packagesDir, version: '0.1.0');
createFakePlugin('no_publish_b', packagesDir, version: '0.2.0');
final List<String> output = await runCapturingPrint(
runner, <String>['publish-check', '--machine']);
expect(output.first, r'''
{
"status": "needs-publish",
"humanMessage": [
"\n============================================================\n|| Running for no_publish_a\n============================================================\n",
"Running pub publish --dry-run:",
"Package no_publish_a version: 0.1.0 has already been published on pub.",
"\n============================================================\n|| Running for no_publish_b\n============================================================\n",
"Running pub publish --dry-run:",
"Package no_publish_b is able to be published.",
"\n",
"------------------------------------------------------------",
"Run overview:",
" no_publish_a - ran",
" no_publish_b - ran",
"",
"Ran for 2 package(s)",
"\n",
"No issues found!"
]
}''');
});
test(
'--machine: Log correct JSON, if there is at least 1 plugin contains error.',
() async {
const Map<String, dynamic> httpResponseA = <String, dynamic>{
'name': 'a',
'versions': <String>[
'0.0.1',
'0.1.0',
],
};
const Map<String, dynamic> httpResponseB = <String, dynamic>{
'name': 'b',
'versions': <String>[
'0.0.1',
'0.1.0',
],
};
final MockClient mockClient = MockClient((http.Request request) async {
print('url ${request.url}');
print(request.url.pathSegments.last);
if (request.url.pathSegments.last == 'no_publish_a.json') {
return http.Response(json.encode(httpResponseA), 200);
} else if (request.url.pathSegments.last == 'no_publish_b.json') {
return http.Response(json.encode(httpResponseB), 200);
}
return http.Response('', 500);
});
runner = configureRunner(httpClient: mockClient);
final RepositoryPackage plugin =
createFakePlugin('no_publish_a', packagesDir, version: '0.1.0');
createFakePlugin('no_publish_b', packagesDir, version: '0.2.0');
await plugin.pubspecFile.writeAsString('bad-yaml');
bool hasError = false;
final List<String> output = await runCapturingPrint(
runner, <String>['publish-check', '--machine'],
errorHandler: (Error error) {
expect(error, isA<ToolExit>());
hasError = true;
});
expect(hasError, isTrue);
expect(output.first, contains(r'''
{
"status": "error",
"humanMessage": [
"\n============================================================\n|| Running for no_publish_a\n============================================================\n",
"Failed to parse `pubspec.yaml` at /packages/no_publish_a/pubspec.yaml: ParsedYamlException:'''));
// This is split into two checks since the details of the YamlException
// aren't controlled by this package, so asserting its exact format would
// make the test fragile to irrelevant changes in those details.
expect(output.first, contains(r'''
"No valid pubspec found.",
"\n============================================================\n|| Running for no_publish_b\n============================================================\n",
"url https://pub.dev/packages/no_publish_b.json",
"no_publish_b.json",
"Running pub publish --dry-run:",
"Package no_publish_b is able to be published.",
"\n",
"The following packages had errors:",
" no_publish_a",
"See above for full details."
]
}'''));
});
});
}