| // Copyright 2013 The Flutter Authors | |
| // 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/validate_command.dart'; | |
| import 'package:git/git.dart'; | |
| import 'package:platform/platform.dart'; | |
| import 'package:test/test.dart'; | |
| import 'mocks.dart'; | |
| import 'util.dart'; | |
| void main() { | |
| late CommandRunner<void> runner; | |
| late NativePlatform mockPlatform; | |
| late Directory packagesDir; | |
| setUp(() { | |
| mockPlatform = createMockPlatform(); | |
| final RecordingProcessRunner processRunner; | |
| final GitDir gitDir; | |
| (:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = configureBaseCommandMocks( | |
| platform: mockPlatform, | |
| ); | |
| final command = ValidateCommand( | |
| packagesDir, | |
| processRunner: processRunner, | |
| platform: mockPlatform, | |
| gitDir: gitDir, | |
| targetedValidators: {Validator.readme}, | |
| ); | |
| 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>['validate']); | |
| expect( | |
| output, | |
| containsAll(<Matcher>[ | |
| contains(' Validating README.md...'), | |
| contains(' Validating example/README.md...'), | |
| contains(' Validating example/example1/README.md...'), | |
| contains(' Validating 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>['validate'], | |
| 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>['validate']); | |
| 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 subpackageName = 'special_test'; | |
| final RepositoryPackage miscSubpackage = createFakePackage(subpackageName, package.directory); | |
| miscSubpackage.readmeFile.deleteSync(); | |
| final List<String> output = await runCapturingPrint(runner, <String>['validate']); | |
| 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>['validate'], | |
| 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>['validate'], | |
| 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>['validate'], | |
| 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>['validate']); | |
| expect( | |
| output, | |
| containsAll(<Matcher>[ | |
| contains(' Validating README.md...'), | |
| contains(' Validating 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>['validate'], | |
| 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>['validate']); | |
| expect( | |
| output, | |
| containsAll(<Matcher>[ | |
| contains(' Validating README.md...'), | |
| contains(' Validating 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>['validate'], | |
| 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 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>['validate']); | |
| 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>['validate'], | |
| 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>['validate'], | |
| 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>['validate'], | |
| 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>['validate'], | |
| 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>['validate'], | |
| 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>['validate'], | |
| 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>['validate'], | |
| 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>['validate']); | |
| 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: | |
| <?code-excerpt "main.dart (SomeSection)"?> | |
| ``` dart | |
| A B C | |
| ``` | |
| '''); | |
| final List<String> output = await runCapturingPrint(runner, <String>['validate']); | |
| 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>['validate']); | |
| expect( | |
| output, | |
| containsAll(<Matcher>[contains('Running for a_package...'), contains('No issues found!')]), | |
| ); | |
| }); | |
| test('fails on missing excerpt tag', () 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>['validate'], | |
| 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'), | |
| ]), | |
| ); | |
| }); | |
| test('passes and warns for missing excerpt tag when opted out', () async { | |
| final RepositoryPackage package = createFakePackage('a_package', packagesDir); | |
| package.readmeFile.writeAsStringSync(''' | |
| Example: | |
| ```dart | |
| A B C | |
| ``` | |
| '''); | |
| package.ciConfigFile.writeAsStringSync('exempt_from_excerpts: true'); | |
| final List<String> output = await runCapturingPrint(runner, <String>['validate']); | |
| expect( | |
| output, | |
| containsAll(<Matcher>[ | |
| contains('Running for a_package...'), | |
| contains('Skipping code excerpt validation due to ci_config.yaml'), | |
| ]), | |
| ); | |
| }); | |
| }); | |
| } |