Files
packages/script/tool/test/readme_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

706 lines
21 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 '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/common/plugin_utils.dart';
import 'package:flutter_plugin_tools/src/readme_check_command.dart';
import 'package:git/git.dart';
import 'package:test/test.dart';
import 'mocks.dart';
import 'util.dart';
void main() {
late CommandRunner<void> runner;
late MockPlatform mockPlatform;
late Directory packagesDir;
setUp(() {
mockPlatform = MockPlatform();
final RecordingProcessRunner processRunner;
final GitDir gitDir;
(:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform);
final ReadmeCheckCommand command = ReadmeCheckCommand(
packagesDir,
processRunner: processRunner,
platform: mockPlatform,
gitDir: gitDir,
);
runner = CommandRunner<void>(
'readme_check_command', 'Test for readme_check_command');
runner.addCommand(command);
});
test('prints paths of checked READMEs', () async {
final RepositoryPackage package = createFakePackage(
'a_package', packagesDir,
examples: <String>['example1', 'example2']);
for (final RepositoryPackage example in package.getExamples()) {
example.readmeFile.writeAsStringSync('A readme');
}
getExampleDir(package).childFile('README.md').writeAsStringSync('A readme');
final List<String> output =
await runCapturingPrint(runner, <String>['readme-check']);
expect(
output,
containsAll(<Matcher>[
contains(' Checking README.md...'),
contains(' Checking example/README.md...'),
contains(' Checking example/example1/README.md...'),
contains(' Checking example/example2/README.md...'),
]),
);
});
test('fails when package README is missing', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.readmeFile.deleteSync();
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Missing README.md'),
]),
);
});
test('passes when example README is missing', () async {
createFakePackage('a_package', packagesDir);
final List<String> output =
await runCapturingPrint(runner, <String>['readme-check']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('No README for example'),
]),
);
});
test('does not inculde non-example subpackages', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
const String subpackageName = 'special_test';
final RepositoryPackage miscSubpackage =
createFakePackage(subpackageName, package.directory);
miscSubpackage.readmeFile.deleteSync();
final List<String> output =
await runCapturingPrint(runner, <String>['readme-check']);
expect(output, isNot(contains(subpackageName)));
});
test('fails when README still has plugin template boilerplate', () async {
final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir);
package.readmeFile.writeAsStringSync('''
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/developing-packages/),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The boilerplate section about getting started with Flutter '
'should not be left in.'),
contains('Contains template boilerplate'),
]),
);
});
test('fails when example README still has application template boilerplate',
() async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.getExamples().first.readmeFile.writeAsStringSync('''
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The boilerplate section about getting started with Flutter '
'should not be left in.'),
contains('Contains template boilerplate'),
]),
);
});
test(
'fails when a plugin implementation package example README has the '
'template boilerplate', () async {
final RepositoryPackage package = createFakePlugin(
'a_plugin_ios', packagesDir.childDirectory('a_plugin'));
package.getExamples().first.readmeFile.writeAsStringSync('''
# a_plugin_ios_example
Demonstrates how to use the a_plugin_ios plugin.
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The boilerplate should not be left in for a federated plugin '
"implementation package's example."),
contains('Contains template boilerplate'),
]),
);
});
test(
'allows the template boilerplate in the example README for packages '
'other than plugin implementation packages', () async {
final RepositoryPackage package = createFakePlugin(
'a_plugin',
packagesDir.childDirectory('a_plugin'),
platformSupport: <String, PlatformDetails>{
platformAndroid: const PlatformDetails(PlatformSupport.inline),
},
);
// Write a README with an OS support table so that the main README check
// passes.
package.readmeFile.writeAsStringSync('''
# a_plugin
| | Android |
|----------------|---------|
| **Support** | SDK 19+ |
A great plugin.
''');
package.getExamples().first.readmeFile.writeAsStringSync('''
# a_plugin_example
Demonstrates how to use the a_plugin plugin.
''');
final List<String> output =
await runCapturingPrint(runner, <String>['readme-check']);
expect(
output,
containsAll(<Matcher>[
contains(' Checking README.md...'),
contains(' Checking example/README.md...'),
]),
);
});
test(
'fails when a plugin implementation package example README does not have '
'the repo-standard message', () async {
final RepositoryPackage package = createFakePlugin(
'a_plugin_ios', packagesDir.childDirectory('a_plugin'));
package.getExamples().first.readmeFile.writeAsStringSync('''
# a_plugin_ios_example
Some random description.
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The example README for a platform implementation package '
'should warn readers about its intended use. Please copy the '
'example README from another implementation package in this '
'repository.'),
contains('Missing implementation package example warning'),
]),
);
});
test('passes for a plugin implementation package with the expected content',
() async {
final RepositoryPackage package = createFakePlugin(
'a_plugin',
packagesDir.childDirectory('a_plugin'),
platformSupport: <String, PlatformDetails>{
platformAndroid: const PlatformDetails(PlatformSupport.inline),
},
);
// Write a README with an OS support table so that the main README check
// passes.
package.readmeFile.writeAsStringSync('''
# a_plugin
| | Android |
|----------------|---------|
| **Support** | SDK 19+ |
A great plugin.
''');
package.getExamples().first.readmeFile.writeAsStringSync('''
# Platform Implementation Test App
This is a test app for manual testing and automated integration testing
of this platform implementation. It is not intended to demonstrate actual use of
this package, since the intent is that plugin clients use the app-facing
package.
Unless you are making changes to this implementation package, this example is
very unlikely to be relevant.
''');
final List<String> output =
await runCapturingPrint(runner, <String>['readme-check']);
expect(
output,
containsAll(<Matcher>[
contains(' Checking README.md...'),
contains(' Checking example/README.md...'),
]),
);
});
test(
'fails when multi-example top-level example directory README still has '
'application template boilerplate', () async {
final RepositoryPackage package = createFakePackage(
'a_package', packagesDir,
examples: <String>['example1', 'example2']);
package.directory
.childDirectory('example')
.childFile('README.md')
.writeAsStringSync('''
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The boilerplate section about getting started with Flutter '
'should not be left in.'),
contains('Contains template boilerplate'),
]),
);
});
group('plugin OS support', () {
test(
'does not check support table for anything other than app-facing plugin packages',
() async {
const String federatedPluginName = 'a_federated_plugin';
final Directory federatedDir =
packagesDir.childDirectory(federatedPluginName);
// A non-plugin package.
createFakePackage('a_package', packagesDir);
// Non-app-facing parts of a federated plugin.
createFakePlugin(
'${federatedPluginName}_platform_interface', federatedDir);
createFakePlugin('${federatedPluginName}_android', federatedDir);
final List<String> output = await runCapturingPrint(runner, <String>[
'readme-check',
]);
expect(
output,
containsAll(<Matcher>[
contains('Running for a_package...'),
contains('Running for a_federated_plugin_platform_interface...'),
contains('Running for a_federated_plugin_android...'),
contains('No issues found!'),
]),
);
});
test('fails when non-federated plugin is missing an OS support table',
() async {
createFakePlugin('a_plugin', packagesDir);
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('No OS support table found'),
]),
);
});
test(
'fails when app-facing part of a federated plugin is missing an OS support table',
() async {
createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin'));
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('No OS support table found'),
]),
);
});
test('fails the OS support table is missing the header', () async {
final RepositoryPackage plugin =
createFakePlugin('a_plugin', packagesDir);
plugin.readmeFile.writeAsStringSync('''
A very useful plugin.
| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] |
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('OS support table does not have the expected header format'),
]),
);
});
test('fails if the OS support table is missing a supported OS', () async {
final RepositoryPackage plugin = createFakePlugin(
'a_plugin',
packagesDir,
platformSupport: <String, PlatformDetails>{
platformAndroid: const PlatformDetails(PlatformSupport.inline),
platformIOS: const PlatformDetails(PlatformSupport.inline),
platformWeb: const PlatformDetails(PlatformSupport.inline),
},
);
plugin.readmeFile.writeAsStringSync('''
A very useful plugin.
| | Android | iOS |
|----------------|---------|----------|
| **Support** | SDK 21+ | iOS 10+* |
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(' OS support table does not match supported platforms:\n'
' Actual: android, ios, web\n'
' Documented: android, ios'),
contains('Incorrect OS support table'),
]),
);
});
test('fails if the OS support table lists an extra OS', () async {
final RepositoryPackage plugin = createFakePlugin(
'a_plugin',
packagesDir,
platformSupport: <String, PlatformDetails>{
platformAndroid: const PlatformDetails(PlatformSupport.inline),
platformIOS: const PlatformDetails(PlatformSupport.inline),
},
);
plugin.readmeFile.writeAsStringSync('''
A very useful plugin.
| | Android | iOS | Web |
|----------------|---------|----------|------------------------|
| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] |
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(' OS support table does not match supported platforms:\n'
' Actual: android, ios\n'
' Documented: android, ios, web'),
contains('Incorrect OS support table'),
]),
);
});
test('fails if the OS support table has unexpected OS formatting',
() async {
final RepositoryPackage plugin = createFakePlugin(
'a_plugin',
packagesDir,
platformSupport: <String, PlatformDetails>{
platformAndroid: const PlatformDetails(PlatformSupport.inline),
platformIOS: const PlatformDetails(PlatformSupport.inline),
platformMacOS: const PlatformDetails(PlatformSupport.inline),
platformWeb: const PlatformDetails(PlatformSupport.inline),
},
);
plugin.readmeFile.writeAsStringSync('''
A very useful plugin.
| | android | ios | MacOS | web |
|----------------|---------|----------|-------|------------------------|
| **Support** | SDK 21+ | iOS 10+* | 10.11 | [See `camera_web `][1] |
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(' Incorrect OS capitalization: android, ios, MacOS, web\n'
' Please use standard capitalizations: Android, iOS, macOS, Web\n'),
contains('Incorrect OS support formatting'),
]),
);
});
});
group('code blocks', () {
test('fails on missing info string', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.readmeFile.writeAsStringSync('''
Example:
```
void main() {
// ...
}
```
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Code block at line 3 is missing a language identifier.'),
contains('Missing language identifier for code block'),
]),
);
});
test('allows unknown info strings', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.readmeFile.writeAsStringSync('''
Example:
```someunknowninfotag
A B C
```
''');
final List<String> output = await runCapturingPrint(runner, <String>[
'readme-check',
]);
expect(
output,
containsAll(<Matcher>[
contains('Running for a_package...'),
contains('No issues found!'),
]),
);
});
test('allows space around info strings', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.readmeFile.writeAsStringSync('''
Example:
``` dart
A B C
```
''');
final List<String> output = await runCapturingPrint(runner, <String>[
'readme-check',
]);
expect(
output,
containsAll(<Matcher>[
contains('Running for a_package...'),
contains('No issues found!'),
]),
);
});
test('passes when excerpt requirement is met', () async {
final RepositoryPackage package = createFakePackage(
'a_package',
packagesDir,
);
package.readmeFile.writeAsStringSync('''
Example:
<?code-excerpt "main.dart (SomeSection)"?>
```dart
A B C
```
''');
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check', '--require-excerpts']);
expect(
output,
containsAll(<Matcher>[
contains('Running for a_package...'),
contains('No issues found!'),
]),
);
});
test('fails on missing excerpt tag when requested', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.readmeFile.writeAsStringSync('''
Example:
```dart
A B C
```
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['readme-check', '--require-excerpts'],
errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Dart code block at line 3 is not managed by code-excerpt.'),
// Ensure that the failure message links to instructions.
contains(
'https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md'),
contains('Missing code-excerpt management for code block'),
]),
);
});
});
}