diff --git a/dev/docs/analytics-footer.html b/dev/docs/analytics-footer.html new file mode 100644 index 0000000000..133ecf3bd3 --- /dev/null +++ b/dev/docs/analytics-footer.html @@ -0,0 +1,4 @@ + + + diff --git a/dev/docs/analytics-header.html b/dev/docs/analytics-header.html new file mode 100644 index 0000000000..bed0852361 --- /dev/null +++ b/dev/docs/analytics-header.html @@ -0,0 +1,7 @@ + + + diff --git a/dev/docs/analytics.html b/dev/docs/analytics.html deleted file mode 100644 index ee11d34b06..0000000000 --- a/dev/docs/analytics.html +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/dev/tools/create_api_docs.dart b/dev/tools/create_api_docs.dart index 87c556e98a..56f6c32695 100644 --- a/dev/tools/create_api_docs.dart +++ b/dev/tools/create_api_docs.dart @@ -518,9 +518,9 @@ class DartdocGenerator { final Version version = FlutterInformation.instance.getFlutterVersion(); // Verify which version of snippets and dartdoc we're using. - final ProcessResult snippetsResult = Process.runSync( - FlutterInformation.instance.getFlutterBinaryPath().path, + final ProcessResult snippetsResult = processManager.runSync( [ + FlutterInformation.instance.getFlutterBinaryPath().path, 'pub', 'global', 'list', @@ -574,13 +574,15 @@ class DartdocGenerator { '--header', docsRoot.childFile('styles.html').path, '--header', - docsRoot.childFile('analytics.html').path, + docsRoot.childFile('analytics-header.html').path, '--header', docsRoot.childFile('survey.html').path, '--header', docsRoot.childFile('snippets.html').path, '--header', docsRoot.childFile('opensearch.html').path, + '--footer', + docsRoot.childFile('analytics-footer.html').path, '--footer-text', packageRoot.childFile('footer.html').path, '--allow-warnings-in-packages', @@ -643,6 +645,7 @@ class DartdocGenerator { arguments: dartdocArgs, workingDirectory: packageRoot, environment: pubEnvironment, + processManager: processManager, )); printStream( process.stdout, @@ -669,7 +672,7 @@ class DartdocGenerator { } _sanityCheckDocs(); - checkForUnresolvedDirectives(publishRoot.childDirectory('flutter').path); + checkForUnresolvedDirectives(publishRoot.childDirectory('flutter')); _createIndexAndCleanup(); @@ -690,12 +693,13 @@ class DartdocGenerator { } } - /// Runs a sanity check by running a test. - void _sanityCheckDocs([Platform platform = const LocalPlatform()]) { + /// A subset of all generated doc files for [_sanityCheckDocs]. + @visibleForTesting + List get canaries { final Directory flutterDirectory = publishRoot.childDirectory('flutter'); final Directory widgetsDirectory = flutterDirectory.childDirectory('widgets'); - final List canaries = [ + return [ publishRoot.childDirectory('assets').childFile('overrides.css'), flutterDirectory.childDirectory('dart-io').childFile('File-class.html'), flutterDirectory.childDirectory('dart-ui').childFile('Canvas-class.html'), @@ -710,12 +714,19 @@ class DartdocGenerator { widgetsDirectory.childFile('Widget-class.html'), widgetsDirectory.childFile('Listener-class.html'), ]; + } + + /// Runs a sanity check by running a test. + void _sanityCheckDocs([Platform platform = const LocalPlatform()]) { for (final File canary in canaries) { if (!canary.existsSync()) { throw Exception('Missing "${canary.path}", which probably means the documentation failed to build correctly.'); } } // Make sure at least one example of each kind includes source code. + final Directory widgetsDirectory = publishRoot + .childDirectory('flutter') + .childDirectory('widgets'); // Check a "sample" example, any one will do. _sanityCheckExample( diff --git a/dev/tools/dartdoc_checker.dart b/dev/tools/dartdoc_checker.dart index 75fbd2be38..cbc47f8540 100644 --- a/dev/tools/dartdoc_checker.dart +++ b/dev/tools/dartdoc_checker.dart @@ -4,9 +4,31 @@ import 'dart:io'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; -/// Scans the dartdoc HTML output in the provided `htmlOutputPath` for +/// Makes sure that the path we were given contains some of the expected +/// libraries. +@visibleForTesting +const List dartdocDirectiveCanaryLibraries = [ + 'animation', + 'cupertino', + 'material', + 'widgets', + 'rendering', + 'flutter_driver', +]; + +/// Makes sure that the path we were given contains some of the expected +/// HTML files. +@visibleForTesting +const List dartdocDirectiveCanaryFiles = [ + 'Widget-class.html', + 'Material-class.html', + 'Canvas-class.html', +]; + +/// Scans the dartdoc HTML output in the provided `dartDocDir` for /// unresolved dartdoc directives (`{@foo x y}`). /// /// Dartdoc usually replaces those directives with other content. However, @@ -22,27 +44,14 @@ import 'package:path/path.dart' as path; /// ``` /// void foo({@required int bar}); /// ``` -void checkForUnresolvedDirectives(String htmlOutputPath) { - final Directory dartDocDir = Directory(htmlOutputPath); +void checkForUnresolvedDirectives(Directory dartDocDir) { if (!dartDocDir.existsSync()) { throw Exception('Directory with dartdoc output (${dartDocDir.path}) does not exist.'); } - // Makes sure that the path we were given contains some of the expected - // libraries and HTML files. - final List canaryLibraries = [ - 'animation', - 'cupertino', - 'material', - 'widgets', - 'rendering', - 'flutter_driver', - ]; - final List canaryFiles = [ - 'Widget-class.html', - 'Material-class.html', - 'Canvas-class.html', - ]; + // Make a copy since this will be mutated + final List canaryLibraries = dartdocDirectiveCanaryLibraries.toList(); + final List canaryFiles = dartdocDirectiveCanaryFiles.toList(); print('Scanning for unresolved dartdoc directives...'); @@ -112,5 +121,5 @@ void main(List args) { if (!Directory(args.single).existsSync()) { throw Exception('The dartdoc HTML output directory ${args.single} does not exist.'); } - checkForUnresolvedDirectives(args.single); + checkForUnresolvedDirectives(Directory(args.single)); } diff --git a/dev/tools/test/create_api_docs_test.dart b/dev/tools/test/create_api_docs_test.dart index 6d6b1e804d..811885e13e 100644 --- a/dev/tools/test/create_api_docs_test.dart +++ b/dev/tools/test/create_api_docs_test.dart @@ -10,6 +10,7 @@ import 'package:test/test.dart'; import '../../../packages/flutter_tools/test/src/fake_process_manager.dart'; import '../create_api_docs.dart' as apidocs; +import '../dartdoc_checker.dart'; void main() { group('FlutterInformation', () { @@ -223,6 +224,235 @@ void main() { expect(info['engineRealm'], equals('realm')); }); }); + + group('DartDocGenerator', () { + late apidocs.DartdocGenerator generator; + late MemoryFileSystem fs; + late FakeProcessManager processManager; + late Directory publishRoot; + + setUp(() { + fs = MemoryFileSystem.test(); + publishRoot = fs.directory('/path/to/publish'); + processManager = FakeProcessManager.empty(); + generator = apidocs.DartdocGenerator( + packageRoot: fs.directory('/path/to/package'), + publishRoot: publishRoot, + docsRoot: fs.directory('/path/to/docs'), + filesystem: fs, + processManager: processManager, + ); + final Directory repoRoot = fs.directory('/flutter'); + repoRoot.childDirectory('packages').createSync(recursive: true); + apidocs.FlutterInformation.instance = apidocs.FlutterInformation( + filesystem: fs, + processManager: processManager, + platform: FakePlatform(environment: { + 'FLUTTER_ROOT': repoRoot.path, + }), + ); + }); + + test('.generateDartDoc() invokes dartdoc with the correct command line arguments', () async { + processManager.addCommands([ + const FakeCommand(command: ['/flutter/bin/flutter', 'pub', 'get']), + const FakeCommand( + command: ['/flutter/bin/flutter', '--version', '--machine'], + stdout: testVersionInfo, + ), + const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + ), + const FakeCommand( + command: ['git', 'rev-parse', 'HEAD'], + ), + const FakeCommand( + command: ['/flutter/bin/flutter', 'pub', 'global', 'list'], + ), + FakeCommand( + command: [ + '/flutter/bin/flutter', + 'pub', + 'global', + 'run', + '--enable-asserts', + 'dartdoc', + '--output', + '/path/to/publish/flutter', + '--allow-tools', + '--json', + '--validate-links', + '--link-to-source-excludes', + '/flutter/bin/cache', + '--link-to-source-root', + '/flutter', + '--link-to-source-uri-template', + 'https://github.com/flutter/flutter/blob/master/%f%#L%l%', + '--inject-html', + '--use-base-href', + '--header', + '/path/to/docs/styles.html', + '--header', + '/path/to/docs/analytics-header.html', + '--header', + '/path/to/docs/survey.html', + '--header', + '/path/to/docs/snippets.html', + '--header', + '/path/to/docs/opensearch.html', + '--footer', + '/path/to/docs/analytics-footer.html', + '--footer-text', + '/path/to/package/footer.html', + '--allow-warnings-in-packages', + // match package names + RegExp(r'^(\w+,)+(\w+)$'), + '--exclude-packages', + RegExp(r'^(\w+,)+(\w+)$'), + '--exclude', + // match dart package URIs + RegExp(r'^([\w\/:.]+,)+([\w\/:.]+)$'), + '--favicon', + '/path/to/docs/favicon.ico', + '--package-order', + 'flutter,Dart,${apidocs.kPlatformIntegrationPackageName},flutter_test,flutter_driver', + '--auto-include-dependencies', + ], + ), + ]); + + // This will throw while sanity checking generated files, which is tested independently + await expectLater( + () => generator.generateDartdoc(), + throwsA( + isA().having( + (Exception e) => e.toString(), + 'message', + contains(RegExp(r'Missing .* which probably means the documentation failed to build correctly.')), + ), + ), + ); + + expect(processManager, hasNoRemainingExpectations); + }); + + test('sanity checks spot check generated files', () async { + processManager.addCommands([ + const FakeCommand(command: ['/flutter/bin/flutter', 'pub', 'get']), + const FakeCommand( + command: ['/flutter/bin/flutter', '--version', '--machine'], + stdout: testVersionInfo, + ), + const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + ), + const FakeCommand( + command: ['git', 'rev-parse', 'HEAD'], + ), + const FakeCommand( + command: ['/flutter/bin/flutter', 'pub', 'global', 'list'], + ), + FakeCommand( + command: [ + '/flutter/bin/flutter', + 'pub', + 'global', + 'run', + '--enable-asserts', + 'dartdoc', + '--output', + '/path/to/publish/flutter', + '--allow-tools', + '--json', + '--validate-links', + '--link-to-source-excludes', + '/flutter/bin/cache', + '--link-to-source-root', + '/flutter', + '--link-to-source-uri-template', + 'https://github.com/flutter/flutter/blob/master/%f%#L%l%', + '--inject-html', + '--use-base-href', + '--header', + '/path/to/docs/styles.html', + '--header', + '/path/to/docs/analytics-header.html', + '--header', + '/path/to/docs/survey.html', + '--header', + '/path/to/docs/snippets.html', + '--header', + '/path/to/docs/opensearch.html', + '--footer', + '/path/to/docs/analytics-footer.html', + '--footer-text', + '/path/to/package/footer.html', + '--allow-warnings-in-packages', + // match package names + RegExp(r'^(\w+,)+(\w+)$'), + '--exclude-packages', + RegExp(r'^(\w+,)+(\w+)$'), + '--exclude', + // match dart package URIs + RegExp(r'^([\w\/:.]+,)+([\w\/:.]+)$'), + '--favicon', + '/path/to/docs/favicon.ico', + '--package-order', + 'flutter,Dart,${apidocs.kPlatformIntegrationPackageName},flutter_test,flutter_driver', + '--auto-include-dependencies', + ], + onRun: () { + for (final File canary in generator.canaries) { + canary.createSync(recursive: true); + } + for (final String path in dartdocDirectiveCanaryFiles) { + publishRoot.childDirectory('flutter').childFile(path).createSync(recursive: true); + } + for (final String path in dartdocDirectiveCanaryLibraries) { + publishRoot.childDirectory('flutter').childDirectory(path).createSync(recursive: true); + } + publishRoot.childDirectory('flutter').childFile('index.html').createSync(); + + final Directory widgetsDir = publishRoot + .childDirectory('flutter') + .childDirectory('widgets') + ..createSync(recursive: true); + widgetsDir.childFile('showGeneralDialog.html').writeAsStringSync(''' +
+  
+    import 'package:flutter/material.dart';
+  
+
+''', + ); + expect(publishRoot.childDirectory('flutter').existsSync(), isTrue); + (widgetsDir + .childDirectory('ModalRoute') + ..createSync(recursive: true)) + .childFile('barrierColor.html') + .writeAsStringSync(''' +
+  
+    class FooClass {
+      Color get barrierColor => FooColor();
+    }
+  
+
+'''); + const String queryParams = 'split=1&run=true&sample_id=widgets.Listener.123&sample_channel=master&channel=master'; + widgetsDir.childFile('Listener-class.html').writeAsStringSync(''' + +'''); + } + ), + ]); + + await generator.generateDartdoc(); + }); + }); } const String branchName = 'stable';