Implement VM coverage gathering (#1088)

Open to feedback on this. Here's how it works at the moment:

There's a new `--coverage` option that you can use to specify the output directory for your coverage. It will create a new output directory if one doesn't already exist.

The tests are run through the engine, and then get spit out to a coverage helper function. That coverage helper function:
**A)** Checks if it's a VM suite
**B)** Gathers coverage if it is
**C)** Outputs coverage to `${suite path}.vm.json`

So, for example, when I run:
```bash
pub run test --coverage hello_world test/vm/simple_repo_test.dart
```

A new file gets added at `hello_world/test/vm/simple_repo_test.dart.vm.json` with the coverage results!
diff --git a/.travis.yml b/.travis.yml
index a58b8a6..854d577 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -18,14 +18,9 @@
       env: PKGS="pkgs/test pkgs/test_api pkgs/test_core"
       script: ./tool/travis.sh dartfmt dartanalyzer_0
     - stage: analyze_and_format
-      name: "SDK: 2.2.0; PKG: pkgs/test; TASKS: `dartanalyzer --fatal-warnings .`"
+      name: "SDK: 2.2.0; PKG: pkgs/test, pkgs/test_api, pkgs/test_core; TASKS: `dartanalyzer --fatal-warnings .`"
       dart: "2.2.0"
-      env: PKGS="pkgs/test"
-      script: ./tool/travis.sh dartanalyzer_1
-    - stage: analyze_and_format
-      name: "SDK: 2.1.0; PKGS: pkgs/test_api, pkgs/test_core; TASKS: `dartanalyzer --fatal-warnings .`"
-      dart: "2.1.0"
-      env: PKGS="pkgs/test_api pkgs/test_core"
+      env: PKGS="pkgs/test pkgs/test_api pkgs/test_core"
       script: ./tool/travis.sh dartanalyzer_1
     - stage: unit_test
       name: "SDK: dev; PKG: pkgs/test; TASKS: `xvfb-run -s \"-screen 0 1024x768x24\" pub run test --preset travis --total-shards 5 --shard-index 0`"
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md
index f2acb13..9278b43 100644
--- a/pkgs/test/CHANGELOG.md
+++ b/pkgs/test/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.9.0
+
+* Implement code coverage collection for VM based tests
+
 ## 1.8.0
 
 * Expose the previously hidden sharding arguments
diff --git a/pkgs/test/README.md b/pkgs/test/README.md
index 0063505..5bd7e1d 100644
--- a/pkgs/test/README.md
+++ b/pkgs/test/README.md
@@ -2,6 +2,8 @@
 
 * [Writing Tests](#writing-tests)
 * [Running Tests](#running-tests)
+  * [Sharding Tests](#sharding-tests)
+  * [Collecting Code Coverage](#collecting-code-coverage)
   * [Restricting Tests to Certain Platforms](#restricting-tests-to-certain-platforms)
   * [Platform Selectors](#platform-selectors)
   * [Running Tests on Node.js](#running-tests-on-nodejs)
@@ -157,6 +159,7 @@
 tests on both platforms with a single command: `pub run test -p "chrome,vm"
 path/to/test.dart`.
 
+### Sharding Tests
 Tests can also be sharded with the `--total-shards` and `--shard-index` arguments,
 allowing you to split up your test suites and run them separately. For example,
 if you wanted to run 3 shards of your test suite, you could run them as follows:
@@ -167,6 +170,20 @@
 pub run test --total-shards 3 --shard-index 2 path/to/test.dart
 ```
 
+### Collecting Code Coverage
+To collect code coverage, you can run tests with the `--coverage <directory>`
+argument. The directory specified can be an absolute or relative path. 
+If a directory does not exist at the path specified, a directory will be
+created. If a directory does exist, files may be overwritten with the latest
+coverage data, if they conflict.
+
+This option will enable code coverage collection on a suite-by-suite basis,
+and the resulting coverage files will be outputted in the directory specified.
+The files can then be formatted using the `package:coverage`
+`format_coverage` executable.
+
+Coverage gathering is currently only implemented for tests run in the Dart VM.
+
 ### Restricting Tests to Certain Platforms
 
 Some test files only make sense to run on particular platforms. They may use
diff --git a/pkgs/test/pubspec.yaml b/pkgs/test/pubspec.yaml
index 4637b4a..c94a1ad 100644
--- a/pkgs/test/pubspec.yaml
+++ b/pkgs/test/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test
-version: 1.8.0
+version: 1.9.0
 author: Dart Team <misc@dartlang.org>
 description: A full featured library for writing and running Dart tests.
 homepage: https://github.com/dart-lang/test/blob/master/pkgs/test
@@ -32,7 +32,7 @@
   yaml: ^2.0.0
   # Use an exact version until the test_api and test_core package are stable.
   test_api: 0.2.8
-  test_core: 0.2.10
+  test_core: 0.2.11
 
 dev_dependencies:
   fake_async: ^1.0.0
diff --git a/pkgs/test/test/runner/coverage_test.dart b/pkgs/test/test/runner/coverage_test.dart
new file mode 100644
index 0000000..7bdaf66
--- /dev/null
+++ b/pkgs/test/test/runner/coverage_test.dart
@@ -0,0 +1,50 @@
+// Copyright (c) 2016, 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.
+
+@TestOn("vm")
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+import '../io.dart';
+
+void main() {
+  group("with the --coverage flag,", () {
+    test("gathers coverage for VM tests", () async {
+      await d.file("test.dart", """
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test 1", () {
+            expect(true, isTrue);
+          });
+        }
+      """).create();
+
+      final coverageDirectory =
+          Directory(p.join(Directory.current.path, "test_coverage"));
+      expect(await coverageDirectory.exists(), isFalse,
+          reason:
+              'Coverage directory exists, cannot safely run coverage tests. Delete the ${coverageDirectory.path} directory to fix.');
+
+      var test =
+          await runTest(["--coverage", coverageDirectory.path, "test.dart"]);
+      expect(test.stdout, emitsThrough(contains("+1: All tests passed!")));
+      await test.shouldExit(0);
+
+      final coverageFile =
+          File(p.join(coverageDirectory.path, "test.dart.vm.json"));
+      final coverage = await coverageFile.readAsString();
+      final jsonCoverage = json.decode(coverage);
+      expect(jsonCoverage['coverage'], isNotEmpty);
+
+      await coverageDirectory.delete(recursive: true);
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/runner_test.dart b/pkgs/test/test/runner/runner_test.dart
index 483d942..2359b0b 100644
--- a/pkgs/test/test/runner/runner_test.dart
+++ b/pkgs/test/test/runner/runner_test.dart
@@ -95,6 +95,9 @@
                                   Currently only supported for browser tests.
 
     --debug                       Runs the VM and Chrome tests in debug mode.
+    --coverage=<directory>        Gathers coverage and outputs it to the specified directory.
+                                  Implies --debug.
+
     --[no-]chain-stack-traces     Chained stack traces to provide greater exception details
                                   especially for asynchronous code. It may be useful to disable
                                   to provide improved test performance but at the cost of
diff --git a/pkgs/test/test/utils.dart b/pkgs/test/test/utils.dart
index eadf3d6..7ebb534 100644
--- a/pkgs/test/test/utils.dart
+++ b/pkgs/test/test/utils.dart
@@ -185,7 +185,7 @@
 }
 
 /// Runs [body] with a declarer and returns an engine that runs those tests.
-Engine declareEngine(void body(), {bool runSkipped = false}) {
+Engine declareEngine(void body(), {bool runSkipped = false, String coverage}) {
   var declarer = Declarer()..declare(body);
   return Engine.withSuites([
     RunnerSuite(
@@ -193,7 +193,7 @@
         SuiteConfiguration(runSkipped: runSkipped),
         declarer.build(),
         suitePlatform)
-  ]);
+  ], coverage: coverage);
 }
 
 /// Returns a [RunnerSuite] with a default environment and configuration.
diff --git a/pkgs/test_api/pubspec.yaml b/pkgs/test_api/pubspec.yaml
index d04789e..e79b863 100644
--- a/pkgs/test_api/pubspec.yaml
+++ b/pkgs/test_api/pubspec.yaml
@@ -5,7 +5,7 @@
 homepage: https://github.com/dart-lang/test/blob/master/pkgs/test_api
 
 environment:
-  sdk: ">=2.1.0 <3.0.0"
+  sdk: ">=2.2.0 <3.0.0"
 
 dependencies:
   async: ^2.0.0
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md
index 4c81fc0..e25a3c4 100644
--- a/pkgs/test_core/CHANGELOG.md
+++ b/pkgs/test_core/CHANGELOG.md
@@ -1,6 +1,10 @@
+## 0.2.11
+
+* Implement code coverage gathering for VM tests.
+
 ## 0.2.10
 
-Add a `--debug` argument for running the VM/Chrome in debug mode.
+* Add a `--debug` argument for running the VM/Chrome in debug mode.
 
 ## 0.2.9+2
 
diff --git a/pkgs/test_core/lib/src/runner.dart b/pkgs/test_core/lib/src/runner.dart
index 9ea4952..5712035 100644
--- a/pkgs/test_core/lib/src/runner.dart
+++ b/pkgs/test_core/lib/src/runner.dart
@@ -71,7 +71,8 @@
 
   /// Creates a new runner based on [configuration].
   factory Runner(Configuration config) => config.asCurrent(() {
-        var engine = Engine(concurrency: config.concurrency);
+        var engine =
+            Engine(concurrency: config.concurrency, coverage: config.coverage);
 
         var reporterDetails = allReporters[config.reporter];
         return Runner._(engine, reporterDetails.factory(config, engine));
@@ -106,6 +107,10 @@
               '`DART_VM_OPTIONS=-DSILENT_OBSERVATORY=true`.');
         }
 
+        if (_config.coverage != null) {
+          await Directory(_config.coverage).create(recursive: true);
+        }
+
         bool success;
         if (_config.pauseAfterLoad) {
           success = await _loadThenPause(suites);
diff --git a/pkgs/test_core/lib/src/runner/configuration.dart b/pkgs/test_core/lib/src/runner/configuration.dart
index cf18a57..f1df78f 100644
--- a/pkgs/test_core/lib/src/runner/configuration.dart
+++ b/pkgs/test_core/lib/src/runner/configuration.dart
@@ -52,9 +52,13 @@
   final bool _pauseAfterLoad;
 
   /// Whether to run browsers in their respective debug modes
-  bool get debug => pauseAfterLoad || (_debug ?? false);
+  bool get debug => pauseAfterLoad || (_debug ?? false) || _coverage != null;
   final bool _debug;
 
+  /// The output folder for coverage gathering
+  String get coverage => _coverage;
+  final String _coverage;
+
   /// The path to the file from which to load more configuration information.
   ///
   /// This is *not* resolved automatically.
@@ -219,6 +223,7 @@
       String configurationPath,
       String dart2jsPath,
       String reporter,
+      String coverage,
       int pubServePort,
       int concurrency,
       int shardIndex,
@@ -264,6 +269,7 @@
         configurationPath: configurationPath,
         dart2jsPath: dart2jsPath,
         reporter: reporter,
+        coverage: coverage,
         pubServePort: pubServePort,
         concurrency: concurrency,
         shardIndex: shardIndex,
@@ -322,6 +328,7 @@
       String configurationPath,
       String dart2jsPath,
       String reporter,
+      String coverage,
       int pubServePort,
       int concurrency,
       this.shardIndex,
@@ -344,6 +351,7 @@
         _configurationPath = configurationPath,
         _dart2jsPath = dart2jsPath,
         _reporter = reporter,
+        _coverage = coverage,
         pubServeUrl = pubServePort == null
             ? null
             : Uri.parse("http://localhost:$pubServePort"),
@@ -465,6 +473,7 @@
         configurationPath: other._configurationPath ?? _configurationPath,
         dart2jsPath: other._dart2jsPath ?? _dart2jsPath,
         reporter: other._reporter ?? _reporter,
+        coverage: other._coverage ?? _coverage,
         pubServePort: (other.pubServeUrl ?? pubServeUrl)?.port,
         concurrency: other._concurrency ?? _concurrency,
         shardIndex: other.shardIndex ?? shardIndex,
diff --git a/pkgs/test_core/lib/src/runner/configuration/args.dart b/pkgs/test_core/lib/src/runner/configuration/args.dart
index 6c244b2..e0150f3 100644
--- a/pkgs/test_core/lib/src/runner/configuration/args.dart
+++ b/pkgs/test_core/lib/src/runner/configuration/args.dart
@@ -91,6 +91,10 @@
       negatable: false);
   parser.addFlag("debug",
       help: 'Runs the VM and Chrome tests in debug mode.', negatable: false);
+  parser.addOption("coverage",
+      help: 'Gathers coverage and outputs it to the specified directory.\n'
+          'Implies --debug.',
+      valueHelp: 'directory');
   parser.addFlag("chain-stack-traces",
       help: 'Chained stack traces to provide greater exception details\n'
           'especially for asynchronous code. It may be useful to disable\n'
@@ -214,6 +218,7 @@
         dart2jsArgs: _ifParsed('dart2js-args'),
         precompiledPath: _ifParsed('precompiled'),
         reporter: _ifParsed('reporter'),
+        coverage: _ifParsed('coverage'),
         pubServePort: _parseOption('pub-serve', int.parse),
         concurrency: _parseOption('concurrency', int.parse),
         shardIndex: shardIndex,
diff --git a/pkgs/test_core/lib/src/runner/engine.dart b/pkgs/test_core/lib/src/runner/engine.dart
index 647c9a5..11ae315 100644
--- a/pkgs/test_core/lib/src/runner/engine.dart
+++ b/pkgs/test_core/lib/src/runner/engine.dart
@@ -4,9 +4,13 @@
 
 import 'dart:async';
 import 'dart:collection';
+import 'dart:convert';
+import 'dart:io';
 
 import 'package:async/async.dart' hide Result;
 import 'package:collection/collection.dart';
+import 'package:coverage/coverage.dart';
+import 'package:path/path.dart' as p;
 import 'package:pedantic/pedantic.dart';
 import 'package:pool/pool.dart';
 
@@ -66,6 +70,9 @@
   /// `false` if the tests finished running before close was called.
   bool _closedBeforeDone;
 
+  /// The coverage output directory.
+  String _coverage;
+
   /// A pool that limits the number of test suites running concurrently.
   final Pool _runPool;
 
@@ -214,9 +221,10 @@
   /// [concurrency] controls how many suites are run at once, and defaults to 1.
   /// [maxSuites] controls how many suites are *loaded* at once, and defaults to
   /// four times [concurrency].
-  Engine({int concurrency, int maxSuites})
+  Engine({int concurrency, int maxSuites, String coverage})
       : _runPool = Pool(concurrency ?? 1),
-        _loadPool = Pool(maxSuites ?? (concurrency ?? 1) * 2) {
+        _loadPool = Pool(maxSuites ?? (concurrency ?? 1) * 2),
+        _coverage = coverage {
     _group.future.then((_) {
       _onTestStartedGroup.close();
       _onSuiteStartedController.close();
@@ -233,8 +241,9 @@
   ///
   /// [concurrency] controls how many suites are run at once. If [runSkipped] is
   /// `true`, skipped tests will be run as though they weren't skipped.
-  factory Engine.withSuites(List<RunnerSuite> suites, {int concurrency}) {
-    var engine = Engine(concurrency: concurrency);
+  factory Engine.withSuites(List<RunnerSuite> suites,
+      {int concurrency, String coverage}) {
+    var engine = Engine(concurrency: concurrency, coverage: coverage);
     for (var suite in suites) {
       engine.suiteSink.add(suite);
     }
@@ -279,6 +288,7 @@
           if (_closed) return;
           await _runGroup(controller, controller.liveSuite.suite.group, []);
           controller.noMoreLiveTests();
+          await _gatherCoverage(controller);
           loadResource.allowRelease(() => controller.close());
         });
       }());
@@ -293,6 +303,29 @@
     return success;
   }
 
+  Future<Null> _gatherCoverage(LiveSuiteController controller) async {
+    if (_coverage == null) return;
+
+    final RunnerSuite suite = controller.liveSuite.suite;
+
+    if (!suite.platform.runtime.isDartVM) return;
+
+    final String isolateId =
+        Uri.parse(suite.environment.observatoryUrl.fragment)
+            .queryParameters['isolateId'];
+
+    final cov = await collect(
+        suite.environment.observatoryUrl, false, false, false, Set(),
+        isolateIds: {isolateId});
+
+    final outfile = File(p.join('$_coverage', '${suite.path}.vm.json'))
+      ..createSync(recursive: true);
+    final IOSink out = outfile.openWrite();
+    out.write(json.encode(cov));
+    await out.flush();
+    await out.close();
+  }
+
   /// Runs all the entries in [group] in sequence.
   ///
   /// [suiteController] is the controller fo the suite that contains [group].
diff --git a/pkgs/test_core/pubspec.yaml b/pkgs/test_core/pubspec.yaml
index cf2e86b..9aec1fb 100644
--- a/pkgs/test_core/pubspec.yaml
+++ b/pkgs/test_core/pubspec.yaml
@@ -1,11 +1,11 @@
 name: test_core
-version: 0.2.10
+version: 0.2.11
 author: Dart Team <misc@dartlang.org>
 description: A basic library for writing tests and running them on the VM.
 homepage: https://github.com/dart-lang/test/blob/master/pkgs/test_core
 
 environment:
-  sdk: ">=2.1.0 <3.0.0"
+  sdk: ">=2.2.0 <3.0.0"
 
 dependencies:
   analyzer: ">=0.36.0 <0.39.0"
@@ -13,6 +13,7 @@
   args: ^1.4.0
   boolean_selector: ^1.0.0
   collection: ^1.8.0
+  coverage: ^0.13.3
   glob: ^1.0.0
   io: ^0.3.0
   meta: ^1.1.5