// 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 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( '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: ['example1', 'example2']); for (final RepositoryPackage example in package.getExamples()) { example.readmeFile.writeAsStringSync('A readme'); } getExampleDir(package).childFile('README.md').writeAsStringSync('A readme'); final List output = await runCapturingPrint(runner, ['readme-check']); expect( output, containsAll([ 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 output = await runCapturingPrint( runner, ['readme-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Missing README.md'), ]), ); }); test('passes when example README is missing', () async { createFakePackage('a_package', packagesDir); final List output = await runCapturingPrint(runner, ['readme-check']); expect( output, containsAllInOrder([ 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.delete(); final List output = await runCapturingPrint(runner, ['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://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. '''); Error? commandError; final List output = await runCapturingPrint( runner, ['readme-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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 output = await runCapturingPrint( runner, ['readme-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('The boilerplate section about getting started with Flutter ' 'should not be left in.'), contains('Contains template boilerplate'), ]), ); }); test( 'fails when multi-example top-level example directory README still has ' 'application template boilerplate', () async { final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, examples: ['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 output = await runCapturingPrint( runner, ['readme-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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 output = await runCapturingPrint(runner, [ 'readme-check', ]); expect( output, containsAll([ 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 output = await runCapturingPrint( runner, ['readme-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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 output = await runCapturingPrint( runner, ['readme-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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 output = await runCapturingPrint( runner, ['readme-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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: { 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 output = await runCapturingPrint( runner, ['readme-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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: { 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 output = await runCapturingPrint( runner, ['readme-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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: { 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 output = await runCapturingPrint( runner, ['readme-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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 output = await runCapturingPrint( runner, ['readme-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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 output = await runCapturingPrint(runner, [ 'readme-check', ]); expect( output, containsAll([ 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 output = await runCapturingPrint(runner, [ 'readme-check', ]); expect( output, containsAll([ contains('Running for a_package...'), contains('No issues found!'), ]), ); }); test('passes when excerpt requirement is met', () async { final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, extraFiles: [kReadmeExcerptConfigPath], ); package.readmeFile.writeAsStringSync(''' Example: ```dart A B C ``` '''); final List output = await runCapturingPrint( runner, ['readme-check', '--require-excerpts']); expect( output, containsAll([ contains('Running for a_package...'), contains('No issues found!'), ]), ); }); test('fails when excerpts are used but the package is not configured', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir); package.readmeFile.writeAsStringSync(''' Example: ```dart A B C ``` '''); Error? commandError; final List output = await runCapturingPrint( runner, ['readme-check', '--require-excerpts'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('code-excerpt tag found, but the package is not configured ' 'for excerpting. Follow the instructions at\n' 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages\n' 'for setting up a build.excerpt.yaml file.'), contains('Missing code-excerpt configuration'), ]), ); }); 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 output = await runCapturingPrint( runner, ['readme-check', '--require-excerpts'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ 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/wiki/Contributing-to-Plugins-and-Packages'), contains('Missing code-excerpt management for code block'), ]), ); }); }); }