diff --git a/test/dartdoc_integration_test.dart b/test/dartdoc_integration_test.dart index b6aeecc230..e12beff778 100644 --- a/test/dartdoc_integration_test.dart +++ b/test/dartdoc_integration_test.dart @@ -12,6 +12,7 @@ import 'package:dartdoc/dartdoc.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; +import '../tool/subprocess_launcher.dart'; import 'src/utils.dart'; Uri get _currentFileUri => diff --git a/test/dartdoc_test.dart b/test/dartdoc_test.dart index fd142e9352..fac044c10f 100644 --- a/test/dartdoc_test.dart +++ b/test/dartdoc_test.dart @@ -17,6 +17,35 @@ import 'package:test/test.dart'; import 'src/utils.dart'; +final Directory _testPackageDir = Directory('testing/test_package'); + +final _testPackageBadDir = Directory('testing/test_package_bad'); +final _testPackageMinimumDir = Directory('testing/test_package_minimum'); +final _testSkyEnginePackage = Directory('testing/sky_engine'); +final _testPackageWithNoReadme = Directory('testing/test_package_small'); +final _testPackageIncludeExclude = + Directory('testing/test_package_include_exclude'); +final _testPackageImportExportError = + Directory('testing/test_package_import_export_error'); +final _testPackageOptions = Directory('testing/test_package_options'); +final _testPackageOptionsImporter = + Directory('testing/test_package_options_importer'); +final _testPackageCustomTemplates = + Directory('testing/test_package_custom_templates'); + +/// Convenience factory to build a [DartdocGeneratorOptionContext] and associate +/// it with a [DartdocOptionSet] based on the current working directory and/or +/// the '--input' flag. +Future _generatorContextFromArgv( + List argv) async { + var optionSet = await DartdocOptionSet.fromOptionGenerators('dartdoc', [ + () => createDartdocOptions(pubPackageMetaProvider), + createGeneratorOptions, + ]); + optionSet.parseArguments(argv); + return DartdocGeneratorOptionContext(optionSet, null); +} + class DartdocLoggingOptionContext extends DartdocGeneratorOptionContext with LoggingContext { DartdocLoggingOptionContext(DartdocOptionSet optionSet, Directory dir) @@ -28,7 +57,6 @@ void main() { Directory tempDir; setUpAll(() async { - tempDir = Directory.systemTemp.createTempSync('dartdoc.test.'); var optionSet = await DartdocOptionSet.fromOptionGenerators( 'dartdoc', [createLoggingOptions]); optionSet.parseArguments([]); @@ -45,7 +73,7 @@ void main() { Future buildDartdoc( List argv, Directory packageRoot, Directory tempDir) async { - var context = await generatorContextFromArgv(argv + var context = await _generatorContextFromArgv(argv ..addAll(['--input', packageRoot.path, '--output', tempDir.path])); return await Dartdoc.fromContext( context, @@ -61,15 +89,11 @@ void main() { setUpAll(() async { tempDir = Directory.systemTemp.createTempSync('dartdoc.test.'); - dartdoc = await buildDartdoc([], testPackageOptions, tempDir); + dartdoc = await buildDartdoc([], _testPackageOptions, tempDir); results = await dartdoc.generateDocsBase(); p = results.packageGraph; }); - tearDownAll(() async { - tempDir.deleteSync(recursive: true); - }); - test('generator parameters', () async { var favicon = File(path.joinAll([tempDir.path, 'static-assets', 'favicon.png'])); @@ -126,16 +150,12 @@ void main() { setUpAll(() async { tempDir = Directory.systemTemp.createTempSync('dartdoc.test.'); results = await (await buildDartdoc( - ['--link-to-remote'], testPackageOptionsImporter, tempDir)) + ['--link-to-remote'], _testPackageOptionsImporter, tempDir)) .generateDocsBase(); testPackageOptions = results.packageGraph.packages .firstWhere((Package p) => p.name == 'test_package_options'); }); - tearDownAll(() async { - tempDir.deleteSync(recursive: true); - }); - test('linkToUrl', () async { var main = testPackageOptions.allLibraries .firstWhere((Library l) => l.name == 'main'); @@ -161,7 +181,7 @@ void main() { test('with broken reexport chain', () async { var dartdoc = - await buildDartdoc([], testPackageImportExportError, tempDir); + await buildDartdoc([], _testPackageImportExportError, tempDir); var results = await dartdoc.generateDocsBase(); var p = results.packageGraph; var unresolvedExportWarnings = p @@ -179,7 +199,7 @@ void main() { group('include/exclude parameters', () { test('with config file', () async { var dartdoc = - await buildDartdoc([], testPackageIncludeExclude, tempDir); + await buildDartdoc([], _testPackageIncludeExclude, tempDir); var results = await dartdoc.generateDocs(); var p = results.packageGraph; expect(p.localPublicLibraries.map((l) => l.name), @@ -188,7 +208,7 @@ void main() { test('with include command line argument', () async { var dartdoc = await buildDartdoc(['--include', 'another_included'], - testPackageIncludeExclude, tempDir); + _testPackageIncludeExclude, tempDir); var results = await dartdoc.generateDocs(); var p = results.packageGraph; expect(p.localPublicLibraries.length, equals(1)); @@ -196,8 +216,8 @@ void main() { }); test('with exclude command line argument', () async { - var dartdoc = await buildDartdoc( - ['--exclude', 'more_included'], testPackageIncludeExclude, tempDir); + var dartdoc = await buildDartdoc(['--exclude', 'more_included'], + _testPackageIncludeExclude, tempDir); var results = await dartdoc.generateDocs(); var p = results.packageGraph; expect(p.localPublicLibraries.length, equals(1)); @@ -207,14 +227,14 @@ void main() { }); test('package without version produces valid semver in docs', () async { - var dartdoc = await buildDartdoc([], testPackageMinimumDir, tempDir); + var dartdoc = await buildDartdoc([], _testPackageMinimumDir, tempDir); var results = await dartdoc.generateDocs(); var p = results.packageGraph; expect(p.defaultPackage.version, equals('0.0.0-unknown')); }); test('basic interlinking test', () async { - var dartdoc = await buildDartdoc([], testPackageDir, tempDir); + var dartdoc = await buildDartdoc([], _testPackageDir, tempDir); var results = await dartdoc.generateDocs(); var p = results.packageGraph; var meta = p.publicPackages.firstWhere((p) => p.name == 'meta'); @@ -243,15 +263,11 @@ void main() { setUpAll(() async { tempDir = Directory.systemTemp.createTempSync('dartdoc.test.'); - dartdoc = await buildDartdoc([], testPackageDir, tempDir); + dartdoc = await buildDartdoc([], _testPackageDir, tempDir); results = await dartdoc.generateDocs(); }); - tearDownAll(() async { - tempDir.deleteSync(recursive: true); - }); - - test('generate docs for ${path.basename(testPackageDir.path)} works', + test('generate docs for ${path.basename(_testPackageDir.path)} works', () async { expect(results.packageGraph, isNotNull); var packageGraph = results.packageGraph; @@ -278,9 +294,9 @@ void main() { }); }); - test('generate docs for ${path.basename(testPackageBadDir.path)} fails', + test('generate docs for ${path.basename(_testPackageBadDir.path)} fails', () async { - var dartdoc = await buildDartdoc([], testPackageBadDir, tempDir); + var dartdoc = await buildDartdoc([], _testPackageBadDir, tempDir); try { await dartdoc.generateDocs(); @@ -293,7 +309,7 @@ void main() { 'from analysis_options'); test('generate docs for a package that does not have a readme', () async { - var dartdoc = await buildDartdoc([], testPackageWithNoReadme, tempDir); + var dartdoc = await buildDartdoc([], _testPackageWithNoReadme, tempDir); var results = await dartdoc.generateDocs(); expect(results.packageGraph, isNotNull); @@ -307,7 +323,7 @@ void main() { test('generate docs including a single library', () async { var dartdoc = - await buildDartdoc(['--include', 'fake'], testPackageDir, tempDir); + await buildDartdoc(['--include', 'fake'], _testPackageDir, tempDir); var results = await dartdoc.generateDocs(); expect(results.packageGraph, isNotNull); @@ -321,7 +337,7 @@ void main() { test('generate docs excluding a single library', () async { var dartdoc = - await buildDartdoc(['--exclude', 'fake'], testPackageDir, tempDir); + await buildDartdoc(['--exclude', 'fake'], _testPackageDir, tempDir); var results = await dartdoc.generateDocs(); expect(results.packageGraph, isNotNull); @@ -338,7 +354,7 @@ void main() { }); test('generate docs for package with embedder yaml', () async { - var dartdoc = await buildDartdoc([], testSkyEnginePackage, tempDir); + var dartdoc = await buildDartdoc([], _testSkyEnginePackage, tempDir); var results = await dartdoc.generateDocs(); expect(results.packageGraph, isNotNull); @@ -370,9 +386,9 @@ void main() { test('generate docs with custom templates', () async { var templatesDir = - path.join(testPackageCustomTemplates.path, 'templates'); + path.join(_testPackageCustomTemplates.path, 'templates'); var dartdoc = await buildDartdoc(['--templates-dir', templatesDir], - testPackageCustomTemplates, tempDir); + _testPackageCustomTemplates, tempDir); var results = await dartdoc.generateDocs(); expect(results.packageGraph, isNotNull); @@ -390,7 +406,7 @@ void main() { var templatesDir = path.join(path.current, 'test/templates'); try { await buildDartdoc(['--templates-dir', templatesDir], - testPackageCustomTemplates, tempDir); + _testPackageCustomTemplates, tempDir); fail('dartdoc should fail with missing required template'); } catch (e) { expect(e is DartdocFailure, isTrue); @@ -403,7 +419,7 @@ void main() { var badPath = path.join(tempDir.path, 'BAD'); try { await buildDartdoc( - ['--templates-dir', badPath], testPackageCustomTemplates, tempDir); + ['--templates-dir', badPath], _testPackageCustomTemplates, tempDir); fail('dartdoc should fail with bad templatesDir path'); } catch (e) { expect(e is DartdocFailure, isTrue); @@ -412,7 +428,7 @@ void main() { test('generating markdown docs does not crash', () async { var dartdoc = - await buildDartdoc(['--format', 'md'], testPackageDir, tempDir); + await buildDartdoc(['--format', 'md'], _testPackageDir, tempDir); await dartdoc.generateDocsBase(); }); @@ -420,7 +436,7 @@ void main() { // ignore: omit_local_variable_types final String prefix = 'foo.bar/baz'; var dartdoc = await buildDartdoc( - ['--rel-canonical-prefix', prefix], testPackageDir, tempDir); + ['--rel-canonical-prefix', prefix], _testPackageDir, tempDir); await dartdoc.generateDocsBase(); // Verify files at different levels have correct content. @@ -438,7 +454,7 @@ void main() { test('generate docs with bad output format', () async { try { - await buildDartdoc(['--format', 'bad'], testPackageDir, tempDir); + await buildDartdoc(['--format', 'bad'], _testPackageDir, tempDir); fail('dartdoc should fail with bad output format'); } catch (e) { expect(e is DartdocFailure, isTrue); diff --git a/test/html_generator_test.dart b/test/html_generator_test.dart index 937c8c98d9..567fa3accd 100644 --- a/test/html_generator_test.dart +++ b/test/html_generator_test.dart @@ -110,11 +110,12 @@ void main() { PackageGraph packageGraph; Directory tempOutput; FileWriter writer; + var testPackageDuplicateDir = Directory('testing/test_package_duplicate'); setUp(() async { generator = await _initGeneratorForTest(); - packageGraph = await utils - .bootBasicPackage(utils.testPackageDuplicateDir.path, []); + packageGraph = + await utils.bootBasicPackage(testPackageDuplicateDir.path, []); tempOutput = await Directory.systemTemp.createTemp('doc_test_temp'); writer = DartdocFileWriter(tempOutput.path); }); diff --git a/test/model_special_cases_test.dart b/test/model_special_cases_test.dart index 4d6052d96d..73b80b7a0a 100644 --- a/test/model_special_cases_test.dart +++ b/test/model_special_cases_test.dart @@ -10,6 +10,7 @@ library dartdoc.model_special_cases_test; import 'dart:io'; +import 'package:async/async.dart'; import 'package:dartdoc/dartdoc.dart'; import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/special_elements.dart'; @@ -21,6 +22,43 @@ import 'src/utils.dart' as utils; final String _platformVersionString = Platform.version.split(' ').first; final Version _platformVersion = Version.parse(_platformVersionString); +final _testPackageGraphExperimentsMemo = AsyncMemoizer(); +Future get _testPackageGraphExperiments => + _testPackageGraphExperimentsMemo.runOnce(() => utils.bootBasicPackage( + 'testing/test_package_experiments', [], additionalArguments: [ + '--enable-experiment', + 'non-nullable', + '--no-link-to-remote' + ])); + +final _testPackageGraphGinormousMemo = AsyncMemoizer(); +Future get _testPackageGraphGinormous => + _testPackageGraphGinormousMemo.runOnce(() => utils.bootBasicPackage( + 'testing/test_package', [ + 'css', + 'code_in_commnets', + 'excluded' + ], additionalArguments: [ + '--auto-include-dependencies', + '--no-link-to-remote' + ])); + +final _testPackageGraphSmallMemo = AsyncMemoizer(); +Future get _testPackageGraphSmall => + _testPackageGraphSmallMemo.runOnce(() => utils.bootBasicPackage( + 'testing/test_package_small', [], + additionalArguments: ['--no-link-to-remote'])); + +final _testPackageGraphSdkMemo = AsyncMemoizer(); +Future get _testPackageGraphSdk => + _testPackageGraphSdkMemo.runOnce(_bootSdkPackage); + +Future _bootSdkPackage() async { + return PubPackageBuilder( + await utils.contextFromArgv(['--input', defaultSdkDir.path])) + .buildPackageGraph(); +} + void main() { var sdkDir = defaultSdkDir; @@ -46,17 +84,16 @@ void main() { Class b; setUpAll(() async { - lateFinalWithoutInitializer = (await utils.testPackageGraphExperiments) + lateFinalWithoutInitializer = (await _testPackageGraphExperiments) .libraries .firstWhere((lib) => lib.name == 'late_final_without_initializer'); - nullSafetyClassMemberDeclarations = (await utils - .testPackageGraphExperiments) + nullSafetyClassMemberDeclarations = (await _testPackageGraphExperiments) .libraries .firstWhere((lib) => lib.name == 'nnbd_class_member_declarations'); - optOutOfNullSafety = (await utils.testPackageGraphExperiments) + optOutOfNullSafety = (await _testPackageGraphExperiments) .libraries .firstWhere((lib) => lib.name == 'opt_out_of_nnbd'); - nullableElements = (await utils.testPackageGraphExperiments) + nullableElements = (await _testPackageGraphExperiments) .libraries .firstWhere((lib) => lib.name == 'nullable_elements'); b = nullSafetyClassMemberDeclarations.allClasses @@ -283,7 +320,7 @@ void main() { PackageGraph ginormousPackageGraph; setUpAll(() async { - ginormousPackageGraph = await utils.testPackageGraphGinormous; + ginormousPackageGraph = await _testPackageGraphGinormous; }); test('Verify that SDK libraries are not canonical when missing', () { @@ -316,7 +353,7 @@ void main() { PackageGraph ginormousPackageGraph; setUpAll(() async { - ginormousPackageGraph = await utils.testPackageGraphGinormous; + ginormousPackageGraph = await _testPackageGraphGinormous; }); test( @@ -378,7 +415,7 @@ void main() { }); test('Verify that packages without categories get handled', () async { - var packageGraphSmall = await utils.testPackageGraphSmall; + var packageGraphSmall = await _testPackageGraphSmall; expect(packageGraphSmall.localPackages.length, equals(1)); expect(packageGraphSmall.localPackages.first.hasCategories, isFalse); var packageCategories = packageGraphSmall.localPackages.first.categories; @@ -390,8 +427,8 @@ void main() { PackageGraph ginormousPackageGraph, sdkAsPackageGraph; setUpAll(() async { - ginormousPackageGraph = await utils.testPackageGraphGinormous; - sdkAsPackageGraph = await utils.testPackageGraphSdk; + ginormousPackageGraph = await _testPackageGraphGinormous; + sdkAsPackageGraph = await _testPackageGraphSdk; }); group('test package', () { @@ -424,7 +461,7 @@ void main() { group('test small package', () { test('does not have documentation', () async { - var packageGraphSmall = await utils.testPackageGraphSmall; + var packageGraphSmall = await _testPackageGraphSmall; expect(packageGraphSmall.defaultPackage.hasDocumentation, isFalse); expect(packageGraphSmall.defaultPackage.hasDocumentationFile, isFalse); expect(packageGraphSmall.defaultPackage.documentationFile, isNull); @@ -471,7 +508,7 @@ void main() { Library dartAsyncLib; setUpAll(() async { - dartAsyncLib = (await utils.testPackageGraphSdk) + dartAsyncLib = (await _testPackageGraphSdk) .libraries .firstWhere((l) => l.name == 'dart:async'); // Make sure the first library is dart:async diff --git a/test/model_test.dart b/test/model_test.dart index 2d8b1cc825..81d261be8c 100644 --- a/test/model_test.dart +++ b/test/model_test.dart @@ -6,6 +6,7 @@ library dartdoc.model_test; import 'dart:io'; +import 'package:async/async.dart'; import 'package:dartdoc/dartdoc.dart'; import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/render/category_renderer.dart'; @@ -19,6 +20,12 @@ import 'package:test/test.dart'; import 'src/utils.dart' as utils; +final _testPackageGraphMemo = AsyncMemoizer(); +Future get _testPackageGraph => + _testPackageGraphMemo.runOnce(() => utils.bootBasicPackage( + 'testing/test_package', ['css', 'code_in_comments'], + additionalArguments: ['--no-link-to-remote'])); + /// For testing sort behavior. class TestLibraryContainer extends LibraryContainer with Nameable { @override @@ -67,7 +74,7 @@ void main() { setUpAll(() async { // Use model_special_cases_test.dart for tests that require // a different package graph. - packageGraph = await utils.testPackageGraph; + packageGraph = await _testPackageGraph; exLibrary = packageGraph.libraries.firstWhere((lib) => lib.name == 'ex'); fakeLibrary = packageGraph.libraries.firstWhere((lib) => lib.name == 'fake'); diff --git a/test/src/utils.dart b/test/src/utils.dart index f3c521b685..6628fe9074 100644 --- a/test/src/utils.dart +++ b/test/src/utils.dart @@ -5,105 +5,19 @@ library test_utils; import 'dart:async'; -import 'dart:convert'; import 'dart:io'; -import 'package:async/async.dart'; import 'package:dartdoc/dartdoc.dart'; import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/package_meta.dart'; -import 'package:path/path.dart' as path; /// The number of public libraries in testing/test_package, minus 2 for /// the excluded libraries listed in the initializers for _testPackageGraphMemo /// and minus 1 for the tag in the 'excluded' library. const int kTestPackagePublicLibraries = 17; -final RegExp quotables = RegExp(r'[ "\r\n\$]'); -final RegExp observatoryPortRegexp = - RegExp(r'^Observatory listening on http://.*:(\d+)'); - -Directory sdkDir = defaultSdkDir; -PackageMeta sdkPackageMeta = pubPackageMetaProvider.fromDir(sdkDir); - -final _testPackageGraphMemo = AsyncMemoizer(); -Future get testPackageGraph => _testPackageGraphMemo.runOnce(() => - bootBasicPackage('testing/test_package', ['css', 'code_in_comments'], - additionalArguments: ['--no-link-to-remote'])); - -final _testPackageGraphExperimentsMemo = AsyncMemoizer(); -Future get testPackageGraphExperiments => - _testPackageGraphExperimentsMemo.runOnce(() => bootBasicPackage( - 'testing/test_package_experiments', [], additionalArguments: [ - '--enable-experiment', - 'non-nullable', - '--no-link-to-remote' - ])); - -final _testPackageGraphGinormousMemo = AsyncMemoizer(); -Future get testPackageGraphGinormous => - _testPackageGraphGinormousMemo.runOnce(() => bootBasicPackage( - 'testing/test_package', [ - 'css', - 'code_in_commnets', - 'excluded' - ], additionalArguments: [ - '--auto-include-dependencies', - '--no-link-to-remote' - ])); - -final _testPackageGraphSmallMemo = AsyncMemoizer(); -Future get testPackageGraphSmall => - _testPackageGraphSmallMemo.runOnce(() => bootBasicPackage( - 'testing/test_package_small', [], - additionalArguments: ['--no-link-to-remote'])); - -final _testPackageGraphErrorsMemo = AsyncMemoizer(); -Future get testPackageGraphErrors => - _testPackageGraphErrorsMemo.runOnce(() => bootBasicPackage( - 'testing/test_package_doc_errors', - ['css', 'code_in_comments', 'excluded'], - additionalArguments: ['--no-link-to-remote'])); - -final _testPackageGraphSdkMemo = AsyncMemoizer(); -Future get testPackageGraphSdk => - _testPackageGraphSdkMemo.runOnce(bootSdkPackage); - -final Directory testPackageBadDir = Directory('testing/test_package_bad'); -final Directory testPackageDir = Directory('testing/test_package'); -final Directory testPackageDuplicateDir = - Directory('testing/test_package_duplicate'); -final Directory testPackageExperimentsDir = - Directory('testing/test_package_experiments'); -final Directory testPackageMinimumDir = - Directory('testing/test_package_minimum'); -final Directory testSkyEnginePackage = Directory('testing/sky_engine'); -final Directory testPackageWithNoReadme = - Directory('testing/test_package_small'); -final Directory testPackageIncludeExclude = - Directory('testing/test_package_include_exclude'); -final Directory testPackageImportExportError = - Directory('testing/test_package_import_export_error'); -final Directory testPackageOptions = Directory('testing/test_package_options'); -final Directory testPackageOptionsImporter = - Directory('testing/test_package_options_importer'); final Directory testPackageToolError = Directory('testing/test_package_tool_error'); -final Directory testPackageCustomTemplates = - Directory('testing/test_package_custom_templates'); - -/// Convenience factory to build a [DartdocGeneratorOptionContext] and associate -/// it with a [DartdocOptionSet] based on the current working directory and/or -/// the '--input' flag. -Future generatorContextFromArgv( - List argv) async { - var optionSet = await DartdocOptionSet.fromOptionGenerators('dartdoc', [ - () => createDartdocOptions(pubPackageMetaProvider), - createGeneratorOptions, - ]); - optionSet.parseArguments(argv); - return DartdocGeneratorOptionContext(optionSet, null); -} /// Convenience factory to build a [DartdocOptionContext] and associate it with a /// [DartdocOptionSet] based on the current working directory. @@ -115,11 +29,6 @@ Future contextFromArgv(List argv) async { return DartdocOptionContext(optionSet, Directory.current); } -Future bootSdkPackage() async { - return PubPackageBuilder(await contextFromArgv(['--input', sdkDir.path])) - .buildPackageGraph(); -} - Future bootBasicPackage( String dirPath, List excludeLibraries, {List additionalArguments}) async { @@ -129,7 +38,7 @@ Future bootBasicPackage( '--input', dir.path, '--sdk-dir', - sdkDir.path, + defaultSdkDir.path, '--exclude', excludeLibraries.join(','), '--allow-tools', @@ -137,259 +46,3 @@ Future bootBasicPackage( additionalArguments)) .buildPackageGraph(); } - -/// Keeps track of coverage data automatically for any processes run by this -/// [CoverageSubprocessLauncher]. Requires that these be dart processes. -class CoverageSubprocessLauncher extends SubprocessLauncher { - CoverageSubprocessLauncher(String context, [Map environment]) - : super(context, environment) { - environment ??= {}; - environment['DARTDOC_COVERAGE_DATA'] = tempDir.path; - } - - static int nextRun = 0; - - /// Set this to true to enable coverage runs. - static bool get coverageEnabled => - Platform.environment.containsKey('COVERAGE_TOKEN'); - - /// A list of all coverage results picked up by all launchers. - static List>> coverageResults = []; - - static Directory _tempDir; - static Directory get tempDir { - if (_tempDir == null) { - if (Platform.environment['DARTDOC_COVERAGE_DATA'] != null) { - _tempDir = Directory(Platform.environment['DARTDOC_COVERAGE_DATA']); - } else { - _tempDir = Directory.systemTemp.createTempSync('dartdoc_coverage_data'); - } - } - return _tempDir; - } - - static String buildNextCoverageFilename() => - path.join(tempDir.path, 'dart-cov-${pid}-${nextRun++}.json'); - - /// Call once all coverage runs have been generated by calling runStreamed - /// on all [CoverageSubprocessLaunchers]. - static Future generateCoverageToFile(File outputFile) async { - if (!coverageEnabled) return Future.value(null); - var currentCoverageResults = coverageResults; - coverageResults = []; - var launcher = SubprocessLauncher('format_coverage'); - - /// Wait for all coverage runs to finish. - await Future.wait(currentCoverageResults); - - return launcher.runStreamed('pub', [ - 'run', - 'coverage:format_coverage', - '--lcov', - '-v', - '-b', - '.', - '--packages=.packages', - '--sdk-root=${path.canonicalize(path.join(path.dirname(Platform.executable), '..'))}', - '--out=${path.canonicalize(outputFile.path)}', - '--report-on=bin,lib', - '-i', - tempDir.path, - ]); - } - - @override - Future> runStreamed(String executable, List arguments, - {String workingDirectory, - Map environment, - bool includeParentEnvironment = true, - void Function(String) perLine}) async { - environment ??= {}; - assert( - executable == Platform.executable || - executable == Platform.resolvedExecutable, - 'Must use dart executable for tracking coverage'); - - var portAsString = Completer(); - void parsePortAsString(String line) { - if (!portAsString.isCompleted && coverageEnabled) { - var m = observatoryPortRegexp.matchAsPrefix(line); - if (m?.group(1) != null) portAsString.complete(m.group(1)); - } else { - if (perLine != null) perLine(line); - } - } - - Completer> coverageResult; - - if (coverageEnabled) { - coverageResult = Completer(); - // This must be added before awaiting in this method. - coverageResults.add(coverageResult.future); - arguments = [ - '--disable-service-auth-codes', - '--enable-vm-service:0', - '--pause-isolates-on-exit', - ...arguments - ]; - if (!environment.containsKey('DARTDOC_COVERAGE_DATA')) { - environment['DARTDOC_COVERAGE_DATA'] = tempDir.path; - } - } - - var results = super.runStreamed(executable, arguments, - environment: environment, - includeParentEnvironment: includeParentEnvironment, - workingDirectory: workingDirectory, - perLine: parsePortAsString); - - if (coverageEnabled) { - // ignore: unawaited_futures - super.runStreamed('pub', [ - 'run', - 'coverage:collect_coverage', - '--wait-paused', - '--resume-isolates', - '--port=${await portAsString.future}', - '--out=${buildNextCoverageFilename()}', - ]).then((r) => coverageResult.complete(r)); - } - return results; - } -} - -class SubprocessLauncher { - final String context; - final Map environmentDefaults; - - String get prefix => context.isNotEmpty ? '$context: ' : ''; - - // from flutter:dev/tools/dartdoc.dart, modified - static Future _printStream(Stream> stream, Stdout output, - {String prefix = '', Iterable Function(String line) filter}) { - assert(prefix != null); - filter ??= (line) => [line]; - return stream - .transform(utf8.decoder) - .transform(const LineSplitter()) - .expand(filter) - .listen((String line) { - if (line != null) { - output.write('$prefix$line'.trim()); - output.write('\n'); - } - }).asFuture(); - } - - SubprocessLauncher(this.context, [Map environment]) - : environmentDefaults = environment ?? {}; - - /// A wrapper around start/await process.exitCode that will display the - /// output of the executable continuously and fail on non-zero exit codes. - /// It will also parse any valid JSON objects (one per line) it encounters - /// on stdout/stderr, and return them. Returns null if no JSON objects - /// were encountered, or if DRY_RUN is set to 1 in the execution environment. - /// - /// Makes running programs in grinder similar to set -ex for bash, even on - /// Windows (though some of the bashisms will no longer make sense). - /// TODO(jcollins-g): refactor to return a stream of stderr/stdout lines - /// and their associated JSON objects. - Future> runStreamed(String executable, List arguments, - {String workingDirectory, - Map environment, - bool includeParentEnvironment = true, - void Function(String) perLine}) async { - environment ??= {}; - environment.addAll(environmentDefaults); - List jsonObjects; - - /// Allow us to pretend we didn't pass the JSON flag in to dartdoc by - /// printing what dartdoc would have printed without it, yet storing - /// json objects into [jsonObjects]. - Iterable jsonCallback(String line) { - if (perLine != null) perLine(line); - Map result; - try { - result = json.decoder.convert(line); - } on FormatException { - // Assume invalid JSON is actually a line of normal text. - } on TypeError { - // The convert function returns a String if there is no JSON in the - // line. Just ignore it and leave result null. - } - if (result != null) { - jsonObjects ??= []; - jsonObjects.add(result); - if (result.containsKey('message')) { - line = result['message']; - } else if (result.containsKey('data')) { - line = result['data']['text']; - } - } - return line.split('\n'); - } - - stderr.write('$prefix+ '); - if (workingDirectory != null) stderr.write('(cd "$workingDirectory" && '); - if (environment != null) { - stderr.write(environment.keys.map((String key) { - if (environment[key].contains(quotables)) { - return "$key='${environment[key]}'"; - } else { - return '$key=${environment[key]}'; - } - }).join(' ')); - stderr.write(' '); - } - stderr.write('$executable'); - if (arguments.isNotEmpty) { - for (var arg in arguments) { - if (arg.contains(quotables)) { - stderr.write(" '$arg'"); - } else { - stderr.write(' $arg'); - } - } - } - if (workingDirectory != null) stderr.write(')'); - stderr.write('\n'); - - if (Platform.environment.containsKey('DRY_RUN')) return null; - - var realExecutable = executable; - var realArguments = []; - if (Platform.isLinux) { - // Use GNU coreutils to force line buffering. This makes sure that - // subprocesses that die due to fatal signals do not chop off the - // last few lines of their output. - // - // Dart does not actually do this (seems to flush manually) unless - // the VM crashes. - realExecutable = 'stdbuf'; - realArguments.addAll(['-o', 'L', '-e', 'L']); - realArguments.add(executable); - } - realArguments.addAll(arguments); - - var process = await Process.start(realExecutable, realArguments, - workingDirectory: workingDirectory, - environment: environment, - includeParentEnvironment: includeParentEnvironment); - var stdoutFuture = _printStream(process.stdout, stdout, - prefix: prefix, filter: jsonCallback); - var stderrFuture = _printStream(process.stderr, stderr, - prefix: prefix, filter: jsonCallback); - await Future.wait([stderrFuture, stdoutFuture, process.exitCode]); - - var exitCode = await process.exitCode; - if (exitCode != 0) { - throw ProcessException( - executable, - arguments, - 'SubprocessLauncher got non-zero exitCode: $exitCode\n\n' - 'stdout: ${process.stdout}\n\nstderr: ${process.stderr}', - exitCode); - } - return jsonObjects; - } -} diff --git a/test/tool_runner_test.dart b/test/tool_runner_test.dart index d1812c4d31..d378acdd5e 100644 --- a/test/tool_runner_test.dart +++ b/test/tool_runner_test.dart @@ -12,7 +12,7 @@ import 'package:path/path.dart' as path; import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; -import 'src/utils.dart' as utils; +final Directory _testPackageDir = Directory('testing/test_package'); void main() { var toolMap; @@ -36,7 +36,7 @@ void main() { '--snapshot-kind=app-jit', 'bin/drill.dart' ], - workingDirectory: utils.testPackageDir.absolute.path); + workingDirectory: _testPackageDir.absolute.path); } on ProcessException catch (exception) { stderr.writeln('Unable to make snapshot of tool: $exception'); expect(result?.exitCode, equals(0)); @@ -73,7 +73,7 @@ echo: windows: ['C:\\Windows\\System32\\cmd.exe', '/c', 'echo'] description: 'Works on everything' '''; - var pathContext = path.Context(current: utils.testPackageDir.absolute.path); + var pathContext = path.Context(current: _testPackageDir.absolute.path); toolMap = ToolConfiguration.fromYamlMap(loadYaml(yamlMap), pathContext); // This shouldn't really happen, but if you didn't load the config from a // yaml map (which would fail on a missing executable), or a file is deleted diff --git a/test/unit/comment_processable_test.dart b/test/unit/comment_processable_test.dart index 23e1d868fa..7aafa2445c 100644 --- a/test/unit/comment_processable_test.dart +++ b/test/unit/comment_processable_test.dart @@ -587,7 +587,7 @@ class _FakePackage extends Fake implements Package { final PackageMeta packageMeta; @override - final usedAnimationIdsByHref = >{}; + final Map> usedAnimationIdsByHref = {}; _FakePackage() : packageMeta = _FakePackageMeta(); } diff --git a/tool/grind.dart b/tool/grind.dart index 420a109ab2..bb0e98a8b2 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -12,7 +12,7 @@ import 'package:io/io.dart'; import 'package:path/path.dart' as path; import 'package:yaml/yaml.dart' as yaml; -import '../test/src/utils.dart'; +import 'subprocess_launcher.dart'; void main([List args]) => grind(args); diff --git a/tool/subprocess_launcher.dart b/tool/subprocess_launcher.dart new file mode 100644 index 0000000000..31c81c2ef5 --- /dev/null +++ b/tool/subprocess_launcher.dart @@ -0,0 +1,270 @@ +// Copyright (c) 2020, 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'; + +import 'package:path/path.dart' as p; + +/// Keeps track of coverage data automatically for any processes run by this +/// [CoverageSubprocessLauncher]. Requires that these be dart processes. +class CoverageSubprocessLauncher extends SubprocessLauncher { + CoverageSubprocessLauncher(String context, [Map environment]) + : super(context, environment) { + environment ??= {}; + environment['DARTDOC_COVERAGE_DATA'] = tempDir.path; + } + + static int nextRun = 0; + + /// Set this to true to enable coverage runs. + static bool get coverageEnabled => + Platform.environment.containsKey('COVERAGE_TOKEN'); + + /// A list of all coverage results picked up by all launchers. + static List>> coverageResults = []; + + static Directory _tempDir; + static Directory get tempDir { + if (_tempDir == null) { + if (Platform.environment['DARTDOC_COVERAGE_DATA'] != null) { + _tempDir = Directory(Platform.environment['DARTDOC_COVERAGE_DATA']); + } else { + _tempDir = Directory.systemTemp.createTempSync('dartdoc_coverage_data'); + } + } + return _tempDir; + } + + static String buildNextCoverageFilename() => + p.join(tempDir.path, 'dart-cov-${pid}-${nextRun++}.json'); + + /// Call once all coverage runs have been generated by calling runStreamed + /// on all [CoverageSubprocessLaunchers]. + static Future generateCoverageToFile(File outputFile) async { + if (!coverageEnabled) return Future.value(null); + var currentCoverageResults = coverageResults; + coverageResults = []; + var launcher = SubprocessLauncher('format_coverage'); + + /// Wait for all coverage runs to finish. + await Future.wait(currentCoverageResults); + + return launcher.runStreamed('pub', [ + 'run', + 'coverage:format_coverage', + '--lcov', + '-v', + '-b', + '.', + '--packages=.packages', + '--sdk-root=${p.canonicalize(p.join(p.dirname(Platform.executable), '..'))}', + '--out=${p.canonicalize(outputFile.path)}', + '--report-on=bin,lib', + '-i', + tempDir.path, + ]); + } + + @override + Future> runStreamed(String executable, List arguments, + {String workingDirectory, + Map environment, + bool includeParentEnvironment = true, + void Function(String) perLine}) async { + environment ??= {}; + assert( + executable == Platform.executable || + executable == Platform.resolvedExecutable, + 'Must use dart executable for tracking coverage'); + + var portAsString = Completer(); + void parsePortAsString(String line) { + if (!portAsString.isCompleted && coverageEnabled) { + var m = _observatoryPortRegexp.matchAsPrefix(line); + if (m?.group(1) != null) portAsString.complete(m.group(1)); + } else { + if (perLine != null) perLine(line); + } + } + + Completer> coverageResult; + + if (coverageEnabled) { + coverageResult = Completer(); + // This must be added before awaiting in this method. + coverageResults.add(coverageResult.future); + arguments = [ + '--disable-service-auth-codes', + '--enable-vm-service:0', + '--pause-isolates-on-exit', + ...arguments + ]; + if (!environment.containsKey('DARTDOC_COVERAGE_DATA')) { + environment['DARTDOC_COVERAGE_DATA'] = tempDir.path; + } + } + + var results = super.runStreamed(executable, arguments, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + workingDirectory: workingDirectory, + perLine: parsePortAsString); + + if (coverageEnabled) { + // ignore: unawaited_futures + super.runStreamed('pub', [ + 'run', + 'coverage:collect_coverage', + '--wait-paused', + '--resume-isolates', + '--port=${await portAsString.future}', + '--out=${buildNextCoverageFilename()}', + ]).then((r) => coverageResult.complete(r)); + } + return results; + } + + static final _observatoryPortRegexp = + RegExp(r'^Observatory listening on http://.*:(\d+)'); +} + +class SubprocessLauncher { + final String context; + final Map environmentDefaults; + + String get prefix => context.isNotEmpty ? '$context: ' : ''; + + // from flutter:dev/tools/dartdoc.dart, modified + static Future _printStream(Stream> stream, Stdout output, + {String prefix = '', Iterable Function(String line) filter}) { + assert(prefix != null); + filter ??= (line) => [line]; + return stream + .transform(utf8.decoder) + .transform(const LineSplitter()) + .expand(filter) + .listen((String line) { + if (line != null) { + output.write('$prefix$line'.trim()); + output.write('\n'); + } + }).asFuture(); + } + + SubprocessLauncher(this.context, [Map environment]) + : environmentDefaults = environment ?? {}; + + /// A wrapper around start/await process.exitCode that will display the + /// output of the executable continuously and fail on non-zero exit codes. + /// It will also parse any valid JSON objects (one per line) it encounters + /// on stdout/stderr, and return them. Returns null if no JSON objects + /// were encountered, or if DRY_RUN is set to 1 in the execution environment. + /// + /// Makes running programs in grinder similar to set -ex for bash, even on + /// Windows (though some of the bashisms will no longer make sense). + /// TODO(jcollins-g): refactor to return a stream of stderr/stdout lines + /// and their associated JSON objects. + Future> runStreamed(String executable, List arguments, + {String workingDirectory, + Map environment, + bool includeParentEnvironment = true, + void Function(String) perLine}) async { + environment ??= {}; + environment.addAll(environmentDefaults); + List jsonObjects; + + /// Allow us to pretend we didn't pass the JSON flag in to dartdoc by + /// printing what dartdoc would have printed without it, yet storing + /// json objects into [jsonObjects]. + Iterable jsonCallback(String line) { + if (perLine != null) perLine(line); + Map result; + try { + result = json.decoder.convert(line); + } on FormatException { + // Assume invalid JSON is actually a line of normal text. + } on TypeError { + // The convert function returns a String if there is no JSON in the + // line. Just ignore it and leave result null. + } + if (result != null) { + jsonObjects ??= []; + jsonObjects.add(result); + if (result.containsKey('message')) { + line = result['message']; + } else if (result.containsKey('data')) { + line = result['data']['text']; + } + } + return line.split('\n'); + } + + stderr.write('$prefix+ '); + if (workingDirectory != null) stderr.write('(cd "$workingDirectory" && '); + if (environment != null) { + stderr.write(environment.keys.map((String key) { + if (environment[key].contains(_quotables)) { + return "$key='${environment[key]}'"; + } else { + return '$key=${environment[key]}'; + } + }).join(' ')); + stderr.write(' '); + } + stderr.write('$executable'); + if (arguments.isNotEmpty) { + for (var arg in arguments) { + if (arg.contains(_quotables)) { + stderr.write(" '$arg'"); + } else { + stderr.write(' $arg'); + } + } + } + if (workingDirectory != null) stderr.write(')'); + stderr.write('\n'); + + if (Platform.environment.containsKey('DRY_RUN')) return null; + + var realExecutable = executable; + var realArguments = []; + if (Platform.isLinux) { + // Use GNU coreutils to force line buffering. This makes sure that + // subprocesses that die due to fatal signals do not chop off the + // last few lines of their output. + // + // Dart does not actually do this (seems to flush manually) unless + // the VM crashes. + realExecutable = 'stdbuf'; + realArguments.addAll(['-o', 'L', '-e', 'L']); + realArguments.add(executable); + } + realArguments.addAll(arguments); + + var process = await Process.start(realExecutable, realArguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment); + var stdoutFuture = _printStream(process.stdout, stdout, + prefix: prefix, filter: jsonCallback); + var stderrFuture = _printStream(process.stderr, stderr, + prefix: prefix, filter: jsonCallback); + await Future.wait([stderrFuture, stdoutFuture, process.exitCode]); + + var exitCode = await process.exitCode; + if (exitCode != 0) { + throw ProcessException( + executable, + arguments, + 'SubprocessLauncher got non-zero exitCode: $exitCode\n\n' + 'stdout: ${process.stdout}\n\nstderr: ${process.stderr}', + exitCode); + } + return jsonObjects; + } + + static final _quotables = RegExp(r'[ "\r\n\$]'); +}