From 977f11709642e2872852686240492ce757160c4f Mon Sep 17 00:00:00 2001
From: Sam Rawlins <srawlins@google.com>
Date: Mon, 17 Aug 2020 17:20:30 -0700
Subject: [PATCH] Refactor shared code in tests and tools. (#2308)

* Move SubprocessLauncher out of test/, into tool/. It's imported by both.
* Move most top-level Directory objects out of utils.dart, into singular test
  files where they are used, mostly dartdoc_test.dart.
* Move `generatorContextFromArgv` from utils.dart to dartdoc_test.dart.
* Clean up set-up and tear-down methods in dartdoc_test.dart.
* Move most top-level PackageGraph code from utils.dart to
  model_special_cases_test.dart and model_test.dart.

Also type-annotate public API in warnings.dart.
---
 test/dartdoc_integration_test.dart      |   1 +
 test/dartdoc_test.dart                  |  92 ++++---
 test/html_generator_test.dart           |   5 +-
 test/model_special_cases_test.dart      |  61 ++++-
 test/model_test.dart                    |   9 +-
 test/src/utils.dart                     | 349 +-----------------------
 test/tool_runner_test.dart              |   6 +-
 test/unit/comment_processable_test.dart |   2 +-
 tool/grind.dart                         |   2 +-
 tool/subprocess_launcher.dart           | 270 ++++++++++++++++++
 10 files changed, 391 insertions(+), 406 deletions(-)
 create mode 100644 tool/subprocess_launcher.dart

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<DartdocGeneratorOptionContext> _generatorContextFromArgv(
+    List<String> 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<Dartdoc> buildDartdoc(
         List<String> 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 <link> 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<PackageGraph>();
+Future<PackageGraph> get _testPackageGraphExperiments =>
+    _testPackageGraphExperimentsMemo.runOnce(() => utils.bootBasicPackage(
+            'testing/test_package_experiments', [], additionalArguments: [
+          '--enable-experiment',
+          'non-nullable',
+          '--no-link-to-remote'
+        ]));
+
+final _testPackageGraphGinormousMemo = AsyncMemoizer<PackageGraph>();
+Future<PackageGraph> 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<PackageGraph>();
+Future<PackageGraph> get _testPackageGraphSmall =>
+    _testPackageGraphSmallMemo.runOnce(() => utils.bootBasicPackage(
+        'testing/test_package_small', [],
+        additionalArguments: ['--no-link-to-remote']));
+
+final _testPackageGraphSdkMemo = AsyncMemoizer<PackageGraph>();
+Future<PackageGraph> get _testPackageGraphSdk =>
+    _testPackageGraphSdkMemo.runOnce(_bootSdkPackage);
+
+Future<PackageGraph> _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<PackageGraph>();
+Future<PackageGraph> 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 <nodoc> 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<PackageGraph>();
-Future<PackageGraph> get testPackageGraph => _testPackageGraphMemo.runOnce(() =>
-    bootBasicPackage('testing/test_package', ['css', 'code_in_comments'],
-        additionalArguments: ['--no-link-to-remote']));
-
-final _testPackageGraphExperimentsMemo = AsyncMemoizer<PackageGraph>();
-Future<PackageGraph> get testPackageGraphExperiments =>
-    _testPackageGraphExperimentsMemo.runOnce(() => bootBasicPackage(
-            'testing/test_package_experiments', [], additionalArguments: [
-          '--enable-experiment',
-          'non-nullable',
-          '--no-link-to-remote'
-        ]));
-
-final _testPackageGraphGinormousMemo = AsyncMemoizer<PackageGraph>();
-Future<PackageGraph> get testPackageGraphGinormous =>
-    _testPackageGraphGinormousMemo.runOnce(() => bootBasicPackage(
-            'testing/test_package', [
-          'css',
-          'code_in_commnets',
-          'excluded'
-        ], additionalArguments: [
-          '--auto-include-dependencies',
-          '--no-link-to-remote'
-        ]));
-
-final _testPackageGraphSmallMemo = AsyncMemoizer<PackageGraph>();
-Future<PackageGraph> get testPackageGraphSmall =>
-    _testPackageGraphSmallMemo.runOnce(() => bootBasicPackage(
-        'testing/test_package_small', [],
-        additionalArguments: ['--no-link-to-remote']));
-
-final _testPackageGraphErrorsMemo = AsyncMemoizer<PackageGraph>();
-Future<PackageGraph> get testPackageGraphErrors =>
-    _testPackageGraphErrorsMemo.runOnce(() => bootBasicPackage(
-        'testing/test_package_doc_errors',
-        ['css', 'code_in_comments', 'excluded'],
-        additionalArguments: ['--no-link-to-remote']));
-
-final _testPackageGraphSdkMemo = AsyncMemoizer<PackageGraph>();
-Future<PackageGraph> 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<DartdocGeneratorOptionContext> generatorContextFromArgv(
-    List<String> 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<DartdocOptionContext> contextFromArgv(List<String> argv) async {
   return DartdocOptionContext(optionSet, Directory.current);
 }
 
-Future<PackageGraph> bootSdkPackage() async {
-  return PubPackageBuilder(await contextFromArgv(['--input', sdkDir.path]))
-      .buildPackageGraph();
-}
-
 Future<PackageGraph> bootBasicPackage(
     String dirPath, List<String> excludeLibraries,
     {List<String> additionalArguments}) async {
@@ -129,7 +38,7 @@ Future<PackageGraph> bootBasicPackage(
             '--input',
             dir.path,
             '--sdk-dir',
-            sdkDir.path,
+            defaultSdkDir.path,
             '--exclude',
             excludeLibraries.join(','),
             '--allow-tools',
@@ -137,259 +46,3 @@ Future<PackageGraph> 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<String, String> 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<Future<Iterable<Map>>> 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<void> 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<Iterable<Map>> runStreamed(String executable, List<String> arguments,
-      {String workingDirectory,
-      Map<String, String> 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<String>();
-    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<Iterable<Map>> 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<String, String> environmentDefaults;
-
-  String get prefix => context.isNotEmpty ? '$context: ' : '';
-
-  // from flutter:dev/tools/dartdoc.dart, modified
-  static Future<void> _printStream(Stream<List<int>> stream, Stdout output,
-      {String prefix = '', Iterable<String> 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<String, String> environment])
-      : environmentDefaults = environment ?? <String, String>{};
-
-  /// 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<Iterable<Map>> runStreamed(String executable, List<String> arguments,
-      {String workingDirectory,
-      Map<String, String> environment,
-      bool includeParentEnvironment = true,
-      void Function(String) perLine}) async {
-    environment ??= {};
-    environment.addAll(environmentDefaults);
-    List<Map> 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<String> 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 = <String>[];
-    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 = <String, Set<String>>{};
+  final Map<String, Set<String>> 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<String> 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<String, String> 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<Future<Iterable<Map>>> 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<void> 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<Iterable<Map>> runStreamed(String executable, List<String> arguments,
+      {String workingDirectory,
+      Map<String, String> 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<String>();
+    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<Iterable<Map>> 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<String, String> environmentDefaults;
+
+  String get prefix => context.isNotEmpty ? '$context: ' : '';
+
+  // from flutter:dev/tools/dartdoc.dart, modified
+  static Future<void> _printStream(Stream<List<int>> stream, Stdout output,
+      {String prefix = '', Iterable<String> 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<String, String> environment])
+      : environmentDefaults = environment ?? <String, String>{};
+
+  /// 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<Iterable<Map>> runStreamed(String executable, List<String> arguments,
+      {String workingDirectory,
+      Map<String, String> environment,
+      bool includeParentEnvironment = true,
+      void Function(String) perLine}) async {
+    environment ??= {};
+    environment.addAll(environmentDefaults);
+    List<Map> 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<String> 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 = <String>[];
+    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\$]');
+}