Add supported OS version tables to READMEs (#5106)

This commit is contained in:
stuartmorgan
2022-04-04 15:26:13 -04:00
committed by GitHub
parent b532e1233b
commit b0567c3fcc
6 changed files with 446 additions and 0 deletions

View File

@ -1,5 +1,6 @@
## NEXT
- Adds a new `readme-check` command.
- Updates `publish-plugin` command documentation.
## 0.8.2

View File

@ -48,6 +48,9 @@ class RepositoryPackage {
/// The package's top-level pubspec.yaml.
File get pubspecFile => directory.childFile('pubspec.yaml');
/// The package's top-level README.
File get readmeFile => directory.childFile('README.md');
late final Pubspec _parsedPubspec =
Pubspec.parse(pubspecFile.readAsStringSync());
@ -62,6 +65,12 @@ class RepositoryPackage {
directory.parent.basename != 'packages' &&
directory.basename.startsWith(directory.parent.basename);
/// True if this appears to be the app-facing package of a federated plugin,
/// according to repository conventions.
bool get isAppFacing =>
directory.parent.basename != 'packages' &&
directory.basename == directory.parent.basename;
/// True if this appears to be a platform interface package, according to
/// repository conventions.
bool get isPlatformInterface =>

View File

@ -26,6 +26,7 @@ import 'native_test_command.dart';
import 'publish_check_command.dart';
import 'publish_plugin_command.dart';
import 'pubspec_check_command.dart';
import 'readme_check_command.dart';
import 'test_command.dart';
import 'version_check_command.dart';
import 'xcode_analyze_command.dart';
@ -65,6 +66,7 @@ void main(List<String> args) {
..addCommand(PublishCheckCommand(packagesDir))
..addCommand(PublishPluginCommand(packagesDir))
..addCommand(PubspecCheckCommand(packagesDir))
..addCommand(ReadmeCheckCommand(packagesDir))
..addCommand(TestCommand(packagesDir))
..addCommand(VersionCheckCommand(packagesDir))
..addCommand(XcodeAnalyzeCommand(packagesDir));

View File

@ -0,0 +1,152 @@
// 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:file/file.dart';
import 'package:git/git.dart';
import 'package:platform/platform.dart';
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:yaml/yaml.dart';
import 'common/core.dart';
import 'common/package_looping_command.dart';
import 'common/process_runner.dart';
import 'common/repository_package.dart';
/// A command to enforce README conventions across the repository.
class ReadmeCheckCommand extends PackageLoopingCommand {
/// Creates an instance of the README check command.
ReadmeCheckCommand(
Directory packagesDir, {
ProcessRunner processRunner = const ProcessRunner(),
Platform platform = const LocalPlatform(),
GitDir? gitDir,
}) : super(
packagesDir,
processRunner: processRunner,
platform: platform,
gitDir: gitDir,
);
// Standardized capitalizations for platforms that a plugin can support.
static const Map<String, String> _standardPlatformNames = <String, String>{
'android': 'Android',
'ios': 'iOS',
'linux': 'Linux',
'macos': 'macOS',
'web': 'Web',
'windows': 'Windows',
};
@override
final String name = 'readme-check';
@override
final String description =
'Checks that READMEs follow repository conventions.';
@override
bool get hasLongOutput => false;
@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
final File readme = package.readmeFile;
if (!readme.existsSync()) {
return PackageResult.fail(<String>['Missing README.md']);
}
final List<String> errors = <String>[];
final Pubspec pubspec = package.parsePubspec();
final bool isPlugin = pubspec.flutter?['plugin'] != null;
if (isPlugin && (!package.isFederated || package.isAppFacing)) {
final String? error = _validateSupportedPlatforms(package, pubspec);
if (error != null) {
errors.add(error);
}
}
return errors.isEmpty
? PackageResult.success()
: PackageResult.fail(errors);
}
/// Validates that the plugin has a supported platforms table following the
/// expected format, returning an error string if any issues are found.
String? _validateSupportedPlatforms(
RepositoryPackage package, Pubspec pubspec) {
final List<String> contents = package.readmeFile.readAsLinesSync();
// Example table following expected format:
// | | Android | iOS | Web |
// |----------------|---------|----------|------------------------|
// | **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] |
final int detailsLineNumber =
contents.indexWhere((String line) => line.startsWith('| **Support**'));
if (detailsLineNumber == -1) {
return 'No OS support table found';
}
final int osLineNumber = detailsLineNumber - 2;
if (osLineNumber < 0 || !contents[osLineNumber].startsWith('|')) {
return 'OS support table does not have the expected header format';
}
// Utility method to convert an iterable of strings to a case-insensitive
// sorted, comma-separated string of its elements.
String sortedListString(Iterable<String> entries) {
final List<String> entryList = entries.toList();
entryList.sort(
(String a, String b) => a.toLowerCase().compareTo(b.toLowerCase()));
return entryList.join(', ');
}
// Validate that the supported OS lists match.
final dynamic platformsEntry = pubspec.flutter!['plugin']!['platforms'];
if (platformsEntry == null) {
logWarning('Plugin not support any platforms');
return null;
}
final YamlMap platformSupportMaps = platformsEntry as YamlMap;
final Set<String> actuallySupportedPlatform =
platformSupportMaps.keys.toSet().cast<String>();
final Iterable<String> documentedPlatforms = contents[osLineNumber]
.split('|')
.map((String entry) => entry.trim())
.where((String entry) => entry.isNotEmpty);
final Set<String> documentedPlatformsLowercase =
documentedPlatforms.map((String entry) => entry.toLowerCase()).toSet();
if (actuallySupportedPlatform.length != documentedPlatforms.length ||
actuallySupportedPlatform
.intersection(documentedPlatformsLowercase)
.length !=
actuallySupportedPlatform.length) {
printError('''
${indentation}OS support table does not match supported platforms:
${indentation * 2}Actual: ${sortedListString(actuallySupportedPlatform)}
${indentation * 2}Documented: ${sortedListString(documentedPlatformsLowercase)}
''');
return 'Incorrect OS support table';
}
// Enforce a standard set of capitalizations for the OS headings.
final Iterable<String> incorrectCapitalizations = documentedPlatforms
.toSet()
.difference(_standardPlatformNames.values.toSet());
if (incorrectCapitalizations.isNotEmpty) {
final Iterable<String> expectedVersions = incorrectCapitalizations
.map((String name) => _standardPlatformNames[name.toLowerCase()]!);
printError('''
${indentation}Incorrect OS capitalization: ${sortedListString(incorrectCapitalizations)}
${indentation * 2}Please use standard capitalizations: ${sortedListString(expectedVersions)}
''');
return 'Incorrect OS support formatting';
}
// TODO(stuartmorgan): Add validation that the minimums in the table are
// consistent with what the current implementations require. See
// https://github.com/flutter/flutter/issues/84200
return null;
}
}

View File

@ -126,6 +126,7 @@ void main() {
test('all return false for a simple plugin', () {
final Directory plugin = createFakePlugin('a_plugin', packagesDir);
expect(RepositoryPackage(plugin).isFederated, false);
expect(RepositoryPackage(plugin).isAppFacing, false);
expect(RepositoryPackage(plugin).isPlatformInterface, false);
expect(RepositoryPackage(plugin).isFederated, false);
});
@ -134,6 +135,7 @@ void main() {
final Directory plugin =
createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin'));
expect(RepositoryPackage(plugin).isFederated, true);
expect(RepositoryPackage(plugin).isAppFacing, true);
expect(RepositoryPackage(plugin).isPlatformInterface, false);
expect(RepositoryPackage(plugin).isPlatformImplementation, false);
});
@ -142,6 +144,7 @@ void main() {
final Directory plugin = createFakePlugin('a_plugin_platform_interface',
packagesDir.childDirectory('a_plugin'));
expect(RepositoryPackage(plugin).isFederated, true);
expect(RepositoryPackage(plugin).isAppFacing, false);
expect(RepositoryPackage(plugin).isPlatformInterface, true);
expect(RepositoryPackage(plugin).isPlatformImplementation, false);
});
@ -152,6 +155,7 @@ void main() {
final Directory plugin = createFakePlugin(
'a_plugin_foo', packagesDir.childDirectory('a_plugin'));
expect(RepositoryPackage(plugin).isFederated, true);
expect(RepositoryPackage(plugin).isAppFacing, false);
expect(RepositoryPackage(plugin).isPlatformInterface, false);
expect(RepositoryPackage(plugin).isPlatformImplementation, true);
});

View File

@ -0,0 +1,278 @@
// 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:file/memory.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:test/test.dart';
import 'mocks.dart';
import 'util.dart';
void main() {
late CommandRunner<void> runner;
late RecordingProcessRunner processRunner;
late FileSystem fileSystem;
late MockPlatform mockPlatform;
late Directory packagesDir;
setUp(() {
fileSystem = MemoryFileSystem();
mockPlatform = MockPlatform();
packagesDir = fileSystem.currentDirectory.childDirectory('packages');
createPackagesDirectory(parentDir: packagesDir.parent);
processRunner = RecordingProcessRunner();
final ReadmeCheckCommand command = ReadmeCheckCommand(
packagesDir,
processRunner: processRunner,
platform: mockPlatform,
);
runner = CommandRunner<void>(
'readme_check_command', 'Test for readme_check_command');
runner.addCommand(command);
});
test('fails when README is missing', () async {
createFakePackage('a_package', 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('Missing README.md'),
]),
);
});
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);
final List<Directory> packageDirectories = <Directory>[
// 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),
];
for (final Directory package in packageDirectories) {
package.childFile('README.md').writeAsStringSync('''
A very useful package.
''');
}
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 {
final Directory pluginDir = createFakePlugin('a_plugin', packagesDir);
pluginDir.childFile('README.md').writeAsStringSync('''
A very useful 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 when app-facing part of a federated plugin is missing an OS support table',
() async {
final Directory pluginDir =
createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin'));
pluginDir.childFile('README.md').writeAsStringSync('''
A very useful 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 Directory pluginDir = createFakePlugin('a_plugin', packagesDir);
pluginDir.childFile('README.md').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 Directory pluginDir = createFakePlugin(
'a_plugin',
packagesDir,
platformSupport: <String, PlatformDetails>{
platformAndroid: const PlatformDetails(PlatformSupport.inline),
platformIOS: const PlatformDetails(PlatformSupport.inline),
platformWeb: const PlatformDetails(PlatformSupport.inline),
},
);
pluginDir.childFile('README.md').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 Directory pluginDir = createFakePlugin(
'a_plugin',
packagesDir,
platformSupport: <String, PlatformDetails>{
platformAndroid: const PlatformDetails(PlatformSupport.inline),
platformIOS: const PlatformDetails(PlatformSupport.inline),
},
);
pluginDir.childFile('README.md').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 Directory pluginDir = 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),
},
);
pluginDir.childFile('README.md').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'),
]),
);
});
});
}