Construct PackageGraphs and run tools asynchronously (#1849)

* asynchronous PackageGraph construction and precaching

* Run tools more asynchronously

* Enhacements and dropping of Future.wait

* dartfmt

* Rate-limit the number of tools in flight

* dartfmt and review comments
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 055e5e7..e472853 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -17,4 +17,5 @@
     - package_api_docs
     - slash_for_doc_comments
     - prefer_final_fields
+    - unawaited_futures
 #    - unnecessary_brace_in_string_interps
diff --git a/lib/src/dartdoc_options.dart b/lib/src/dartdoc_options.dart
index e523843..2d77708 100644
--- a/lib/src/dartdoc_options.dart
+++ b/lib/src/dartdoc_options.dart
@@ -20,6 +20,7 @@
 import 'package:analyzer/dart/element/element.dart';
 import 'package:args/args.dart';
 import 'package:dartdoc/dartdoc.dart';
+import 'package:dartdoc/src/tuple.dart';
 import 'package:path/path.dart' as pathLib;
 import 'package:yaml/yaml.dart';
 
@@ -226,22 +227,23 @@
   /// to run. If no snapshot file existed, then create one and modify the args
   /// so that if they are executed with dart, will result in the snapshot being
   /// built.
-  String createSnapshotIfNeeded(List<String> args) {
-    assert(ToolDefinition.isDartExecutable(args[0]));
-    // Generate a new snapshot, if needed, and use the first run as the training
+  Future<Tuple2<String, Function()>> modifyArgsToCreateSnapshotIfNeeded(
+      List<String> args) async {
+    assert(args[0] == command.first);
+    // Set up flags to create a new snapshot, if needed, and use the first run as the training
     // run.
-    File snapshotPath = _snapshotPath;
-    snapshotPath ??= SnapshotCache.instance.getSnapshot(args[0]);
-    if (snapshotPath.existsSync()) {
+    File snapshotFile = await getSnapshotFile();
+    if (snapshotFile.existsSync()) {
       // replace the first argument with the path to the snapshot.
-      args[0] = snapshotPath.absolute.path;
+      args[0] = snapshotFile.absolute.path;
     } else {
       args.insertAll(0, [
-        '--snapshot=${snapshotPath.absolute.path}',
+        '--snapshot=${snapshotFile.absolute.path}',
         '--snapshot_kind=app-jit'
       ]);
     }
-    return Platform.resolvedExecutable;
+    return new Tuple2(Platform.resolvedExecutable,
+        _snapshotCompleter.isCompleted ? null : _snapshotCompleter.complete);
   }
 
   DartToolDefinition(
@@ -250,11 +252,23 @@
     // If the dart tool is already a snapshot, then we just use that.
     if (command[0].endsWith('.snapshot')) {
       _snapshotPath = File(command[0]);
+      _snapshotCompleter.complete();
     }
   }
 
+  final Completer _snapshotCompleter = new Completer();
+
   /// If the tool has a pre-built snapshot, it will be stored here.
   File _snapshotPath;
+
+  Future<File> getSnapshotFile() async {
+    if (_snapshotPath == null) {
+      _snapshotPath = SnapshotCache.instance.getSnapshot(command.first);
+    } else {
+      await _snapshotCompleter.future;
+    }
+    return _snapshotPath;
+  }
 }
 
 /// A configuration class that can interpret [ToolDefinition]s from a YAML map.
diff --git a/lib/src/io_utils.dart b/lib/src/io_utils.dart
index 3ff3a32..913a0cd 100644
--- a/lib/src/io_utils.dart
+++ b/lib/src/io_utils.dart
@@ -75,18 +75,6 @@
 
   MultiFutureTracker(this.parallel);
 
-  /// Adds a Future to the queue of outstanding Futures, and returns a Future
-  /// that completes only when the number of Futures outstanding is < [parallel]
-  /// (and so it is OK to start another).
-  ///
-  /// That can be extremely brief and there's no longer a guarantee after that
-  /// point that another async task has not added a Future to the list.
-  Future<void> addFuture(Future<T> future) async {
-    _queue.add(future);
-    future.then((f) => _queue.remove(future));
-    await _waitUntil(parallel - 1);
-  }
-
   /// Wait until fewer or equal to this many Futures are outstanding.
   Future<void> _waitUntil(int max) async {
     while (_queue.length > max) {
@@ -94,6 +82,18 @@
     }
   }
 
+  /// Generates a [Future] from the given closure and adds it to the queue,
+  /// once the queue is sufficiently empty.
+  Future<void> addFutureFromClosure(Future<T> Function() closure) async {
+    while (_queue.length > parallel - 1) {
+      await Future.any(_queue);
+    }
+    Future future = closure();
+    _queue.add(future);
+    // ignore: unawaited_futures
+    future.then((f) => _queue.remove(future));
+  }
+
   /// Wait until all futures added so far have completed.
   Future<void> wait() async => await _waitUntil(0);
 }
diff --git a/lib/src/model.dart b/lib/src/model.dart
index 7dec3eb..1c92fc1 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -627,7 +627,8 @@
         .toList(growable: false);
   }
 
-  Iterable<Method> get allInstanceMethods => quiverIterables.concat([instanceMethods, inheritedMethods]);
+  Iterable<Method> get allInstanceMethods =>
+      quiverIterables.concat([instanceMethods, inheritedMethods]);
 
   Iterable<Method> get allPublicInstanceMethods =>
       filterNonPublic(allInstanceMethods);
@@ -635,9 +636,13 @@
   bool get allPublicInstanceMethodsInherited =>
       instanceMethods.every((f) => f.isInherited);
 
-  Iterable<Field> get allInstanceFields => quiverIterables.concat([instanceProperties, inheritedProperties]);
+  Iterable<Field> get allInstanceFields =>
+      quiverIterables.concat([instanceProperties, inheritedProperties]);
 
-  Iterable<Accessor> get allAccessors => quiverIterables.concat([allInstanceFields.expand((f) => f.allAccessors), constants.map((c) => c.getter)]);
+  Iterable<Accessor> get allAccessors => quiverIterables.concat([
+        allInstanceFields.expand((f) => f.allAccessors),
+        constants.map((c) => c.getter)
+      ]);
 
   Iterable<Field> get allPublicInstanceProperties =>
       filterNonPublic(allInstanceFields);
@@ -645,7 +650,8 @@
   bool get allPublicInstancePropertiesInherited =>
       allPublicInstanceProperties.every((f) => f.isInherited);
 
-  Iterable<Operator> get allOperators => quiverIterables.concat([operators, inheritedOperators]);
+  Iterable<Operator> get allOperators =>
+      quiverIterables.concat([operators, inheritedOperators]);
 
   Iterable<Operator> get allPublicOperators => filterNonPublic(allOperators);
 
@@ -1295,11 +1301,8 @@
 /// Mixin implementing dartdoc categorization for ModelElements.
 abstract class Categorization implements ModelElement {
   @override
-  String _buildDocumentationLocal() {
-    _rawDocs = _buildDocumentationBase();
-    _rawDocs = _stripAndSetDartdocCategories(_rawDocs);
-    return _rawDocs;
-  }
+  String _buildDocumentationAddition(String rawDocs) =>
+      _stripAndSetDartdocCategories(rawDocs);
 
   /// Parse {@category ...} and related information in API comments, stripping
   /// out that information from the given comments and returning the stripped
@@ -3227,23 +3230,48 @@
     return docFrom;
   }
 
-  String _buildDocumentationLocal() => _buildDocumentationBase();
+  String _buildDocumentationLocal() => _buildDocumentationBaseSync();
+
+  /// Override this to add more features to the documentation builder in a
+  /// subclass.
+  String _buildDocumentationAddition(String docs) => docs;
 
   /// Separate from _buildDocumentationLocal for overriding.
-  String _buildDocumentationBase() {
+  String _buildDocumentationBaseSync() {
     assert(_rawDocs == null);
+    // Do not use the sync method if we need to evaluate tools or templates.
+    assert(packageGraph._localDocumentationBuilt);
+    if (config.dropTextFrom.contains(element.library.name)) {
+      _rawDocs = '';
+    } else {
+      _rawDocs = documentationComment ?? '';
+      _rawDocs = stripComments(_rawDocs) ?? '';
+      _rawDocs = _injectExamples(_rawDocs);
+      _rawDocs = _injectAnimations(_rawDocs);
+      _rawDocs = _stripHtmlAndAddToIndex(_rawDocs);
+    }
+    _rawDocs = _buildDocumentationAddition(_rawDocs);
+    return _rawDocs;
+  }
+
+  /// Separate from _buildDocumentationLocal for overriding.  Can only be
+  /// used as part of [PackageGraph.setUpPackageGraph].
+  Future<String> _buildDocumentationBase() async {
+    assert(_rawDocs == null);
+    // Do not use the sync method if we need to evaluate tools or templates.
     if (config.dropTextFrom.contains(element.library.name)) {
       _rawDocs = '';
     } else {
       _rawDocs = documentationComment ?? '';
       _rawDocs = stripComments(_rawDocs) ?? '';
       // Must evaluate tools first, in case they insert any other directives.
-      _rawDocs = _evaluateTools(_rawDocs);
+      _rawDocs = await _evaluateTools(_rawDocs);
       _rawDocs = _injectExamples(_rawDocs);
       _rawDocs = _injectAnimations(_rawDocs);
       _rawDocs = _stripMacroTemplatesAndAddToIndex(_rawDocs);
       _rawDocs = _stripHtmlAndAddToIndex(_rawDocs);
     }
+    _rawDocs = _buildDocumentationAddition(_rawDocs);
     return _rawDocs;
   }
 
@@ -3674,11 +3702,11 @@
     return _documentationComment;
   }
 
-  /// Call this method to precache docs for this object if it might possibly
-  /// have a macro template or a tool definition.
-  void precacheLocalDocsIfNeeded() {
-    if (documentationComment != null &&
-        needsPrecacheRegExp.hasMatch(documentationComment)) documentationLocal;
+  /// Unconditionally precache local documentation.
+  ///
+  /// Use only in factory for [PackageGraph].
+  Future _precacheLocalDocs() async {
+    _documentationLocal = await _buildDocumentationBase();
   }
 
   Documentation get _documentation {
@@ -3895,6 +3923,19 @@
     });
   }
 
+  static Future<String> _replaceAllMappedAsync(
+      String string, Pattern exp, Future<String> replace(Match match)) async {
+    StringBuffer replaced = new StringBuffer();
+    int currentIndex = 0;
+    for (Match match in exp.allMatches(string)) {
+      String prefix = match.input.substring(currentIndex, match.start);
+      currentIndex = match.end;
+      replaced..write(prefix)..write(await replace(match));
+    }
+    replaced.write(string.substring(currentIndex));
+    return replaced.toString();
+  }
+
   /// Replace &#123;@tool ...&#125&#123;@end-tool&#125; in API comments with the
   /// output of an external tool.
   ///
@@ -3946,43 +3987,39 @@
   ///
   /// ## Content to send to tool.
   /// 2018-09-18T21:15+00:00
-  String _evaluateTools(String rawDocs) {
-    var runner = new ToolRunner(config.tools, (String message) {
+  Future<String> _evaluateTools(String rawDocs) async {
+    var runner = new ToolRunner(config.tools, (String message) async {
       warn(PackageWarning.toolError, message: message);
     });
     int invocationIndex = 0;
-    try {
-      return rawDocs.replaceAllMapped(basicToolRegExp, (basicMatch) {
-        List<String> args = _splitUpQuotedArgs(basicMatch[1]).toList();
-        // Tool name must come first.
-        if (args.isEmpty) {
-          warn(PackageWarning.toolError,
-              message:
-                  'Must specify a tool to execute for the @tool directive.');
-          return '';
-        }
-        // Count the number of invocations of tools in this dartdoc block,
-        // so that tools can differentiate different blocks from each other.
-        invocationIndex++;
-        return runner.run(args,
-            content: basicMatch[2],
-            environment: {
-              'SOURCE_LINE': lineAndColumn?.item1?.toString(),
-              'SOURCE_COLUMN': lineAndColumn?.item2?.toString(),
-              'SOURCE_PATH': (sourceFileName == null ||
-                      package?.packagePath == null)
-                  ? null
-                  : pathLib.relative(sourceFileName, from: package.packagePath),
-              'PACKAGE_PATH': package?.packagePath,
-              'PACKAGE_NAME': package?.name,
-              'LIBRARY_NAME': library?.fullyQualifiedName,
-              'ELEMENT_NAME': fullyQualifiedNameWithoutLibrary,
-              'INVOCATION_INDEX': invocationIndex.toString(),
-            }..removeWhere((key, value) => value == null));
-      });
-    } finally {
-      runner.dispose();
-    }
+    return await _replaceAllMappedAsync(rawDocs, basicToolRegExp,
+        (basicMatch) async {
+      List<String> args = _splitUpQuotedArgs(basicMatch[1]).toList();
+      // Tool name must come first.
+      if (args.isEmpty) {
+        warn(PackageWarning.toolError,
+            message: 'Must specify a tool to execute for the @tool directive.');
+        return Future.value('');
+      }
+      // Count the number of invocations of tools in this dartdoc block,
+      // so that tools can differentiate different blocks from each other.
+      invocationIndex++;
+      return await runner.run(args,
+          content: basicMatch[2],
+          environment: {
+            'SOURCE_LINE': lineAndColumn?.item1?.toString(),
+            'SOURCE_COLUMN': lineAndColumn?.item2?.toString(),
+            'SOURCE_PATH': (sourceFileName == null ||
+                    package?.packagePath == null)
+                ? null
+                : pathLib.relative(sourceFileName, from: package.packagePath),
+            'PACKAGE_PATH': package?.packagePath,
+            'PACKAGE_NAME': package?.name,
+            'LIBRARY_NAME': library?.fullyQualifiedName,
+            'ELEMENT_NAME': fullyQualifiedNameWithoutLibrary,
+            'INVOCATION_INDEX': invocationIndex.toString(),
+          }..removeWhere((key, value) => value == null));
+    }).whenComplete(runner.dispose);
   }
 
   /// Replace &#123;@animation ...&#125; in API comments with some HTML to manage an
@@ -4554,21 +4591,24 @@
   // building Libraries and adding them to Packages, then adding Packages
   // to this graph.
 
-  /// Construct a package graph.
-  /// [libraryElements] - Libraries to be documented.
-  /// [specialLibraryElements] - Any libraries that may not be documented, but
-  /// contain required [SpecialClass]es.
-  PackageGraph(
-      Iterable<LibraryElement> libraryElements,
-      Iterable<LibraryElement> specialLibraryElements,
-      this.config,
-      this.packageMeta,
-      this._packageWarningOptions,
-      this.driver,
-      this.sdk) {
-    assert(_allConstructedModelElements.isEmpty);
-    assert(allLibraries.isEmpty);
-    _packageWarningCounter = new PackageWarningCounter(_packageWarningOptions);
+  PackageGraph._(this.config, this.packageMeta, this._packageWarningOptions,
+      this.driver, this.sdk) {}
+
+  static Future<PackageGraph> setUpPackageGraph(
+    Iterable<LibraryElement> libraryElements,
+    Iterable<LibraryElement> specialLibraryElements,
+    DartdocOptionContext config,
+    PackageMeta packageMeta,
+    packageWarningOptions,
+    driver,
+    sdk,
+  ) async {
+    PackageGraph newGraph =
+        PackageGraph._(config, packageMeta, packageWarningOptions, driver, sdk);
+    assert(newGraph._allConstructedModelElements.isEmpty);
+    assert(newGraph.allLibraries.isEmpty);
+    newGraph._packageWarningCounter =
+        new PackageWarningCounter(newGraph._packageWarningOptions);
 
     // Build [Package] objects.
     libraryElements.forEach((element) {});
@@ -4576,45 +4616,57 @@
     // Build [Library] objects, and link them to [Package]s.
     libraryElements.forEach((element) {
       var packageMeta = new PackageMeta.fromElement(element, config);
-      var lib = new Library._(
-          element, this, new Package.fromPackageMeta(packageMeta, this));
-      packageMap[packageMeta.name]._libraries.add(lib);
-      allLibraries[element] = lib;
+      var lib = new Library._(element, newGraph,
+          new Package.fromPackageMeta(packageMeta, newGraph));
+      newGraph.packageMap[packageMeta.name]._libraries.add(lib);
+      newGraph.allLibraries[element] = lib;
     });
 
     // Make sure the default package exists, even if it has no libraries.
     // This can happen for packages that only contain embedder SDKs.
-    new Package.fromPackageMeta(packageMeta, this);
-    allLibrariesAdded = true;
+    new Package.fromPackageMeta(packageMeta, newGraph);
+    newGraph.allLibrariesAdded = true;
 
     // [findOrCreateLibraryFor] already adds to the proper structures.
     specialLibraryElements.forEach((element) {
-      findOrCreateLibraryFor(element);
+      newGraph.findOrCreateLibraryFor(element);
     });
 
     // From here on in, we might find special objects.  Initialize the
     // specialClasses handler so when we find them, they get added.
-    specialClasses = new SpecialClasses();
+    newGraph.specialClasses = new SpecialClasses();
     // Go through docs of every ModelElement in package to pre-build the macros
     // index.
-    allModelElements.forEach((m) => m.precacheLocalDocsIfNeeded());
-    _localDocumentationBuilt = true;
+    List<Future> precacheFutures = newGraph.precacheLocalDocs().toList();
+    for (Future f in precacheFutures) await f;
+    newGraph._localDocumentationBuilt = true;
 
     // Scan all model elements to insure that interceptor and other special
     // objects are found.
     // After the allModelElements traversal to be sure that all packages
     // are picked up.
-    documentedPackages.toList().forEach((package) {
+    newGraph.documentedPackages.toList().forEach((package) {
       package._libraries.sort((a, b) => compareNatural(a.name, b.name));
       package._libraries.forEach((library) {
-        library._allClasses.forEach(_addToImplementors);
+        library._allClasses.forEach(newGraph._addToImplementors);
       });
     });
-    _implementors.values.forEach((l) => l.sort());
-    allImplementorsAdded = true;
+    newGraph._implementors.values.forEach((l) => l.sort());
+    newGraph.allImplementorsAdded = true;
 
     // We should have found all special classes by now.
-    specialClasses.assertSpecials();
+    newGraph.specialClasses.assertSpecials();
+    return newGraph;
+  }
+
+  /// Generate a list of futures for any docs that actually require precaching.
+  Iterable<Future> precacheLocalDocs() sync* {
+    for (ModelElement m in allModelElements) {
+      if (m.documentationComment != null &&
+          needsPrecacheRegExp.hasMatch(m.documentationComment)) {
+        yield m._precacheLocalDocs();
+      }
+    }
   }
 
   SpecialClasses specialClasses;
@@ -6396,8 +6448,8 @@
     }
     await getLibraries(libraries, specialLibraries, getFiles,
         specialLibraryFiles(findSpecialsSdk).toSet());
-    return new PackageGraph(libraries, specialLibraries, config,
-        config.topLevelPackageMeta, getWarningOptions(), driver, sdk);
+    return await PackageGraph.setUpPackageGraph(libraries, specialLibraries,
+        config, config.topLevelPackageMeta, getWarningOptions(), driver, sdk);
   }
 
   DartSdk _sdk;
diff --git a/lib/src/tool_runner.dart b/lib/src/tool_runner.dart
index aa79d4f..c31a7ae 100644
--- a/lib/src/tool_runner.dart
+++ b/lib/src/tool_runner.dart
@@ -7,6 +7,7 @@
 import 'dart:async';
 import 'dart:io';
 
+import 'package:dartdoc/src/io_utils.dart';
 import 'package:path/path.dart' as pathLib;
 import 'dartdoc_options.dart';
 
@@ -14,6 +15,10 @@
 typedef FakeResultCallback = String Function(String tool,
     {List<String> args, String content});
 
+/// Set a ceiling on how many tool instances can be in progress at once,
+/// limiting both parallelization and the number of open temporary files.
+final MultiFutureTracker _toolTracker = new MultiFutureTracker(4);
+
 /// A helper class for running external tools.
 class ToolRunner {
   /// Creates a new ToolRunner.
@@ -27,11 +32,10 @@
   final ToolErrorCallback _errorCallback;
   int _temporaryFileCount = 0;
 
-  Directory _temporaryDirectory;
-  Directory get temporaryDirectory {
+  Future<Directory> _temporaryDirectory;
+  Future<Directory> get temporaryDirectory {
     if (_temporaryDirectory == null) {
-      _temporaryDirectory =
-          Directory.systemTemp.createTempSync('dartdoc_tools_');
+      _temporaryDirectory = Directory.systemTemp.createTemp('dartdoc_tools_');
     }
     return _temporaryDirectory;
   }
@@ -42,11 +46,13 @@
     }
   }
 
-  File _createTemporaryFile() {
+  Future<File> _createTemporaryFile() async {
     _temporaryFileCount++;
-    return new File(pathLib.join(
-        temporaryDirectory.absolute.path, 'input_$_temporaryFileCount'))
-      ..createSync(recursive: true);
+    File tempFile = new File(pathLib.join(
+        (await temporaryDirectory).absolute.path,
+        'input_$_temporaryFileCount'));
+    await tempFile.create(recursive: true);
+    return tempFile;
   }
 
   /// Must be called when the ToolRunner is no longer needed. Ideally, this is
@@ -58,14 +64,15 @@
   }
 
   /// Avoid blocking on I/O for cleanups.
-  static Future<void> disposeAsync(Directory temporaryDirectory) async {
-    temporaryDirectory.exists().then((bool exists) {
-      if (exists) return temporaryDirectory.delete(recursive: true);
-    });
+  static Future<void> disposeAsync(Future<Directory> temporaryDirectory) async {
+    Directory tempDir = await temporaryDirectory;
+    if (await tempDir.exists()) {
+      return tempDir.delete(recursive: true);
+    }
   }
 
   void _runSetup(
-      String name, ToolDefinition tool, Map<String, String> environment) {
+      String name, ToolDefinition tool, Map<String, String> environment) async {
     bool isDartSetup = ToolDefinition.isDartExecutable(tool.setupCommand[0]);
     var args = tool.setupCommand.toList();
     String commandPath;
@@ -75,15 +82,16 @@
     } else {
       commandPath = args.removeAt(0);
     }
-    _runProcess(name, '', commandPath, args, environment);
+    await _runProcess(name, '', commandPath, args, environment);
     tool.setupComplete = true;
   }
 
-  String _runProcess(String name, String content, String commandPath,
-      List<String> args, Map<String, String> environment) {
+  Future<String> _runProcess(String name, String content, String commandPath,
+      List<String> args, Map<String, String> environment) async {
     String commandString() => ([commandPath] + args).join(' ');
     try {
-      var result = Process.runSync(commandPath, args, environment: environment);
+      ProcessResult result =
+          await Process.run(commandPath, args, environment: environment);
       if (result.exitCode != 0) {
         _error('Tool "$name" returned non-zero exit code '
             '(${result.exitCode}) when run as '
@@ -110,8 +118,19 @@
   ///
   /// The [args] must not be null, and it must have at least one member (the name
   /// of the tool).
-  String run(List<String> args,
-      {String content, Map<String, String> environment}) {
+  Future<String> run(List<String> args,
+      {String content, Map<String, String> environment}) async {
+    Future runner;
+    // Prevent too many tools from running simultaneously.
+    await _toolTracker.addFutureFromClosure(() {
+      runner = _run(args, content: content, environment: environment);
+      return runner;
+    });
+    return runner;
+  }
+
+  Future<String> _run(List<String> args,
+      {String content, Map<String, String> environment}) async {
     assert(args != null);
     assert(args.isNotEmpty);
     content ??= '';
@@ -133,8 +152,8 @@
     // file before running the tool synchronously.
 
     // Write the content to a temp file.
-    var tmpFile = _createTemporaryFile();
-    tmpFile.writeAsStringSync(content);
+    var tmpFile = await _createTemporaryFile();
+    await tmpFile.writeAsString(content);
 
     // Substitute the temp filename for the "$INPUT" token, and all of the other
     // environment variables. Variables are allowed to either be in $(VAR) form,
@@ -169,15 +188,26 @@
     }
 
     if (toolDefinition.setupCommand != null && !toolDefinition.setupComplete)
-      _runSetup(tool, toolDefinition, envWithInput);
+      await _runSetup(tool, toolDefinition, envWithInput);
 
     argsWithInput = toolArgs + argsWithInput;
     var commandPath;
+    void Function() callCompleter;
     if (toolDefinition is DartToolDefinition) {
-      commandPath = toolDefinition.createSnapshotIfNeeded(argsWithInput);
+      var modified = await toolDefinition
+          .modifyArgsToCreateSnapshotIfNeeded(argsWithInput);
+      commandPath = modified.item1;
+      callCompleter = modified.item2;
     } else {
       commandPath = argsWithInput.removeAt(0);
     }
-    return _runProcess(tool, content, commandPath, argsWithInput, envWithInput);
+    if (callCompleter != null) {
+      return _runProcess(
+              tool, content, commandPath, argsWithInput, envWithInput)
+          .whenComplete(callCompleter);
+    } else {
+      return _runProcess(
+          tool, content, commandPath, argsWithInput, envWithInput);
+    }
   }
 }
diff --git a/test/model_test.dart b/test/model_test.dart
index 5b6004f..3dfe7db 100644
--- a/test/model_test.dart
+++ b/test/model_test.dart
@@ -1459,8 +1459,8 @@
     });
 
     test(('Verify non-overridden members have right canonical classes'), () {
-      final Field member =
-          TypeInferenceMixedIn.allInstanceFields.firstWhere((f) => f.name == 'member');
+      final Field member = TypeInferenceMixedIn.allInstanceFields
+          .firstWhere((f) => f.name == 'member');
       final Field modifierMember = TypeInferenceMixedIn.allInstanceFields
           .firstWhere((f) => f.name == 'modifierMember');
       final Field mixinMember = TypeInferenceMixedIn.allInstanceFields
@@ -2420,8 +2420,8 @@
       documentedPartialFieldInSubclassOnly = UnusualProperties.allModelElements
           .firstWhere((e) => e.name == 'documentedPartialFieldInSubclassOnly');
 
-      isEmpty = CatString.allInstanceFields
-          .firstWhere((p) => p.name == 'isEmpty');
+      isEmpty =
+          CatString.allInstanceFields.firstWhere((p) => p.name == 'isEmpty');
       dynamicGetter = LongFirstLine.instanceProperties
           .firstWhere((p) => p.name == 'dynamicGetter');
       onlySetter = LongFirstLine.instanceProperties
diff --git a/test/tool_runner_test.dart b/test/tool_runner_test.dart
index 8044187..361d3e2 100644
--- a/test/tool_runner_test.dart
+++ b/test/tool_runner_test.dart
@@ -97,8 +97,8 @@
     });
     // This test must come first, to verify that the first run creates
     // a snapshot.
-    test('can invoke a Dart tool, and second run is a snapshot.', () {
-      var result = runner.run(
+    test('can invoke a Dart tool, and second run is a snapshot.', () async {
+      var result = await runner.run(
         ['drill', r'--file=$INPUT'],
         content: 'TEST INPUT',
       );
@@ -107,7 +107,7 @@
       expect(result, contains('## `TEST INPUT`'));
       expect(result, contains('Script location is in dartdoc tree.'));
       expect(setupFile.existsSync(), isFalse);
-      result = runner.run(
+      result = await runner.run(
         ['drill', r'--file=$INPUT'],
         content: 'TEST INPUT 2',
       );
@@ -117,8 +117,8 @@
       expect(result, contains('Script location is in snapshot cache.'));
       expect(setupFile.existsSync(), isFalse);
     });
-    test('can invoke a Dart tool', () {
-      var result = runner.run(
+    test('can invoke a Dart tool', () async {
+      var result = await runner.run(
         ['drill', r'--file=$INPUT'],
         content: 'TEST INPUT',
       );
@@ -128,16 +128,16 @@
       expect(result, contains('## `TEST INPUT`'));
       expect(setupFile.existsSync(), isFalse);
     });
-    test('can invoke a non-Dart tool', () {
-      String result = runner.run(
+    test('can invoke a non-Dart tool', () async {
+      String result = await runner.run(
         ['non_dart', '--version'],
         content: 'TEST INPUT',
       );
       expect(errors, isEmpty);
       expect(result, isEmpty); // Output is on stderr.
     });
-    test('can invoke a pre-snapshotted tool', () {
-      var result = runner.run(
+    test('can invoke a pre-snapshotted tool', () async {
+      var result = await runner.run(
         ['snapshot_drill', r'--file=$INPUT'],
         content: 'TEST INPUT',
       );
@@ -145,8 +145,8 @@
       expect(result, contains('--file=<INPUT_FILE>'));
       expect(result, contains('## `TEST INPUT`'));
     });
-    test('can invoke a tool with a setup action', () {
-      var result = runner.run(
+    test('can invoke a tool with a setup action', () async {
+      var result = await runner.run(
         ['setup_drill', r'--file=$INPUT'],
         content: 'TEST INPUT',
       );
@@ -155,8 +155,8 @@
       expect(result, contains('## `TEST INPUT`'));
       expect(setupFile.existsSync(), isTrue);
     });
-    test('fails if tool not in tool map', () {
-      String result = runner.run(
+    test('fails if tool not in tool map', () async {
+      String result = await runner.run(
         ['hammer', r'--file=$INPUT'],
         content: 'TEST INPUT',
       );
@@ -165,8 +165,8 @@
           errors[0], contains('Unable to find definition for tool "hammer"'));
       expect(result, isEmpty);
     });
-    test('fails if tool returns non-zero status', () {
-      String result = runner.run(
+    test('fails if tool returns non-zero status', () async {
+      String result = await runner.run(
         ['drill', r'--file=/a/missing/file'],
         content: 'TEST INPUT',
       );
@@ -174,8 +174,8 @@
       expect(errors[0], contains('Tool "drill" returned non-zero exit code'));
       expect(result, isEmpty);
     });
-    test("fails if tool in tool map doesn't exist", () {
-      String result = runner.run(
+    test("fails if tool in tool map doesn't exist", () async {
+      String result = await runner.run(
         ['missing'],
         content: 'TEST INPUT',
       );
diff --git a/tool/grind.dart b/tool/grind.dart
index 895119d..e33c0f5 100644
--- a/tool/grind.dart
+++ b/tool/grind.dart
@@ -731,7 +731,7 @@
 @Task(
     'Serve an arbitrary pub package based on PACKAGE_NAME and PACKAGE_VERSION environment variables')
 Future<void> servePubPackage() async {
-  _serveDocsFrom(await buildPubPackage(), 9000, 'serve-pub-package');
+  await _serveDocsFrom(await buildPubPackage(), 9000, 'serve-pub-package');
 }
 
 @Task('Checks that CHANGELOG mentions current version')
@@ -842,7 +842,7 @@
   List<String> parameters = ['--enable-asserts'];
 
   for (File dartFile in testFiles) {
-    await testFutures.addFuture(
+    await testFutures.addFutureFromClosure(() =>
         new SubprocessLauncher('dart2-${pathLib.basename(dartFile.path)}')
             .runStreamed(
                 Platform.resolvedExecutable,
@@ -852,7 +852,7 @@
   }
 
   for (File dartFile in binFiles) {
-    await testFutures.addFuture(new SubprocessLauncher(
+    await testFutures.addFutureFromClosure(() => new SubprocessLauncher(
             'dart2-bin-${pathLib.basename(dartFile.path)}-help')
         .runStreamed(
             Platform.resolvedExecutable,