| // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io' hide ProcessException; |
| |
| import 'package:analyzer/file_system/physical_file_system.dart'; |
| import 'package:crypto/crypto.dart' as crypto; |
| import 'package:dartdoc/src/io_utils.dart'; |
| import 'package:dartdoc/src/package_meta.dart'; |
| import 'package:grinder/grinder.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:yaml/yaml.dart' as yaml; |
| |
| import 'subprocess_launcher.dart'; |
| |
| void main(List<String> args) => grind(args); |
| |
| /// Thrown on failure to find something in a file. |
| class GrindTestFailure { |
| final String message; |
| |
| GrindTestFailure(this.message); |
| |
| @override |
| String toString() => message; |
| } |
| |
| /// Kind of an inefficient grepper for now. |
| void expectFileContains(String path, List<Pattern> items) { |
| var source = File(path); |
| if (!source.existsSync()) { |
| throw GrindTestFailure('file not found: $path'); |
| } |
| for (var item in items) { |
| if (!File(path).readAsStringSync().contains(item)) { |
| throw GrindTestFailure('"$item" not found in $path'); |
| } |
| } |
| } |
| |
| /// Enable the following experiments for language tests. |
| final List<String> languageExperiments = |
| (Platform.environment['LANGUAGE_EXPERIMENTS'] ?? '').split(RegExp(r'\s+')); |
| |
| /// The pub cache inherited by grinder. |
| final String defaultPubCache = Platform.environment['PUB_CACHE'] ?? |
| p.context.resolveTildePath('~/.pub-cache'); |
| |
| /// Run no more than the number of processors available in parallel. |
| final TaskQueue testFutures = TaskQueue( |
| maxJobs: int.tryParse(Platform.environment['MAX_TEST_FUTURES'] ?? '') ?? |
| Platform.numberOfProcessors); |
| |
| // Directory.systemTemp is not a constant. So wrap it. |
| Directory createTempSync(String prefix) => |
| Directory.systemTemp.createTempSync(prefix); |
| |
| /// Global so that the lock is retained for the life of the process. |
| Future<void>? _lockFuture; |
| Completer<FlutterRepo>? _cleanFlutterRepo; |
| |
| /// Returns true if we need to replace the existing flutter. We never release |
| /// this lock until the program exits to prevent edge case runs from |
| /// spontaneously deciding to download a new Flutter SDK in the middle of a run. |
| // TODO(srawlins): The above comment is outdated. |
| Future<FlutterRepo> get cleanFlutterRepo async { |
| var repoCompleter = _cleanFlutterRepo; |
| if (repoCompleter != null) { |
| return repoCompleter.future; |
| } |
| |
| // No await is allowed between check of _cleanFlutterRepo and its assignment, |
| // to prevent reentering this function. |
| repoCompleter = Completer(); |
| |
| // Figure out where the repository is supposed to be and lock updates for |
| // it. |
| await cleanFlutterDir.parent.create(recursive: true); |
| assert(_lockFuture == null); |
| _lockFuture = File(p.join(cleanFlutterDir.parent.path, 'lock')) |
| .openSync(mode: FileMode.write) |
| .lock(); |
| await _lockFuture; |
| var lastSynced = File(p.join(cleanFlutterDir.parent.path, 'lastSynced')); |
| var newRepo = FlutterRepo.fromPath(cleanFlutterDir.path, {}, 'clean'); |
| |
| // We have a repository, but is it up to date? |
| DateTime? lastSyncedTime; |
| if (lastSynced.existsSync()) { |
| lastSyncedTime = DateTime.fromMillisecondsSinceEpoch( |
| int.parse(lastSynced.readAsStringSync())); |
| } |
| if (lastSyncedTime == null || |
| DateTime.now().difference(lastSyncedTime) > Duration(hours: 24)) { |
| // Rebuild the repository. |
| if (cleanFlutterDir.existsSync()) { |
| cleanFlutterDir.deleteSync(recursive: true); |
| } |
| cleanFlutterDir.createSync(recursive: true); |
| await newRepo._init(); |
| await lastSynced |
| .writeAsString((DateTime.now()).millisecondsSinceEpoch.toString()); |
| } |
| repoCompleter.complete(newRepo); |
| _cleanFlutterRepo = repoCompleter; |
| return repoCompleter.future; |
| } |
| |
| final String _dartdocDocsPath = createTempSync('dartdoc').path; |
| |
| final Directory _sdkDocsDir = createTempSync('sdkdocs').absolute; |
| |
| Directory cleanFlutterDir = Directory( |
| p.join(p.context.resolveTildePath('~/.dartdoc_grinder'), 'cleanFlutter')); |
| |
| final Directory _flutterDir = createTempSync('flutter'); |
| |
| final Directory _languageTestPackageDir = |
| createTempSync('languageTestPackageDir'); |
| |
| Directory get testPackage => Directory(p.joinAll(['testing', 'test_package'])); |
| |
| Directory get testPackageExperiments => |
| Directory(p.joinAll(['testing', 'test_package_experiments'])); |
| |
| Directory get testPackageFlutterPlugin => Directory( |
| p.joinAll(['testing', 'flutter_packages', 'test_package_flutter_plugin'])); |
| |
| final Directory _testPackageDocsDir = createTempSync('test_package'); |
| |
| final Directory _testPackageExperimentsDocsDir = |
| createTempSync('test_package_experiments'); |
| |
| final String _pluginPackageDocsPath = |
| createTempSync('test_package_flutter_plugin').path; |
| |
| /// Version of dartdoc we should use when making comparisons. |
| String get dartdocOriginalBranch { |
| var branch = Platform.environment['DARTDOC_ORIGINAL']; |
| if (branch == null) { |
| return 'master'; |
| } else { |
| log('using branch/tag: $branch for comparison from \$DARTDOC_ORIGINAL'); |
| return branch; |
| } |
| } |
| |
| final _whitespacePattern = RegExp(r'\s+'); |
| |
| final List<String> _extraDartdocParameters = [ |
| ...?Platform.environment['DARTDOC_PARAMS']?.split(_whitespacePattern), |
| ]; |
| |
| final Directory flutterDirDevTools = |
| Directory(p.join(_flutterDir.path, 'dev', 'tools')); |
| |
| /// Creates a throwaway pub cache and returns the environment variables |
| /// necessary to use it. |
| Map<String, String> _createThrowawayPubCache() { |
| var pubCache = Directory.systemTemp.createTempSync('pubcache'); |
| var pubCacheBin = Directory(p.join(pubCache.path, 'bin')); |
| var defaultCache = Directory(defaultPubCache); |
| if (defaultCache.existsSync()) { |
| copy(defaultCache, pubCache); |
| } else { |
| pubCacheBin.createSync(); |
| } |
| return Map.fromIterables([ |
| 'PUB_CACHE', |
| 'PATH' |
| ], [ |
| pubCache.path, |
| [pubCacheBin.path, Platform.environment['PATH']].join(':') |
| ]); |
| } |
| |
| @Task('Analyze dartdoc to ensure there are no errors and warnings') |
| @Depends(analyzeTestPackages) |
| void analyze() async { |
| await SubprocessLauncher('analyze').runStreamed( |
| Platform.resolvedExecutable, |
| ['analyze', '--fatal-infos', '.'], |
| ); |
| } |
| |
| @Task('Analyze the test packages') |
| void analyzeTestPackages() async { |
| var testPackagePaths = [testPackage.path]; |
| if (Platform.version.contains('dev')) { |
| testPackagePaths.add(testPackageExperiments.path); |
| } |
| for (var testPackagePath in testPackagePaths) { |
| await SubprocessLauncher('pub-get').runStreamed( |
| Platform.resolvedExecutable, |
| ['pub', 'get'], |
| workingDirectory: testPackagePath, |
| ); |
| await SubprocessLauncher('analyze-test-package').runStreamed( |
| Platform.resolvedExecutable, |
| // TODO(srawlins): Analyze the whole directory by ignoring the pubspec |
| // reports. |
| ['analyze', 'lib'], |
| workingDirectory: testPackagePath, |
| ); |
| } |
| } |
| |
| @Task('Check for dart format cleanliness') |
| void checkFormat() async { |
| if (Platform.version.contains('dev')) { |
| var filesToFix = <String>[]; |
| // Filter out test packages as they always have strange formatting. |
| // Passing parameters to dart format for directories to search results in |
| // filenames being stripped of the dirname so we have to filter here. |
| void addFileToFix(String line) { |
| if (!line.startsWith('Changed ')) return; |
| var fileName = line.substring(8); |
| var pathComponents = p.split(fileName); |
| if (pathComponents.isNotEmpty && pathComponents.first == 'testing') { |
| return; |
| } |
| filesToFix.add(fileName); |
| } |
| |
| log('Validating dart format with version ${Platform.version}'); |
| await SubprocessLauncher('dart format').runStreamed( |
| Platform.resolvedExecutable, |
| [ |
| 'format', |
| '-o', |
| 'none', |
| '.', |
| ], |
| perLine: addFileToFix); |
| if (filesToFix.isNotEmpty) { |
| fail( |
| 'dart format found files needing reformatting. Use this command to reformat:\n' |
| 'dart format ${filesToFix.map((f) => "'$f'").join(' ')}'); |
| } |
| } else { |
| log('Skipping dart format check, requires latest dev version of SDK'); |
| } |
| } |
| |
| @Task('Run quick presubmit checks.') |
| @Depends( |
| analyze, |
| checkFormat, |
| checkBuild, |
| tryPublish, |
| smokeTest, |
| ) |
| void presubmit() {} |
| |
| @Task('Run long tests, self-test dartdoc, and run the publish test') |
| @Depends(presubmit, longTest, testDartdoc) |
| void buildbot() {} |
| |
| @Task('Run buildbot tests, but without publish test') |
| @Depends(analyze, checkFormat, checkBuild, smokeTest, longTest, testDartdoc) |
| void buildbotNoPublish() {} |
| |
| @Task('Generate docs for the Dart SDK') |
| Future<void> buildSdkDocs() async { |
| log('building SDK docs'); |
| await _buildSdkDocs(_sdkDocsDir.path, Future.value(Directory.current.path)); |
| } |
| |
| class WarningsCollection { |
| final String tempDir; |
| final Map<String, int> warningKeyCounts; |
| final String branch; |
| final String? pubCachePath; |
| |
| WarningsCollection(this.tempDir, this.pubCachePath, this.branch) |
| : warningKeyCounts = {}; |
| |
| static const String kPubCachePathReplacement = '_xxxPubDirectoryxxx_'; |
| static const String kTempDirReplacement = '_xxxTempDirectoryxxx_'; |
| |
| String _toKey(String text) { |
| var key = text.replaceAll(tempDir, kTempDirReplacement); |
| var pubCachePath = this.pubCachePath; |
| if (pubCachePath != null) { |
| key = key.replaceAll(pubCachePath, kPubCachePathReplacement); |
| } |
| return key; |
| } |
| |
| String _fromKey(String text) { |
| var key = text.replaceAll(kTempDirReplacement, tempDir); |
| if (pubCachePath != null) { |
| key = key.replaceAll(kPubCachePathReplacement, pubCachePath!); |
| } |
| return key; |
| } |
| |
| void add(String text) { |
| var key = _toKey(text); |
| warningKeyCounts.update(key, (e) => e + 1, ifAbsent: () => 1); |
| } |
| |
| /// Output formatter for comparing warnings. [this] is the original. |
| String getPrintableWarningDelta(String title, WarningsCollection current) { |
| var printBuffer = StringBuffer(); |
| var quantityChangedOuts = <String>{}; |
| var onlyOriginal = <String>{}; |
| var onlyCurrent = <String>{}; |
| var identical = <String>{}; |
| var allKeys = <String>{ |
| ...warningKeyCounts.keys, |
| ...current.warningKeyCounts.keys |
| }; |
| |
| for (var key in allKeys) { |
| if (warningKeyCounts.containsKey(key) && |
| !current.warningKeyCounts.containsKey(key)) { |
| onlyOriginal.add(key); |
| } else if (!warningKeyCounts.containsKey(key) && |
| current.warningKeyCounts.containsKey(key)) { |
| onlyCurrent.add(key); |
| } else if (warningKeyCounts.containsKey(key) && |
| current.warningKeyCounts.containsKey(key) && |
| warningKeyCounts[key] != current.warningKeyCounts[key]) { |
| quantityChangedOuts.add(key); |
| } else { |
| identical.add(key); |
| } |
| } |
| |
| if (onlyOriginal.isNotEmpty) { |
| printBuffer.writeln( |
| '*** $title : ${onlyOriginal.length} warnings from $branch, missing in ${current.branch}:'); |
| for (var key in onlyOriginal) { |
| printBuffer.writeln(_fromKey(key)); |
| } |
| } |
| if (onlyCurrent.isNotEmpty) { |
| printBuffer.writeln( |
| '*** $title : ${onlyCurrent.length} new warnings in ${current.branch}, missing in $branch'); |
| for (var key in onlyCurrent) { |
| printBuffer.writeln(current._fromKey(key)); |
| } |
| } |
| if (quantityChangedOuts.isNotEmpty) { |
| printBuffer.writeln('*** $title : Identical warning quantity changed'); |
| for (var key in quantityChangedOuts) { |
| printBuffer.writeln( |
| '* Appeared ${warningKeyCounts[key]} times in $branch, ${current.warningKeyCounts[key]} in ${current.branch}:'); |
| printBuffer.writeln(current._fromKey(key)); |
| } |
| } |
| if (onlyOriginal.isEmpty && |
| onlyCurrent.isEmpty && |
| quantityChangedOuts.isEmpty) { |
| printBuffer.writeln( |
| '*** $title : No difference in warning output from $branch to ${current.branch}${allKeys.isEmpty ? "" : " (${allKeys.length} warnings found)"}'); |
| } else if (identical.isNotEmpty) { |
| printBuffer.writeln( |
| '*** $title : Difference in warning output found for ${allKeys.length - identical.length} warnings (${allKeys.length} warnings found)"'); |
| } |
| return printBuffer.toString(); |
| } |
| } |
| |
| /// Returns a map of warning texts to the number of times each has been seen. |
| WarningsCollection jsonMessageIterableToWarnings( |
| Iterable<Map<Object, Object?>> messageIterable, |
| String tempPath, |
| String? pubDir, |
| String branch) { |
| var warningTexts = WarningsCollection(tempPath, pubDir, branch); |
| for (final message in messageIterable) { |
| if (message.containsKey('level') && |
| message['level'] == 'WARNING' && |
| message.containsKey('data')) { |
| var data = message['data'] as Map; |
| warningTexts.add(data['text']); |
| } |
| } |
| return warningTexts; |
| } |
| |
| @Task('Display delta in SDK warnings') |
| Future<void> compareSdkWarnings() async { |
| var originalDartdocSdkDocs = |
| Directory.systemTemp.createTempSync('dartdoc-comparison-sdkdocs'); |
| var originalDartdoc = createComparisonDartdoc(); |
| var currentDartdocSdkBuild = _buildSdkDocs( |
| _sdkDocsDir.path, Future.value(Directory.current.path), 'current'); |
| var originalDartdocSdkBuild = |
| _buildSdkDocs(originalDartdocSdkDocs.path, originalDartdoc, 'original'); |
| var currentDartdocWarnings = jsonMessageIterableToWarnings( |
| await currentDartdocSdkBuild, _sdkDocsDir.path, null, 'HEAD'); |
| var originalDartdocWarnings = jsonMessageIterableToWarnings( |
| await originalDartdocSdkBuild, |
| originalDartdocSdkDocs.absolute.path, |
| null, |
| dartdocOriginalBranch); |
| |
| print(originalDartdocWarnings.getPrintableWarningDelta( |
| 'SDK docs', currentDartdocWarnings)); |
| } |
| |
| /// Helper function to create a clean version of dartdoc (based on the current |
| /// directory, assumed to be a git repository). Uses [dartdocOriginalBranch] |
| /// to checkout a branch or tag. |
| Future<String> createComparisonDartdoc() async { |
| var launcher = SubprocessLauncher('create-comparison-dartdoc'); |
| var dartdocClean = Directory.systemTemp.createTempSync('dartdoc-comparison'); |
| await launcher |
| .runStreamed('git', ['clone', Directory.current.path, dartdocClean.path]); |
| await launcher.runStreamed('git', ['checkout', dartdocOriginalBranch], |
| workingDirectory: dartdocClean.path); |
| await launcher.runStreamed(Platform.resolvedExecutable, ['pub', 'get'], |
| workingDirectory: dartdocClean.path); |
| return dartdocClean.path; |
| } |
| |
| /// Creates a clean version of dartdoc (based on the current directory, assumed |
| /// to be a git repository), configured to use packages from the Dart SDK. |
| /// |
| /// This copy of dartdoc depends on the HEAD versions of various packages |
| /// developed within the SDK, such as 'analyzer', '_fe_analyzer_shared', |
| /// and 'meta'. |
| Future<String> createSdkDartdoc() async { |
| var launcher = SubprocessLauncher('create-sdk-dartdoc'); |
| var dartdocSdk = Directory.systemTemp.createTempSync('dartdoc-sdk'); |
| await launcher |
| .runStreamed('git', ['clone', Directory.current.path, dartdocSdk.path]); |
| await launcher.runStreamed('git', ['checkout'], |
| workingDirectory: dartdocSdk.path); |
| |
| var sdkClone = Directory.systemTemp.createTempSync('sdk-checkout'); |
| await launcher.runStreamed('git', [ |
| 'clone', |
| '--branch', |
| 'master', |
| '--depth', |
| '1', |
| 'https://dart.googlesource.com/sdk.git', |
| sdkClone.path |
| ]); |
| var dartdocPubspec = File(p.join(dartdocSdk.path, 'pubspec.yaml')); |
| var pubspecLines = await dartdocPubspec.readAsLines(); |
| var pubspecLinesFiltered = <String>[]; |
| for (var line in pubspecLines) { |
| if (line.startsWith('dependency_overrides:')) { |
| pubspecLinesFiltered.add('#dependency_overrides:'); |
| } else { |
| pubspecLinesFiltered.add(line); |
| } |
| } |
| |
| await dartdocPubspec.writeAsString(pubspecLinesFiltered.join('\n')); |
| dartdocPubspec.writeAsStringSync(''' |
| |
| dependency_overrides: |
| analyzer: |
| path: '${sdkClone.path}/pkg/analyzer' |
| _fe_analyzer_shared: |
| path: '${sdkClone.path}/pkg/_fe_analyzer_shared' |
| meta: |
| path: '${sdkClone.path}/pkg/meta' |
| ''', mode: FileMode.append); |
| await launcher.runStreamed(Platform.resolvedExecutable, ['pub', 'get'], |
| workingDirectory: dartdocSdk.path); |
| return dartdocSdk.path; |
| } |
| |
| @Task('Run grind tasks with the analyzer SDK.') |
| Future<void> testWithAnalyzerSdk() async { |
| var launcher = SubprocessLauncher('test-with-analyzer-sdk'); |
| // Do not override meta on branches outside of stable. |
| var sdkDartdoc = await createSdkDartdoc(); |
| var defaultGrindParameter = |
| Platform.environment['DARTDOC_GRIND_STEP'] ?? 'test'; |
| // TODO(srawlins): Re-enable sdk-analyzer when dart_style is published using |
| // analyzer 3.0.0. |
| try { |
| await launcher.runStreamed( |
| Platform.resolvedExecutable, ['run', 'grinder', defaultGrindParameter], |
| workingDirectory: sdkDartdoc); |
| } catch (e, st) { |
| print('Warning: SDK analyzer job threw "$e":\n$st'); |
| } |
| } |
| |
| Future<Iterable<Map<String, Object?>>> _buildSdkDocs( |
| String sdkDocsPath, Future<String> futureCwd, |
| [String label = '']) async { |
| if (label != '') label = '-$label'; |
| var launcher = SubprocessLauncher('build-sdk-docs$label'); |
| var cwd = await futureCwd; |
| await launcher.runStreamed(Platform.resolvedExecutable, ['pub', 'get'], |
| workingDirectory: cwd); |
| return await launcher.runStreamed( |
| Platform.resolvedExecutable, |
| [ |
| '--enable-asserts', |
| p.join('bin', 'dartdoc.dart'), |
| '--output', |
| sdkDocsPath, |
| '--sdk-docs', |
| '--json', |
| '--show-progress', |
| ..._extraDartdocParameters, |
| ], |
| workingDirectory: cwd); |
| } |
| |
| Future<Iterable<Map<String, Object?>>> _buildTestPackageDocs( |
| String outputDir, String cwd, |
| {List<String> params = const [], |
| String label = '', |
| String? testPackagePath}) async { |
| if (label != '') label = '-$label'; |
| testPackagePath ??= testPackage.absolute.path; |
| var launcher = SubprocessLauncher('build-test-package-docs$label'); |
| var testPackagePubGet = launcher.runStreamed( |
| Platform.resolvedExecutable, ['pub', 'get'], |
| workingDirectory: testPackagePath); |
| var dartdocPubGet = launcher.runStreamed( |
| Platform.resolvedExecutable, ['pub', 'get'], |
| workingDirectory: cwd); |
| await Future.wait([testPackagePubGet, dartdocPubGet]); |
| return await launcher.runStreamed( |
| Platform.resolvedExecutable, |
| [ |
| '--enable-asserts', |
| p.join(cwd, 'bin', 'dartdoc.dart'), |
| '--output', |
| outputDir, |
| '--example-path-prefix', |
| 'examples', |
| '--include-source', |
| '--json', |
| '--link-to-remote', |
| '--pretty-index-json', |
| ...params, |
| ..._extraDartdocParameters, |
| ], |
| workingDirectory: testPackagePath); |
| } |
| |
| @Task('Build generated test package docs from the experiment test package') |
| @Depends(clean) |
| Future<void> buildTestExperimentsPackageDocs() async { |
| await _buildTestPackageDocs( |
| _testPackageExperimentsDocsDir.absolute.path, Directory.current.path, |
| testPackagePath: testPackageExperiments.absolute.path, |
| params: [ |
| '--enable-experiment', |
| 'non-nullable,generic-metadata', |
| '--no-link-to-remote' |
| ]); |
| } |
| |
| @Task('Serve experimental test package on port 8003.') |
| @Depends(buildTestExperimentsPackageDocs) |
| Future<void> serveTestExperimentsPackageDocs() async { |
| await _serveDocsFrom(_testPackageExperimentsDocsDir.absolute.path, 8003, |
| 'test-package-docs-experiments'); |
| } |
| |
| @Task('Build test package docs (HTML) with inherited docs and source code') |
| @Depends(clean) |
| Future<void> buildTestPackageDocs() async { |
| await _buildTestPackageDocs( |
| _testPackageDocsDir.absolute.path, Directory.current.path); |
| } |
| |
| @Task('Build test package docs (Markdown) with inherited docs and source code') |
| @Depends(clean) |
| Future<void> buildTestPackageDocsMd() async { |
| await _buildTestPackageDocs( |
| _testPackageDocsDir.absolute.path, Directory.current.path, |
| params: ['--format', 'md']); |
| } |
| |
| @Task('Serve test package docs locally with dhttpd on port 8002') |
| @Depends(buildTestPackageDocs) |
| Future<void> serveTestPackageDocs() async { |
| await startTestPackageDocsServer(); |
| } |
| |
| @Task('Serve test package docs (in Markdown) locally with dhttpd on port 8002') |
| @Depends(buildTestPackageDocsMd) |
| Future<void> serveTestPackageDocsMd() async { |
| await startTestPackageDocsServer(); |
| } |
| |
| Future<void> startTestPackageDocsServer() async { |
| log('launching dhttpd on port 8002 for SDK'); |
| var launcher = SubprocessLauncher('serve-test-package-docs'); |
| await launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '8002', |
| '--path', |
| _testPackageDocsDir.absolute.path, |
| ]); |
| } |
| |
| bool _serveReady = false; |
| |
| Future<void> _serveDocsFrom(String servePath, int port, String context) async { |
| log('launching dhttpd on port $port for $context'); |
| var launcher = SubprocessLauncher(context); |
| if (!_serveReady) { |
| await launcher.runStreamed(Platform.resolvedExecutable, ['pub', 'get']); |
| await launcher.runStreamed( |
| Platform.resolvedExecutable, ['pub', 'global', 'activate', 'dhttpd']); |
| _serveReady = true; |
| } |
| await launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '$port', |
| '--path', |
| servePath |
| ]); |
| } |
| |
| @Task('Serve generated SDK docs locally with dhttpd on port 8000') |
| @Depends(buildSdkDocs) |
| Future<void> serveSdkDocs() async { |
| log('launching dhttpd on port 8000 for SDK'); |
| var launcher = SubprocessLauncher('serve-sdk-docs'); |
| await launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '8000', |
| '--path', |
| _sdkDocsDir.path, |
| ]); |
| } |
| |
| @Task('Compare warnings in Dartdoc for Flutter') |
| Future<void> compareFlutterWarnings() async { |
| var originalDartdocFlutter = |
| Directory.systemTemp.createTempSync('dartdoc-comparison-flutter'); |
| var originalDartdoc = createComparisonDartdoc(); |
| var envCurrent = _createThrowawayPubCache(); |
| var envOriginal = _createThrowawayPubCache(); |
| var currentDartdocFlutterBuild = _buildFlutterDocs(_flutterDir.path, |
| Future.value(Directory.current.path), envCurrent, 'docs-current'); |
| var originalDartdocFlutterBuild = _buildFlutterDocs( |
| originalDartdocFlutter.path, |
| originalDartdoc, |
| envOriginal, |
| 'docs-original'); |
| var currentDartdocWarnings = jsonMessageIterableToWarnings( |
| await currentDartdocFlutterBuild, |
| _flutterDir.absolute.path, |
| envCurrent['PUB_CACHE'], |
| 'HEAD'); |
| var originalDartdocWarnings = jsonMessageIterableToWarnings( |
| await originalDartdocFlutterBuild, |
| originalDartdocFlutter.absolute.path, |
| envOriginal['PUB_CACHE'], |
| dartdocOriginalBranch); |
| |
| print(originalDartdocWarnings.getPrintableWarningDelta( |
| 'Flutter repo', currentDartdocWarnings)); |
| |
| if (Platform.environment['SERVE_FLUTTER'] == '1') { |
| var launcher = SubprocessLauncher('serve-flutter-docs'); |
| await launcher.runStreamed(Platform.resolvedExecutable, ['pub', 'get']); |
| var original = launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '9000', |
| '--path', |
| p.join(originalDartdocFlutter.absolute.path, 'dev', 'docs', 'doc'), |
| ]); |
| var current = launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '9001', |
| '--path', |
| p.join(_flutterDir.absolute.path, 'dev', 'docs', 'doc'), |
| ]); |
| await Future.wait([original, current]); |
| } |
| } |
| |
| @Task('Serve generated Flutter docs locally with dhttpd on port 8001') |
| @Depends(buildFlutterDocs) |
| Future<void> serveFlutterDocs() async { |
| log('launching dhttpd on port 8001 for Flutter'); |
| var launcher = SubprocessLauncher('serve-flutter-docs'); |
| await launcher.runStreamed(Platform.resolvedExecutable, ['pub', 'get']); |
| await launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '8001', |
| '--path', |
| p.join(_flutterDir.path, 'dev', 'docs', 'doc'), |
| ]); |
| } |
| |
| @Task('Serve language test directory docs on port 8004') |
| @Depends(buildLanguageTestDocs) |
| Future<void> serveLanguageTestDocs() async { |
| log('launching dhttpd on port 8004 for language tests'); |
| var launcher = SubprocessLauncher('serve-language-test-docs'); |
| await launcher.runStreamed(Platform.resolvedExecutable, ['pub', 'get']); |
| await launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '8004', |
| '--path', |
| p.join(_languageTestPackageDir.path, 'doc', 'api'), |
| ]); |
| } |
| |
| @Task('Build docs for a language test directory in the SDK') |
| Future<void> buildLanguageTestDocs() async { |
| // The path to the base directory for language tests. |
| var languageTestPath = Platform.environment['LANGUAGE_TESTS']; |
| if (languageTestPath == null) { |
| fail( |
| 'LANGUAGE_TESTS must be set to the SDK language test directory from which to copy tests'); |
| } |
| var launcher = SubprocessLauncher('build-language-test-docs'); |
| var pubspecFile = File(p.join(_languageTestPackageDir.path, 'pubspec.yaml')); |
| pubspecFile.writeAsStringSync('''name: _language_test_package |
| version: 0.0.1 |
| environment: |
| sdk: '>=${Platform.version.split(' ').first}' |
| '''); |
| |
| var analyzerOptionsFile = |
| File(p.join(_languageTestPackageDir.path, 'analysis_options.yaml')); |
| var analyzerOptions = languageExperiments.map((e) => ' - $e').join('\n'); |
| analyzerOptionsFile.writeAsStringSync('''analyzer: |
| enable-experiment: |
| $analyzerOptions |
| '''); |
| |
| var libDir = Directory(p.join(_languageTestPackageDir.path, 'lib')) |
| ..createSync(); |
| var languageTestDir = Directory(p.context.resolveTildePath(languageTestPath)); |
| if (!languageTestDir.existsSync()) { |
| fail('language test dir does not exist: $languageTestDir'); |
| } |
| |
| for (var entry in languageTestDir.listSync(recursive: true)) { |
| if (entry is File && |
| entry.existsSync() && |
| !entry.path.endsWith('_error_test.dart') && |
| !entry.path.endsWith('_error_lib.dart')) { |
| var destDir = Directory(p.join( |
| libDir.path, |
| p.dirname(entry.absolute.path |
| .replaceFirst(languageTestDir.absolute.path + p.separator, '')))); |
| if (!destDir.existsSync()) destDir.createSync(recursive: true); |
| copy(entry, destDir); |
| } |
| } |
| |
| await launcher.runStreamed(Platform.resolvedExecutable, ['pub', 'get'], |
| workingDirectory: _languageTestPackageDir.absolute.path); |
| await launcher.runStreamed( |
| Platform.resolvedExecutable, |
| [ |
| '--enable-asserts', |
| p.join(Directory.current.absolute.path, 'bin', 'dartdoc.dart'), |
| '--json', |
| '--link-to-remote', |
| '--show-progress', |
| '--enable-experiment', |
| languageExperiments.join(','), |
| ..._extraDartdocParameters, |
| ], |
| workingDirectory: _languageTestPackageDir.absolute.path); |
| } |
| |
| @Task('Validate flutter docs') |
| @Depends(buildFlutterDocs, testDartdocFlutterPlugin) |
| void validateFlutterDocs() {} |
| |
| @Task('Build flutter docs') |
| Future<void> buildFlutterDocs() async { |
| log('building flutter docs into: $_flutterDir'); |
| var env = _createThrowawayPubCache(); |
| await _buildFlutterDocs( |
| _flutterDir.path, Future.value(Directory.current.path), env, 'docs'); |
| var indexContents = |
| File(p.join(_flutterDir.path, 'dev', 'docs', 'doc', 'index.html')) |
| .readAsLinesSync(); |
| stdout.write([...indexContents.take(25), '...\n'].join('\n')); |
| } |
| |
| /// A class wrapping a flutter SDK. |
| class FlutterRepo { |
| final String flutterPath; |
| final Map<String, String> env; |
| final String flutterCmd = p.join('bin', 'flutter'); |
| |
| final String cacheDart; |
| final SubprocessLauncher launcher; |
| |
| FlutterRepo._(this.flutterPath, this.env, this.cacheDart, this.launcher); |
| |
| Future<void> _init() async { |
| Directory(flutterPath).createSync(recursive: true); |
| await launcher.runStreamed( |
| 'git', ['clone', 'https://github.com/flutter/flutter.git', '.'], |
| workingDirectory: flutterPath); |
| await launcher.runStreamed( |
| flutterCmd, |
| ['--version'], |
| workingDirectory: flutterPath, |
| ); |
| await launcher.runStreamed( |
| flutterCmd, |
| ['update-packages'], |
| workingDirectory: flutterPath, |
| ); |
| } |
| |
| factory FlutterRepo.fromPath(String flutterPath, Map<String, String> env, |
| [String? label]) { |
| var cacheDart = |
| p.join(flutterPath, 'bin', 'cache', 'dart-sdk', 'bin', 'dart'); |
| env['PATH'] = |
| '${p.join(p.canonicalize(flutterPath), "bin")}:${env['PATH'] ?? Platform.environment['PATH']}'; |
| env['FLUTTER_ROOT'] = flutterPath; |
| var launcher = |
| SubprocessLauncher('flutter${label == null ? "" : "-$label"}', env); |
| return FlutterRepo._(flutterPath, env, cacheDart, launcher); |
| } |
| |
| /// Copy an existing, initialized flutter repo. |
| static Future<FlutterRepo> copyFromExistingFlutterRepo( |
| FlutterRepo origRepo, String flutterPath, Map<String, String> env, |
| [String? label]) async { |
| copy(Directory(origRepo.flutterPath), Directory(flutterPath)); |
| var flutterRepo = FlutterRepo.fromPath(flutterPath, env, label); |
| return flutterRepo; |
| } |
| |
| /// Doesn't actually copy the existing repo; use for read-only operations |
| /// only. |
| static Future<FlutterRepo> fromExistingFlutterRepo(FlutterRepo origRepo, |
| [String? label]) async { |
| return FlutterRepo.fromPath(origRepo.flutterPath, {}, label); |
| } |
| } |
| |
| Future<Iterable<Map<String, Object?>>> _buildFlutterDocs( |
| String flutterPath, Future<String> futureCwd, Map<String, String> env, |
| [String? label]) async { |
| var flutterRepo = await FlutterRepo.copyFromExistingFlutterRepo( |
| await cleanFlutterRepo, flutterPath, env, label); |
| await flutterRepo.launcher.runStreamed( |
| flutterRepo.cacheDart, |
| ['pub', 'get'], |
| workingDirectory: p.join(flutterPath, 'dev', 'tools'), |
| ); |
| await flutterRepo.launcher.runStreamed( |
| flutterRepo.cacheDart, |
| ['pub', 'global', 'activate', 'snippets'], |
| ); |
| // TODO(jcollins-g): flutter's dart SDK pub tries to precompile the universe |
| // when using -spath. Why? |
| await flutterRepo.launcher.runStreamed(flutterRepo.cacheDart, |
| ['pub', 'global', 'activate', '-spath', '.', '-x', 'dartdoc'], |
| workingDirectory: await futureCwd); |
| return await flutterRepo.launcher.runStreamed( |
| flutterRepo.cacheDart, |
| [p.join('dev', 'tools', 'dartdoc.dart'), '-c', '--json'], |
| workingDirectory: flutterPath, |
| ); |
| } |
| |
| /// Returns the directory in which we generated documentation. |
| Future<String> _buildPubPackageDocs( |
| String pubPackageName, |
| List<String> dartdocParameters, |
| PackageMetaProvider packageMetaProvider, [ |
| String? version, |
| String? label, |
| ]) async { |
| var env = _createThrowawayPubCache(); |
| var versionContext = version == null ? '' : '-$version'; |
| var labelContext = label == null ? '' : '-$label'; |
| var launcher = SubprocessLauncher( |
| 'build-$pubPackageName$versionContext$labelContext', env); |
| await launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'pub', |
| 'cache', |
| 'add', |
| if (version != null) ...['-v', version], |
| pubPackageName, |
| ]); |
| var cache = |
| Directory(p.join(env['PUB_CACHE']!, 'hosted', 'pub.dartlang.org')); |
| var pubPackageDirOrig = |
| cache.listSync().firstWhere((e) => e.path.contains(pubPackageName)); |
| var pubPackageDir = Directory.systemTemp.createTempSync(pubPackageName); |
| copy(pubPackageDirOrig, pubPackageDir); |
| |
| if (packageMetaProvider |
| .fromDir(PhysicalResourceProvider.INSTANCE.getFolder(pubPackageDir.path))! |
| .requiresFlutter) { |
| var flutterRepo = |
| await FlutterRepo.fromExistingFlutterRepo(await cleanFlutterRepo); |
| await launcher.runStreamed(flutterRepo.cacheDart, ['pub', 'get'], |
| environment: flutterRepo.env, |
| workingDirectory: pubPackageDir.absolute.path); |
| await launcher.runStreamed( |
| flutterRepo.cacheDart, |
| [ |
| '--enable-asserts', |
| p.join(Directory.current.absolute.path, 'bin', 'dartdoc.dart'), |
| '--json', |
| '--link-to-remote', |
| '--show-progress', |
| ...dartdocParameters, |
| ], |
| environment: flutterRepo.env, |
| workingDirectory: pubPackageDir.absolute.path); |
| } else { |
| await launcher.runStreamed(Platform.resolvedExecutable, ['pub', 'get'], |
| workingDirectory: pubPackageDir.absolute.path); |
| await launcher.runStreamed( |
| Platform.resolvedExecutable, |
| [ |
| '--enable-asserts', |
| p.join(Directory.current.absolute.path, 'bin', 'dartdoc.dart'), |
| '--json', |
| '--link-to-remote', |
| '--show-progress', |
| ...dartdocParameters, |
| ], |
| workingDirectory: pubPackageDir.absolute.path); |
| } |
| return p.join(pubPackageDir.absolute.path, 'doc', 'api'); |
| } |
| |
| @Task( |
| 'Build an arbitrary pub package based on PACKAGE_NAME and PACKAGE_VERSION environment variables') |
| Future<String> buildPubPackage() async { |
| var packageName = Platform.environment['PACKAGE_NAME']!; |
| var version = Platform.environment['PACKAGE_VERSION']; |
| return _buildPubPackageDocs( |
| packageName, |
| _extraDartdocParameters, |
| pubPackageMetaProvider, |
| version, |
| ); |
| } |
| |
| @Task( |
| 'Serve an arbitrary pub package based on PACKAGE_NAME and PACKAGE_VERSION environment variables') |
| Future<void> servePubPackage() async { |
| await _serveDocsFrom(await buildPubPackage(), 9000, 'serve-pub-package'); |
| } |
| |
| @Task('Checks that CHANGELOG mentions current version') |
| Future<void> checkChangelogHasVersion() async { |
| var changelog = File('CHANGELOG.md'); |
| if (!changelog.existsSync()) { |
| fail('ERROR: No CHANGELOG.md found in ${Directory.current}'); |
| } |
| |
| var version = _getPackageVersion(); |
| |
| if (!changelog.readAsLinesSync().contains('## $version')) { |
| fail('ERROR: CHANGELOG.md does not mention version $version'); |
| } |
| } |
| |
| String _getPackageVersion() { |
| var pubspec = File('pubspec.yaml'); |
| if (!pubspec.existsSync()) { |
| fail('Cannot find pubspec.yaml in ${Directory.current}'); |
| } |
| var yamlDoc = yaml.loadYaml(pubspec.readAsStringSync()) as yaml.YamlMap; |
| return yamlDoc['version']; |
| } |
| |
| @Task('Rebuild generated files') |
| @Depends(buildWeb) |
| Future<void> build() async { |
| var launcher = SubprocessLauncher('build'); |
| await launcher.runStreamed(Platform.resolvedExecutable, |
| ['pub', 'run', 'build_runner', 'build', '--delete-conflicting-outputs']); |
| |
| // TODO(jcollins-g): port to build system? |
| var version = _getPackageVersion(); |
| var dartdocOptions = File('dartdoc_options.yaml'); |
| await dartdocOptions.writeAsString('''dartdoc: |
| linkToSource: |
| root: '.' |
| uriTemplate: 'https://github.com/dart-lang/dartdoc/blob/v$version/%f%#L%l%' |
| '''); |
| } |
| |
| @Task('Build the web frontend') |
| Future<void> buildWeb() async { |
| // Compile the web app. |
| var launcher = SubprocessLauncher('build'); |
| await launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'compile', |
| 'js', |
| '--output=lib/resources/docs.dart.js', |
| 'web/docs.dart', |
| ]); |
| delete(File('lib/resources/docs.dart.js.deps')); |
| |
| final compileSig = calcDartFilesSig(Directory('web')); |
| File(p.join('web', 'sig.txt')).writeAsStringSync('$compileSig\n'); |
| } |
| |
| /// Paths in this list are relative to lib/. |
| final _generatedFilesList = <String>[ |
| '../dartdoc_options.yaml', |
| 'src/generator/html_resources.g.dart', |
| 'src/generator/templates.aot_renderers_for_html.dart', |
| 'src/generator/templates.aot_renderers_for_md.dart', |
| 'src/generator/templates.runtime_renderers.dart', |
| 'src/version.dart', |
| '../test/mustachio/foo.dart', |
| ].map((s) => p.joinAll(p.posix.split(s))); |
| |
| @Task('Verify generated files are up to date') |
| Future<void> checkBuild() async { |
| var originalFileContents = <String, String>{}; |
| var differentFiles = <String>[]; |
| |
| // Load original file contents into memory before running the builder; it |
| // modifies them in place. |
| for (var relPath in _generatedFilesList) { |
| var origPath = p.joinAll(['lib', relPath]); |
| var oldVersion = File(origPath); |
| if (oldVersion.existsSync()) { |
| originalFileContents[relPath] = oldVersion.readAsStringSync(); |
| } |
| } |
| |
| await build(); |
| |
| for (var relPath in _generatedFilesList) { |
| var newVersion = File(p.join('lib', relPath)); |
| if (!newVersion.existsSync()) { |
| log('${newVersion.path} does not exist\n'); |
| differentFiles.add(relPath); |
| } else if (originalFileContents[relPath] != |
| await newVersion.readAsString()) { |
| log('${newVersion.path} has changed to: \n${newVersion.readAsStringSync()})'); |
| differentFiles.add(relPath); |
| } |
| } |
| |
| if (differentFiles.isNotEmpty) { |
| fail('The following generated files needed to be rebuilt:\n' |
| ' ${differentFiles.map((f) => p.join('lib', f)).join("\n ")}\n' |
| 'Rebuild them with "grind build" and check the results in.'); |
| } |
| |
| // Verify that the web frontend has been compiled. |
| final currentCodeSig = calcDartFilesSig(Directory('web')); |
| final lastCompileSig = |
| File(p.join('web', 'sig.txt')).readAsStringSync().trim(); |
| if (currentCodeSig != lastCompileSig) { |
| log('current files: $currentCodeSig'); |
| log('cached sig : $lastCompileSig'); |
| fail('The web frontend (web/docs.dart) needs to be recompiled; rebuild it ' |
| 'with "grind build-web" or "grind build".'); |
| } |
| } |
| |
| @Task('Dry run of publish to pub.dartlang') |
| @Depends(checkChangelogHasVersion) |
| Future<void> tryPublish() async { |
| var launcher = SubprocessLauncher('try-publish'); |
| await launcher |
| .runStreamed(Platform.resolvedExecutable, ['pub', 'publish', '-n']); |
| } |
| |
| @Task('Run a smoke test, only') |
| @Depends(clean) |
| Future<void> smokeTest() async { |
| await testDart(smokeTestFiles); |
| await testFutures.tasksComplete; |
| } |
| |
| @Task('Run non-smoke tests, only') |
| @Depends(clean) |
| Future<void> longTest() async { |
| await testDart(testFiles); |
| await testFutures.tasksComplete; |
| } |
| |
| @Task('Run all the tests.') |
| @Depends(clean) |
| Future<void> test() async { |
| await testDart(smokeTestFiles.followedBy(testFiles)); |
| await testFutures.tasksComplete; |
| } |
| |
| @Task('Clean up pub data from test directories') |
| Future<void> clean() async { |
| var toDelete = nonRootPubData; |
| for (var e in toDelete) { |
| e.deleteSync(recursive: true); |
| } |
| } |
| |
| Iterable<FileSystemEntity> get nonRootPubData { |
| // This involves deleting things, so be careful. |
| if (!File(p.join('tool', 'grind.dart')).existsSync()) { |
| throw FileSystemException('wrong CWD, run from root of dartdoc package'); |
| } |
| return Directory('.') |
| .listSync(recursive: true) |
| .where((e) => p.dirname(e.path) != '.') |
| .where((e) => <String>['.dart_tool', '.packages', 'pubspec.lock'] |
| .contains(p.basename(e.path))); |
| } |
| |
| List<File> get smokeTestFiles => Directory('test') |
| .listSync(recursive: true) |
| .whereType<File>() |
| .where((e) => p.basename(e.path) == 'model_test.dart') |
| .toList(growable: false); |
| |
| List<File> get testFiles => Directory('test') |
| .listSync(recursive: true) |
| .whereType<File>() |
| .where((e) => e.path.endsWith('test.dart')) |
| .where((e) => p.basename(e.path) != 'model_test.dart') |
| .toList(growable: false); |
| |
| Future<void> testDart(Iterable<File> tests) async { |
| var parameters = <String>['--enable-asserts']; |
| |
| for (var dartFile in tests) { |
| await testFutures.add(() => |
| CoverageSubprocessLauncher('dart-${p.basename(dartFile.path)}') |
| .runStreamed(Platform.resolvedExecutable, |
| <String>[...parameters, dartFile.path])); |
| } |
| |
| return CoverageSubprocessLauncher.generateCoverageToFile( |
| PhysicalResourceProvider.INSTANCE.getFile(p.canonicalize('lcov.info')), |
| PhysicalResourceProvider.INSTANCE); |
| } |
| |
| @Task('Generate docs for dartdoc without link-to-remote') |
| Future<void> testDartdoc() async { |
| var launcher = SubprocessLauncher('test-dartdoc'); |
| await launcher.runStreamed(Platform.resolvedExecutable, [ |
| '--enable-asserts', |
| 'bin/dartdoc.dart', |
| '--output', |
| _dartdocDocsPath, |
| '--no-link-to-remote', |
| ]); |
| expectFileContains(p.join(_dartdocDocsPath, 'index.html'), |
| ['<title>dartdoc - Dart API docs</title>']); |
| var object = RegExp('<li>Object</li>', multiLine: true); |
| expectFileContains( |
| p.join(_dartdocDocsPath, 'dartdoc', 'ModelElement-class.html'), [object]); |
| } |
| |
| @Task('Generate docs for dartdoc with remote linking') |
| Future<void> testDartdocRemote() async { |
| var launcher = SubprocessLauncher('test-dartdoc-remote'); |
| var object = RegExp( |
| '<a href="https://api.dart.dev/(dev|stable|beta|edge)/[^/]*/dart-core/Object-class.html">Object</a>', |
| multiLine: true); |
| await launcher.runStreamed(Platform.resolvedExecutable, |
| ['--enable-asserts', 'bin/dartdoc.dart', '--output', _dartdocDocsPath]); |
| expectFileContains(p.join(_dartdocDocsPath, 'index.html'), |
| ['<title>dartdoc - Dart API docs</title>']); |
| expectFileContains( |
| p.join(_dartdocDocsPath, 'dartdoc', 'ModelElement-class.html'), [object]); |
| } |
| |
| @Task('serve docs for a package that requires flutter with remote linking') |
| @Depends(buildDartdocFlutterPluginDocs) |
| Future<void> serveDartdocFlutterPluginDocs() async { |
| await _serveDocsFrom( |
| _pluginPackageDocsPath, 8005, 'serve-dartdoc-flutter-plugin-docs'); |
| } |
| |
| Future<WarningsCollection> _buildDartdocFlutterPluginDocs() async { |
| var flutterRepo = await FlutterRepo.fromExistingFlutterRepo( |
| await cleanFlutterRepo, 'docs-flutter-plugin'); |
| |
| await flutterRepo.launcher.runStreamed(flutterRepo.cacheDart, ['pub', 'get'], |
| workingDirectory: testPackageFlutterPlugin.path); |
| |
| return jsonMessageIterableToWarnings( |
| await flutterRepo.launcher.runStreamed( |
| flutterRepo.cacheDart, |
| [ |
| '--enable-asserts', |
| p.join(Directory.current.path, 'bin', 'dartdoc.dart'), |
| '--json', |
| '--link-to-remote', |
| '--output', |
| _pluginPackageDocsPath |
| ], |
| workingDirectory: testPackageFlutterPlugin.path, |
| ), |
| _pluginPackageDocsPath, |
| defaultPubCache, |
| 'HEAD', |
| ); |
| } |
| |
| @Task('Build docs for a package that requires flutter with remote linking') |
| @Depends(clean) |
| Future<void> buildDartdocFlutterPluginDocs() async { |
| await _buildDartdocFlutterPluginDocs(); |
| } |
| |
| @Task('Verify docs for a package that requires flutter with remote linking') |
| Future<void> testDartdocFlutterPlugin() async { |
| var warnings = await _buildDartdocFlutterPluginDocs(); |
| if (warnings.warningKeyCounts.isNotEmpty) { |
| fail('No warnings should exist in : ${warnings.warningKeyCounts}'); |
| } |
| // Verify that links to Dart SDK and Flutter SDK go to the flutter site. |
| expectFileContains( |
| p.join(_pluginPackageDocsPath, 'testlib', 'MyAwesomeWidget-class.html'), [ |
| '<a href="https://api.flutter.dev/flutter/widgets/Widget-class.html">Widget</a>', |
| '<a href="https://api.flutter.dev/flutter/dart-core/Object-class.html">Object</a>' |
| ]); |
| } |
| |
| @Task('Validate the SDK doc build.') |
| @Depends(buildSdkDocs) |
| void validateSdkDocs() { |
| const expectedLibCounts = 0; |
| const expectedSubLibCount = {18, 19}; |
| const expectedTotalCount = {18, 19}; |
| var indexHtml = joinFile(_sdkDocsDir, ['index.html']); |
| if (!indexHtml.existsSync()) { |
| fail('no index.html found for SDK docs'); |
| } |
| log('found index.html'); |
| var indexContents = indexHtml.readAsStringSync(); |
| var foundLibs = _findCount(indexContents, ' <li><a href="dart-'); |
| if (expectedLibCounts != foundLibs) { |
| fail('expected $expectedLibCounts "dart:" index.html entries, found ' |
| '$foundLibs'); |
| } |
| log('$foundLibs index.html dart: entries found'); |
| |
| var foundSubLibs = |
| _findCount(indexContents, '<li class="section-subitem"><a href="dart-'); |
| if (!expectedSubLibCount.contains(foundSubLibs)) { |
| fail('expected $expectedSubLibCount "dart:" index.html entries in ' |
| 'categories, found $foundSubLibs'); |
| } |
| log('$foundSubLibs index.html dart: entries in categories found'); |
| |
| // check for the existence of certain files/dirs |
| var libsLength = |
| _sdkDocsDir.listSync().where((fs) => fs.path.contains('dart-')).length; |
| if (!expectedTotalCount.contains(libsLength)) { |
| fail('docs not generated for all the SDK libraries, ' |
| 'expected $expectedTotalCount directories, generated $libsLength directories'); |
| } |
| log('$libsLength dart: libraries found'); |
| |
| var futureConstFile = |
| joinFile(_sdkDocsDir, [p.join('dart-async', 'Future', 'Future.html')]); |
| if (!futureConstFile.existsSync()) { |
| fail('no Future.html found for dart:async Future constructor'); |
| } |
| log('found Future.async ctor'); |
| } |
| |
| int _findCount(String str, String match) { |
| var count = 0; |
| var index = str.indexOf(match); |
| while (index != -1) { |
| count++; |
| index = str.indexOf(match, index + match.length); |
| } |
| return count; |
| } |
| |
| String calcDartFilesSig(Directory dir) { |
| final files = dir |
| .listSync(recursive: true) |
| .whereType<File>() |
| .where((file) => file.path.endsWith('.dart')) |
| .toList(); |
| files.sort((a, b) => a.path.toLowerCase().compareTo(b.path.toLowerCase())); |
| |
| var output = AccumulatorSink<crypto.Digest>(); |
| var input = crypto.md5.startChunkedConversion(output); |
| for (var file in files) { |
| for (var line in file.readAsLinesSync()) { |
| input.add(utf8.encoder.convert(line.trim())); |
| } |
| } |
| input.close(); |
| |
| var result = output.events.single; |
| return result.bytes |
| .map((byte) => byte.toRadixString(16).padLeft(2, '0').toUpperCase()) |
| .join(); |
| } |
| |
| class AccumulatorSink<T> implements Sink<T> { |
| List<T> get events => _events; |
| final _events = <T>[]; |
| |
| var _isClosed = false; |
| |
| @override |
| void add(T event) { |
| if (_isClosed) { |
| throw StateError("Can't add to a closed sink."); |
| } |
| |
| _events.add(event); |
| } |
| |
| @override |
| void close() { |
| _isClosed = true; |
| } |
| } |