Files
packages/script/tool/test/publish_check_command_test.dart
stuartmorgan 74cf0287f9 [flutter_plugin_tools] Improve process mocking (#4254)
The mock process runner used in most of the tests had poor handling of
stdout/stderr:
- By default it would return the `List<int>` output from the mock
  process, which was never correct since the parent process runner
  interface always sets encodings, thus the `dynamic` should always be
  of type `String`
- The process for setting output on a `MockProcess` was awkward, since
  it was based on a `Stream<Lint<int>>`, even though in every case what
  we actually want to do is just set the full output to a string.
- A hack was at some point added (presumably due to the above issues)
  to bypass that flow at the process runner level, and instead return a
  `String` set there. That meant there were two ways of setting output
  (one of which that only worked for one of the ways of running
  processes)
  - That hack wasn't updated when the ability to return multiple mock
    processes instead of a single global mock process was added, so the
    API was even more confusing, and there was no way to set different
    output for different processes.

This changes the test APIs so that:
- `MockProcess` takes stdout and stderr as strings, and internally
  manages converting them to a `Stream<List<int>>`.
- `RecordingProcessRunner` correctly decodes and returns the output
  streams when constructing a process result.

It also removes the resultStdout and resultStderr hacks, as well as the
legacy `processToReturn` API, and converts all uses to the new
structure, which is both simpler to use, and clearly associates output
with specific processes.
2021-08-24 11:42:21 -07:00

359 lines
12 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 'dart:io' as io;
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/publish_check_command.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', () {
FileSystem fileSystem;
late MockPlatform mockPlatform;
late Directory packagesDir;
late RecordingProcessRunner processRunner;
late CommandRunner<void> runner;
setUp(() {
fileSystem = MemoryFileSystem();
mockPlatform = MockPlatform();
packagesDir = createPackagesDirectory(fileSystem: fileSystem);
processRunner = RecordingProcessRunner();
final PublishCheckCommand publishCheckCommand = PublishCheckCommand(
packagesDir,
processRunner: processRunner,
platform: mockPlatform,
);
runner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
runner.addCommand(publishCheckCommand);
});
test('publish check all packages', () async {
final Directory plugin1Dir =
createFakePlugin('plugin_tools_test_package_a', packagesDir);
final Directory plugin2Dir =
createFakePlugin('plugin_tools_test_package_b', packagesDir);
processRunner.mockProcessesForExecutable['flutter'] = <io.Process>[
MockProcess(),
MockProcess(),
];
await runCapturingPrint(runner, <String>['publish-check']);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'flutter',
const <String>['pub', 'publish', '--', '--dry-run'],
plugin1Dir.path),
ProcessCall(
'flutter',
const <String>['pub', 'publish', '--', '--dry-run'],
plugin2Dir.path),
]));
});
test('fail on negative test', () async {
createFakePlugin('plugin_tools_test_package_a', packagesDir);
processRunner.mockProcessesForExecutable['flutter'] = <io.Process>[
MockProcess(exitCode: 1)
];
expect(
() => runCapturingPrint(runner, <String>['publish-check']),
throwsA(isA<ToolExit>()),
);
});
test('fail on bad pubspec', () async {
final Directory dir = createFakePlugin('c', packagesDir);
await dir.childFile('pubspec.yaml').writeAsString('bad-yaml');
processRunner.mockProcessesForExecutable['flutter'] = <io.Process>[
MockProcess(),
];
expect(() => runCapturingPrint(runner, <String>['publish-check']),
throwsA(isA<ToolExit>()));
});
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'] = <io.Process>[
process,
];
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'] = <io.Process>[
process,
];
expect(runCapturingPrint(runner, <String>['publish-check']),
throwsA(isA<ToolExit>()));
});
test('Success message on stderr is not printed as an error', () async {
createFakePlugin('d', packagesDir);
processRunner.mockProcessesForExecutable['flutter'] = <io.Process>[
MockProcess(stdout: 'Package has 0 warnings.'),
];
final List<String> output =
await runCapturingPrint(runner, <String>['publish-check']);
expect(output, isNot(contains(contains('ERROR:'))));
});
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);
});
final PublishCheckCommand command = PublishCheckCommand(packagesDir,
processRunner: processRunner, httpClient: mockClient);
runner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
runner.addCommand(command);
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",
"Package no_publish_a version: 0.1.0 has already be published on pub.",
"\n============================================================\n|| Running for no_publish_b\n============================================================\n",
"Package no_publish_b version: 0.2.0 has already be 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);
});
final PublishCheckCommand command = PublishCheckCommand(packagesDir,
processRunner: processRunner, httpClient: mockClient);
runner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
runner.addCommand(command);
createFakePlugin('no_publish_a', packagesDir, version: '0.1.0');
createFakePlugin('no_publish_b', packagesDir, version: '0.2.0');
processRunner.mockProcessesForExecutable['flutter'] = <io.Process>[
MockProcess(),
];
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",
"Package no_publish_a version: 0.1.0 has already be 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);
});
final PublishCheckCommand command = PublishCheckCommand(packagesDir,
processRunner: processRunner, httpClient: mockClient);
runner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
runner.addCommand(command);
final Directory plugin1Dir =
createFakePlugin('no_publish_a', packagesDir, version: '0.1.0');
createFakePlugin('no_publish_b', packagesDir, version: '0.2.0');
await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml');
processRunner.mockProcessesForExecutable['flutter'] = <io.Process>[
MockProcess(),
];
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 pubspec",
"\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."
]
}'''));
});
});
}