Cleanup and move test-only code into the test utils library (#1878)

diff --git a/lib/src/io_utils.dart b/lib/src/io_utils.dart
index 92ce509..3c99b3a 100644
--- a/lib/src/io_utils.dart
+++ b/lib/src/io_utils.dart
@@ -6,10 +6,8 @@
 library dartdoc.io_utils;
 
 import 'dart:async';
-import 'dart:convert';
 import 'dart:io';
 
-import 'package:dartdoc/src/tuple.dart';
 import 'package:path/path.dart' as pathLib;
 
 /// Return a resolved path including the home directory in place of tilde
@@ -82,7 +80,6 @@
 final partOfRegexp = new RegExp('part of ');
 final newLinePartOfRegexp = new RegExp('\npart of ');
 
-final RegExp quotables = new RegExp(r'[ "\r\n\$]');
 
 /// Best used with Future<void>.
 class MultiFutureTracker<T> {
@@ -117,204 +114,4 @@
   Future<void> wait() async => await _waitUntil(0);
 }
 
-/// 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);
 
-  static int nextObservatoryPort = 9292;
-
-  /// Set this to true to enable coverage runs.
-  static bool coverageEnabled = false;
-
-  /// A list of all coverage results picked up by all launchers.
-  static List<Tuple2<String, Future<Iterable<Map>>>> coverageResults = [];
-
-  static Directory _tempDir;
-  static Directory get tempDir =>
-      _tempDir ??= Directory.systemTemp.createTempSync('dartdoc_coverage_data');
-
-  int _observatoryPort;
-  // TODO(jcollins-g): use ephemeral ports
-  int get observatoryPort => _observatoryPort ??= nextObservatoryPort++;
-
-  String _outCoverageFilename;
-  String get outCoverageFilename => _outCoverageFilename ??=
-      pathLib.join(tempDir.path, 'dart-cov-0-${observatoryPort}.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.map((t) => t.item2));
-
-    return launcher.runStreamed(Platform.executable, [
-      'tool/format_coverage.dart', // TODO(jcollins-g): use pub after dart-lang/coverage#240 is landed
-      '--lcov',
-      '-v',
-      '-b', '.',
-      '--packages=.packages',
-      '--sdk-root=${pathLib.canonicalize(pathLib.join(pathLib.dirname(Platform.executable), '..'))}',
-      '--out=${pathLib.canonicalize(outputFile.path)}',
-      '--report-on=bin,lib',
-      '-i', tempDir.path,
-    ]);
-  }
-
-  @override
-  Future<Iterable<Map>> runStreamed(String executable, List<String> arguments,
-      {String workingDirectory}) {
-    assert(executable == Platform.executable,
-        'Must use dart executable for tracking coverage');
-
-    if (coverageEnabled) {
-      arguments = [
-        '--enable-vm-service=${observatoryPort}',
-        '--pause-isolates-on-exit'
-      ]..addAll(arguments);
-    }
-
-    Future<Iterable<Map>> results = super
-        .runStreamed(executable, arguments, workingDirectory: workingDirectory);
-
-    if (coverageEnabled) {
-      coverageResults.add(new Tuple2(
-          outCoverageFilename,
-          super.runStreamed('pub', [
-            'run',
-            'coverage:collect_coverage',
-            '--wait-paused',
-            '--resume-isolates',
-            '--port=${observatoryPort}',
-            '--out=${outCoverageFilename}',
-          ])));
-    }
-    return results;
-  }
-}
-
-class SubprocessLauncher {
-  final String context;
-  final Map<String, String> environment;
-
-  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);
-    if (filter == 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])
-      : this.environment = 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): move this to grinder?
-  Future<Iterable<Map>> runStreamed(String executable, List<String> arguments,
-      {String workingDirectory}) async {
-    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) {
-      Map result;
-      try {
-        result = json.decoder.convert(line);
-      } catch (FormatException) {}
-      if (result != null) {
-        if (jsonObjects == null) {
-          jsonObjects = new List();
-        }
-        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 (String 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;
-
-    String realExecutable = executable;
-    final List<String> 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);
-
-    Process process = await Process.start(realExecutable, realArguments,
-        workingDirectory: workingDirectory, environment: environment);
-    Future<void> stdoutFuture = _printStream(process.stdout, stdout,
-        prefix: prefix, filter: jsonCallback);
-    Future<void> stderrFuture = _printStream(process.stderr, stderr,
-        prefix: prefix, filter: jsonCallback);
-    await Future.wait([stderrFuture, stdoutFuture, process.exitCode]);
-
-    int exitCode = await process.exitCode;
-    if (exitCode != 0) {
-      throw new ProcessException(executable, arguments,
-          "SubprocessLauncher got non-zero exitCode: $exitCode", exitCode);
-    }
-    return jsonObjects;
-  }
-}
diff --git a/lib/src/model.dart b/lib/src/model.dart
index 4659baf..dbbce53 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -388,7 +388,7 @@
               } else {
                 _overriddenElement = foundField.setter;
               }
-              assert(!(_overriddenElement as Accessor).isInherited);
+              assert(!(_overriddenElement as InheritableAccessor).isInherited);
               break;
             }
           }
@@ -413,16 +413,7 @@
   }
 
   GetterSetterCombo get enclosingCombo {
-    if (_enclosingCombo == null) {
-      if (enclosingElement is Class) {
-        // TODO(jcollins-g): this side effect is rather hacky.  Make sure
-        // enclosingCombo always gets set at accessor creation time, somehow, to
-        // avoid this.
-        // TODO(jcollins-g): This also doesn't work for private accessors sometimes.
-        (enclosingElement as Class).allInstanceFields;
-      }
-      assert(_enclosingCombo != null);
-    }
+    assert(_enclosingCombo != null);
     return _enclosingCombo;
   }
 
@@ -473,19 +464,12 @@
   @override
   void warn(PackageWarning kind,
       {String message,
-      Iterable<Locatable> referredFrom,
-      Iterable<String> extendedDebug}) {
-    if (enclosingCombo != null) {
-      enclosingCombo.warn(kind,
-          message: message,
-          referredFrom: referredFrom,
-          extendedDebug: extendedDebug);
-    } else {
-      super.warn(kind,
-          message: message,
-          referredFrom: referredFrom,
-          extendedDebug: extendedDebug);
-    }
+        Iterable<Locatable> referredFrom,
+        Iterable<String> extendedDebug}) {
+    enclosingCombo.warn(kind,
+        message: message,
+        referredFrom: referredFrom,
+        extendedDebug: extendedDebug);
   }
 
   @override
@@ -510,8 +494,6 @@
 
   bool get isCovariant => isSetter && parameters.first.isCovariant;
 
-  bool get isInherited => false;
-
   @override
   String get href {
     return enclosingCombo.href;
@@ -596,7 +578,7 @@
     with TypeParameters, Categorization
     implements EnclosedElement {
   List<DefinedElementType> _mixins;
-  DefinedElementType _supertype;
+  DefinedElementType supertype;
   List<DefinedElementType> _interfaces;
   List<Constructor> _constructors;
   List<Method> _allMethods;
@@ -623,7 +605,7 @@
         .toList(growable: false);
 
     if (_cls.supertype != null && _cls.supertype.element.supertype != null) {
-      _supertype = new ElementType.from(_cls.supertype, packageGraph);
+      supertype = new ElementType.from(_cls.supertype, packageGraph);
     }
 
     _interfaces = _cls.interfaces
@@ -1032,7 +1014,7 @@
 
   List<DefinedElementType> get superChain {
     List<DefinedElementType> typeChain = [];
-    DefinedElementType parent = _supertype;
+    DefinedElementType parent = supertype;
     while (parent != null) {
       typeChain.add(parent);
       if (parent.type is InterfaceType) {
@@ -1045,7 +1027,7 @@
               (parent.type as InterfaceType).superclass, packageGraph);
         }
       } else {
-        parent = (parent.element as Class)._supertype;
+        parent = (parent.element as Class).supertype;
       }
     }
     return typeChain;
@@ -1056,8 +1038,6 @@
   Iterable<DefinedElementType> get publicSuperChainReversed =>
       publicSuperChain.toList().reversed;
 
-  DefinedElementType get supertype => _supertype;
-
   List<ExecutableElement> __inheritedElements;
   List<ExecutableElement> get _inheritedElements {
     if (__inheritedElements == null) {
@@ -1133,9 +1113,9 @@
       PropertyAccessorElement setterElement,
       Set<PropertyAccessorElement> inheritedAccessors,
       [FieldElement f]) {
-    Accessor getter =
+    InheritableAccessor getter =
         new InheritableAccessor.from(getterElement, inheritedAccessors, this);
-    Accessor setter =
+    InheritableAccessor setter =
         new InheritableAccessor.from(setterElement, inheritedAccessors, this);
     // Rebind getterElement/setterElement as ModelElement.from can resolve
     // MultiplyInheritedExecutableElements or resolve Members.
@@ -1154,7 +1134,7 @@
         // different places in the inheritance chain, there are two FieldElements
         // for this single Field we're trying to compose.  Pick the one closest
         // to this class on the inheritance chain.
-        if ((setter.enclosingElement as Class)
+        if ((setter.enclosingElement)
             .isInheritingFrom(getter.enclosingElement)) {
           f = setterElement.variable;
         } else {
@@ -2033,10 +2013,7 @@
   }
 
   bool get hasExplicitGetter => hasPublicGetter && !getter.isSynthetic;
-
   bool get hasExplicitSetter => hasPublicSetter && !setter.isSynthetic;
-  bool get hasImplicitSetter => hasPublicSetter && setter.isSynthetic;
-  bool get hasImplicitGetter => hasPublicGetter && getter.isSynthetic;
 
   bool get hasGetter => getter != null;
 
@@ -2054,10 +2031,7 @@
     if (writeOnly) return r'&#8592;';
     // ↔
     if (readWrite) return r'&#8596;';
-    // A GetterSetterCombo should always be one of readOnly, writeOnly,
-    // or readWrite (if documented).
-    assert(!isPublic);
-    return null;
+    throw UnsupportedError('GetterSetterCombo must be one of readOnly, writeOnly, or readWrite');
   }
 
   bool get readOnly => hasPublicGetter && !hasPublicSetter;
@@ -2201,12 +2175,6 @@
     return true;
   }
 
-  /// A special case where the SDK has defined that we should not document
-  /// this library.  This is implemented by tweaking canonicalization so
-  /// even though the library is public and part of the Package's list,
-  /// we don't count it as a candidate for canonicalization.
-  bool get isSdkUndocumented => (sdkLib != null && !sdkLib.isDocumented);
-
   @override
   Iterable<TopLevelVariable> get constants {
     if (_constants == null) {
@@ -2462,8 +2430,6 @@
     return _packageMeta;
   }
 
-  String get path => _libraryElement.definingCompilationUnit.name;
-
   /// All variables ("properties") except constants.
   @override
   Iterable<TopLevelVariable> get properties {
@@ -2530,18 +2496,6 @@
     return _allClasses.firstWhere((it) => it.name == name, orElse: () => null);
   }
 
-  bool hasInExportedNamespace(Element element) {
-    Element found = _exportedNamespace.get(element.name);
-    if (found == null) return false;
-    if (found == element) return true; // this checks more than just the name
-
-    // Fix for #587, comparison between elements isn't reliable on windows.
-    // for some reason. sigh.
-
-    return found.runtimeType == element.runtimeType &&
-        found.nameOffset == element.nameOffset;
-  }
-
   List<TopLevelVariable> _getVariables() {
     if (_variables != null) return _variables;
 
@@ -2591,21 +2545,8 @@
     } else {
       hidePackage = package.packageGraph.packageMeta;
     }
-    if (name.startsWith('file:')) {
-      // restoreUri doesn't do anything for the package we're documenting.
-      String canonicalPackagePath =
-          '${pathLib.canonicalize(hidePackage.dir.path)}${pathLib.separator}lib${pathLib.separator}';
-      String canonicalElementPath =
-          pathLib.canonicalize(element.source.uri.toFilePath());
-      assert(canonicalElementPath.startsWith(canonicalPackagePath));
-      List<String> pathSegments = [hidePackage.name]..addAll(pathLib
-          .split(canonicalElementPath.replaceFirst(canonicalPackagePath, '')));
-      Uri libraryUri = new Uri(
-        scheme: 'package',
-        pathSegments: pathSegments,
-      );
-      name = libraryUri.toString();
-    }
+    // restoreUri must not result in another file URI.
+    assert(!name.startsWith('file:'));
 
     String defaultPackagePrefix = 'package:$hidePackage/';
     if (name.startsWith(defaultPackagePrefix)) {
@@ -3391,8 +3332,7 @@
       if (!hasPublicName(element)) {
         _canonicalLibrary = null;
       } else if (!packageGraph.localPublicLibraries.contains(definingLibrary)) {
-        List<Library> candidateLibraries = packageGraph
-            .libraryElementReexportedBy[definingLibrary.element]
+        List<Library> candidateLibraries = definingLibrary.exportedInLibraries
             ?.where((l) =>
                 l.isPublic &&
                 l.package.documentedWhere != DocumentLocation.missing)
@@ -4609,6 +4549,9 @@
     }
     return _namePart;
   }
+
+  @override
+  String toString() => name;
 }
 
 /// Something able to be indexed.
@@ -4881,17 +4824,6 @@
 
   PackageWarningCounter get packageWarningCounter => _packageWarningCounter;
 
-  /// Returns colon-stripped name and location of the given locatable.
-  static Tuple2<String, String> nameAndLocation(Locatable locatable) {
-    String locatableName = '<unknown>';
-    String locatableLocation = '';
-    if (locatable != null) {
-      locatableName = locatable.fullyQualifiedName.replaceFirst(':', '-');
-      locatableLocation = locatable.location;
-    }
-    return new Tuple2(locatableName, locatableLocation);
-  }
-
   final Set<Tuple3<Element, PackageWarning, String>> _warnAlreadySeen =
       new Set();
   void warnOnElement(Warnable warnable, PackageWarning kind,
@@ -5074,8 +5006,6 @@
     return locatable.fullyQualifiedName.replaceFirst(':', '-');
   }
 
-  bool get hasMultiplePackages => localPackages.length > 1;
-
   List<Package> get packages => packageMap.values.toList();
 
   List<Package> _publicPackages;
@@ -5197,8 +5127,8 @@
         _checkAndAddClass(t.element, c);
       });
     }
-    if (c._supertype != null) {
-      _checkAndAddClass(c._supertype.element, c);
+    if (c.supertype != null) {
+      _checkAndAddClass(c.supertype.element, c);
     }
     if (!c.interfaces.isEmpty) {
       c.interfaces.forEach((t) {
@@ -5571,9 +5501,6 @@
   Iterable<TopLevelVariable> get publicProperties =>
       filterNonPublic(properties);
   Iterable<Typedef> get publicTypedefs => filterNonPublic(typedefs);
-
-  @override
-  String toString() => name;
 }
 
 /// A set of libraries, initialized after construction by accessing [_libraries].
@@ -5598,9 +5525,6 @@
   /// Sorting key.  [containerOrder] should contain these.
   String get sortKey => name;
 
-  @override
-  String toString() => name;
-
   /// Does this container represent the SDK?  This can be false for containers
   /// that only represent a part of the SDK.
   bool get isSdk => false;
@@ -6133,9 +6057,6 @@
   PackageMeta get packageMeta => _packageMeta;
 
   @override
-  String toString() => name;
-
-  @override
   Element get element => null;
 
   @override
@@ -6195,9 +6116,6 @@
   String get kind => 'parameter';
 
   ParameterElement get _parameter => element as ParameterElement;
-
-  @override
-  String toString() => element.name;
 }
 
 abstract class SourceCodeMixin implements Documentable {
@@ -6427,9 +6345,6 @@
   }
 
   TypeParameterElement get _typeParameter => element as TypeParameterElement;
-
-  @override
-  String toString() => element.name;
 }
 
 /// Everything you need to instantiate a PackageGraph object for documenting.
@@ -6556,22 +6471,11 @@
           options);
       driver.results.listen((_) {});
       driver.exceptions.listen((_) {});
-      _session = driver.currentSession;
       scheduler.start();
     }
     return _driver;
   }
 
-  AnalysisSession _session;
-  AnalysisSession get session {
-    if (_session == null) {
-      // The current session is initialized as a side-effect of initializing the
-      // driver.
-      driver;
-    }
-    return _session;
-  }
-
   PackageWarningOptions getWarningOptions() {
     PackageWarningOptions warningOptions =
         new PackageWarningOptions(config.verboseWarnings);
@@ -6594,12 +6498,10 @@
   /// Return an Iterable with the sdk files we should parse.
   /// Filter can be String or RegExp (technically, anything valid for
   /// [String.contains])
-  Iterable<String> getSdkFilesToDocument([dynamic filter]) sync* {
+  Iterable<String> getSdkFilesToDocument() sync* {
     for (var sdkLib in sdk.sdkLibraries) {
       Source source = sdk.mapDartUri(sdkLib.shortName);
-      if (filter == null || source.uri.toString().contains(filter)) {
-        yield source.fullName;
-      }
+      yield source.fullName;
     }
   }
 
@@ -6631,7 +6533,7 @@
     if (sourceKind == SourceKind.LIBRARY) {
       // Loading libraryElements from part files works, but is painfully slow
       // and creates many duplicates.
-      return await session.getResolvedLibrary(source.fullName);
+      return await driver.currentSession.getResolvedLibrary(source.fullName);
     }
     return null;
   }
diff --git a/test/src/utils.dart b/test/src/utils.dart
index 2002660..17ae32e 100644
--- a/test/src/utils.dart
+++ b/test/src/utils.dart
@@ -5,12 +5,17 @@
 library test_utils;
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:dartdoc/dartdoc.dart';
 import 'package:dartdoc/src/html/html_generator.dart';
 import 'package:dartdoc/src/model.dart';
 import 'package:dartdoc/src/package_meta.dart';
+import 'package:dartdoc/src/tuple.dart';
+import 'package:path/path.dart' as pathLib;
+
+final RegExp quotables = new RegExp(r'[ "\r\n\$]');
 
 Directory sdkDir;
 PackageMeta sdkPackageMeta;
@@ -59,10 +64,6 @@
   return new DartdocOptionContext(optionSet, Directory.current);
 }
 
-void delete(Directory dir) {
-  if (dir.existsSync()) dir.deleteSync(recursive: true);
-}
-
 void init({List<String> additionalArguments}) async {
   sdkDir = defaultSdkDir;
   sdkPackageMeta = new PackageMeta.fromDir(sdkDir);
@@ -108,3 +109,205 @@
           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);
+
+  static int nextObservatoryPort = 9292;
+
+  /// Set this to true to enable coverage runs.
+  static bool coverageEnabled = false;
+
+  /// A list of all coverage results picked up by all launchers.
+  static List<Tuple2<String, Future<Iterable<Map>>>> coverageResults = [];
+
+  static Directory _tempDir;
+  static Directory get tempDir =>
+      _tempDir ??= Directory.systemTemp.createTempSync('dartdoc_coverage_data');
+
+  int _observatoryPort;
+  // TODO(jcollins-g): use ephemeral ports
+  int get observatoryPort => _observatoryPort ??= nextObservatoryPort++;
+
+  String _outCoverageFilename;
+  String get outCoverageFilename => _outCoverageFilename ??=
+      pathLib.join(tempDir.path, 'dart-cov-0-${observatoryPort}.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.map((t) => t.item2));
+
+    return launcher.runStreamed(Platform.executable, [
+      'tool/format_coverage.dart', // TODO(jcollins-g): use pub after dart-lang/coverage#240 is landed
+      '--lcov',
+      '-v',
+      '-b', '.',
+      '--packages=.packages',
+      '--sdk-root=${pathLib.canonicalize(pathLib.join(pathLib.dirname(Platform.executable), '..'))}',
+      '--out=${pathLib.canonicalize(outputFile.path)}',
+      '--report-on=bin,lib',
+      '-i', tempDir.path,
+    ]);
+  }
+
+  @override
+  Future<Iterable<Map>> runStreamed(String executable, List<String> arguments,
+      {String workingDirectory}) {
+    assert(executable == Platform.executable,
+    'Must use dart executable for tracking coverage');
+
+    if (coverageEnabled) {
+      arguments = [
+        '--enable-vm-service=${observatoryPort}',
+        '--pause-isolates-on-exit'
+      ]..addAll(arguments);
+    }
+
+    Future<Iterable<Map>> results = super
+        .runStreamed(executable, arguments, workingDirectory: workingDirectory);
+
+    if (coverageEnabled) {
+      coverageResults.add(new Tuple2(
+          outCoverageFilename,
+          super.runStreamed('pub', [
+            'run',
+            'coverage:collect_coverage',
+            '--wait-paused',
+            '--resume-isolates',
+            '--port=${observatoryPort}',
+            '--out=${outCoverageFilename}',
+          ])));
+    }
+    return results;
+  }
+}
+
+class SubprocessLauncher {
+  final String context;
+  final Map<String, String> environment;
+
+  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);
+    if (filter == 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])
+      : this.environment = 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): move this to grinder?
+  Future<Iterable<Map>> runStreamed(String executable, List<String> arguments,
+      {String workingDirectory}) async {
+    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) {
+      Map result;
+      try {
+        result = json.decoder.convert(line);
+      } catch (FormatException) {}
+      if (result != null) {
+        if (jsonObjects == null) {
+          jsonObjects = new List();
+        }
+        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 (String 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;
+
+    String realExecutable = executable;
+    final List<String> 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);
+
+    Process process = await Process.start(realExecutable, realArguments,
+        workingDirectory: workingDirectory, environment: environment);
+    Future<void> stdoutFuture = _printStream(process.stdout, stdout,
+        prefix: prefix, filter: jsonCallback);
+    Future<void> stderrFuture = _printStream(process.stderr, stderr,
+        prefix: prefix, filter: jsonCallback);
+    await Future.wait([stderrFuture, stdoutFuture, process.exitCode]);
+
+    int exitCode = await process.exitCode;
+    if (exitCode != 0) {
+      throw new ProcessException(executable, arguments,
+          "SubprocessLauncher got non-zero exitCode: $exitCode", exitCode);
+    }
+    return jsonObjects;
+  }
+}
\ No newline at end of file
diff --git a/tool/grind.dart b/tool/grind.dart
index 9c8d7ee..d07ebd6 100644
--- a/tool/grind.dart
+++ b/tool/grind.dart
@@ -11,6 +11,8 @@
 import 'package:path/path.dart' as pathLib;
 import 'package:yaml/yaml.dart' as yaml;
 
+import '../test/src/utils.dart';
+
 void main([List<String> args]) => grind(args);
 
 /// Thrown on failure to find something in a file.