Allow injecting an IOSink into the reporters (#1101)

Towards #1100 

Prepare for a world where we a reporter can be configured to write directly to a file.
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md
index ee9ccaa..93ea471 100644
--- a/pkgs/test_core/CHANGELOG.md
+++ b/pkgs/test_core/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.2.15-dev
+
+* Add an `IOSink` argument to reporters to prepare for reporting to a file.
+
 ## 0.2.14
 
 * Support the latest `package:analyzer`.
diff --git a/pkgs/test_core/lib/src/runner.dart b/pkgs/test_core/lib/src/runner.dart
index 5712035..68a3331 100644
--- a/pkgs/test_core/lib/src/runner.dart
+++ b/pkgs/test_core/lib/src/runner.dart
@@ -75,7 +75,8 @@
             Engine(concurrency: config.concurrency, coverage: config.coverage);
 
         var reporterDetails = allReporters[config.reporter];
-        return Runner._(engine, reporterDetails.factory(config, engine));
+        return Runner._(
+            engine, reporterDetails.factory(config, engine, stdout));
       });
 
   Runner._(this._engine, this._reporter);
diff --git a/pkgs/test_core/lib/src/runner/configuration/reporters.dart b/pkgs/test_core/lib/src/runner/configuration/reporters.dart
index 1bf5663..d9ff380 100644
--- a/pkgs/test_core/lib/src/runner/configuration/reporters.dart
+++ b/pkgs/test_core/lib/src/runner/configuration/reporters.dart
@@ -15,8 +15,7 @@
 
 /// Constructs a reporter for the provided engine with the provided
 /// configuration.
-typedef ReporterFactory = Reporter Function(
-    Configuration configuration, Engine engine);
+typedef ReporterFactory = Reporter Function(Configuration, Engine, IOSink);
 
 /// Container for a reporter description and corresponding factory.
 class ReporterDetails {
@@ -32,16 +31,17 @@
 final _allReporters = <String, ReporterDetails>{
   "expanded": ReporterDetails(
       "A separate line for each update.",
-      (config, engine) => ExpandedReporter.watch(engine,
+      (config, engine, sink) => ExpandedReporter.watch(engine,
           color: config.color,
           printPath: config.paths.length > 1 ||
               Directory(config.paths.single).existsSync(),
-          printPlatform: config.suiteDefaults.runtimes.length > 1)),
+          printPlatform: config.suiteDefaults.runtimes.length > 1,
+          sink: sink)),
   "compact": ReporterDetails("A single line, updated continuously.",
-      (_, engine) => CompactReporter.watch(engine)),
+      (_, engine, sink) => CompactReporter.watch(engine, sink)),
   "json": ReporterDetails(
       "A machine-readable format (see https://goo.gl/gBsV1a).",
-      (_, engine) => JsonReporter.watch(engine)),
+      (_, engine, sink) => JsonReporter.watch(engine, sink)),
 };
 
 final defaultReporter =
diff --git a/pkgs/test_core/lib/src/runner/reporter/compact.dart b/pkgs/test_core/lib/src/runner/reporter/compact.dart
index a0a6507..d8166ae 100644
--- a/pkgs/test_core/lib/src/runner/reporter/compact.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/compact.dart
@@ -56,6 +56,8 @@
   /// The engine used to run the tests.
   final Engine _engine;
 
+  final IOSink _sink;
+
   /// A stopwatch that tracks the duration of the full run.
   final _stopwatch = Stopwatch();
 
@@ -101,9 +103,10 @@
 
   /// Watches the tests run by [engine] and prints their results to the
   /// terminal.
-  static CompactReporter watch(Engine engine) => CompactReporter._(engine);
+  static CompactReporter watch(Engine engine, IOSink sink) =>
+      CompactReporter._(engine, sink);
 
-  CompactReporter._(this._engine) {
+  CompactReporter._(this._engine, this._sink) {
     _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted));
 
     /// Convert the future to a stream so that the subscription can be paused or
@@ -115,7 +118,7 @@
     if (_paused) return;
     _paused = true;
 
-    if (!_printedNewline) print('');
+    if (!_printedNewline) _sink.writeln('');
     _printedNewline = true;
     _stopwatch.stop();
 
@@ -173,12 +176,12 @@
 
     _subscriptions.add(liveTest.onMessage.listen((message) {
       _progressLine(_description(liveTest), truncate: false);
-      if (!_printedNewline) print('');
+      if (!_printedNewline) _sink.writeln('');
       _printedNewline = true;
 
       var text = message.text;
       if (message.type == MessageType.skip) text = '  $_yellow$text$_noColor';
-      print(text);
+      _sink.writeln(text);
     }));
   }
 
@@ -205,24 +208,24 @@
 
     _progressLine(_description(liveTest),
         truncate: false, suffix: " $_bold$_red[E]$_noColor");
-    if (!_printedNewline) print('');
+    if (!_printedNewline) _sink.writeln('');
     _printedNewline = true;
 
     if (error is! LoadException) {
-      print(indent(error.toString()));
-      print(indent('$stackTrace'));
+      _sink.writeln(indent(error.toString()));
+      _sink.writeln(indent('$stackTrace'));
       return;
     }
 
     // TODO - what type is this?
-    print(indent(error.toString(color: _config.color) as String));
+    _sink.writeln(indent(error.toString(color: _config.color) as String));
 
     // Only print stack traces for load errors that come from the user's code.
     if (error.innerError is! IOException &&
         error.innerError is! IsolateSpawnException &&
         error.innerError is! FormatException &&
         error.innerError is! String) {
-      print(indent('$stackTrace'));
+      _sink.writeln(indent('$stackTrace'));
     }
   }
 
@@ -239,34 +242,34 @@
     // shouldn't print summary information, we should just make sure the
     // terminal cursor is on its own line.
     if (success == null) {
-      if (!_printedNewline) print("");
+      if (!_printedNewline) _sink.writeln("");
       _printedNewline = true;
       return;
     }
 
     if (_engine.liveTests.isEmpty) {
-      if (!_printedNewline) stdout.write("\r");
+      if (!_printedNewline) _sink.write("\r");
       var message = "No tests ran.";
-      stdout.write(message);
+      _sink.write(message);
 
       // Add extra padding to overwrite any load messages.
-      if (!_printedNewline) stdout.write(" " * (lineLength - message.length));
-      stdout.writeln();
+      if (!_printedNewline) _sink.write(" " * (lineLength - message.length));
+      _sink.writeln('');
     } else if (!success) {
       for (var liveTest in _engine.active) {
         _progressLine(_description(liveTest),
             truncate: false,
             suffix: " - did not complete $_bold$_red[E]$_noColor");
-        print('');
+        _sink.writeln('');
       }
       _progressLine('Some tests failed.', color: _red);
-      print('');
+      _sink.writeln('');
     } else if (_engine.passed.isEmpty) {
       _progressLine("All tests skipped.");
-      print('');
+      _sink.writeln('');
     } else {
       _progressLine("All tests passed!");
-      print('');
+      _sink.writeln('');
     }
   }
 
@@ -344,7 +347,7 @@
 
     // Pad the rest of the line so that it looks erased.
     buffer.write(' ' * (lineLength - withoutColors(buffer.toString()).length));
-    stdout.write(buffer.toString());
+    _sink.write(buffer.toString());
 
     _printedNewline = false;
     return true;
diff --git a/pkgs/test_core/lib/src/runner/reporter/expanded.dart b/pkgs/test_core/lib/src/runner/reporter/expanded.dart
index d2b3454..9e87345 100644
--- a/pkgs/test_core/lib/src/runner/reporter/expanded.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/expanded.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:io';
 
 import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports
@@ -84,6 +85,8 @@
   /// The set of all subscriptions to various streams.
   final _subscriptions = Set<StreamSubscription>();
 
+  final IOSink _sink;
+
   // TODO(nweiz): Get configuration from [Configuration.current] once we have
   // cross-platform imports.
   /// Watches the tests run by [engine] and prints their results to the
@@ -94,15 +97,25 @@
   /// the test description. Likewise, if [printPlatform] is `true`, this will
   /// print the platform as part of the test description.
   static ExpandedReporter watch(Engine engine,
-      {bool color = true, bool printPath = true, bool printPlatform = true}) {
+      {bool color = true,
+      bool printPath = true,
+      bool printPlatform = true,
+      IOSink sink}) {
     return ExpandedReporter._(engine,
-        color: color, printPath: printPath, printPlatform: printPlatform);
+        color: color,
+        printPath: printPath,
+        printPlatform: printPlatform,
+        sink: sink ?? stdout);
   }
 
   ExpandedReporter._(this._engine,
-      {bool color = true, bool printPath = true, bool printPlatform = true})
+      {bool color = true,
+      bool printPath = true,
+      bool printPlatform = true,
+      IOSink sink})
       : _printPath = printPath,
         _printPlatform = printPlatform,
+        _sink = sink,
         _color = color,
         _green = color ? '\u001b[32m' : '',
         _red = color ? '\u001b[31m' : '',
@@ -173,7 +186,7 @@
       _progressLine(_description(liveTest));
       var text = message.text;
       if (message.type == MessageType.skip) text = '  $_yellow$text$_noColor';
-      print(text);
+      _sink.writeln(text);
     }));
   }
 
@@ -195,17 +208,16 @@
     _progressLine(_description(liveTest), suffix: " $_bold$_red[E]$_noColor");
 
     if (error is! LoadException) {
-      print(indent(error.toString()));
-      print(indent('$stackTrace'));
+      _sink..writeln(indent('$error'))..writeln(indent('$stackTrace'));
       return;
     }
 
     // TODO - what type is this?
-    print(indent((error as dynamic).toString(color: _color) as String));
+    _sink.writeln(indent((error as dynamic).toString(color: _color) as String));
 
     // Only print stack traces for load errors that come from the user's code.
     if (error.innerError is! FormatException && error.innerError is! String) {
-      print(indent('$stackTrace'));
+      _sink.writeln(indent('$stackTrace'));
     }
   }
 
@@ -220,7 +232,7 @@
     if (success == null) return;
 
     if (_engine.liveTests.isEmpty) {
-      print("No tests ran.");
+      _sink.writeln("No tests ran.");
     } else if (!success) {
       for (var liveTest in _engine.active) {
         _progressLine(_description(liveTest),
@@ -287,7 +299,7 @@
     buffer.write(message);
     buffer.write(_noColor);
 
-    print(buffer.toString());
+    _sink.writeln(buffer.toString());
   }
 
   /// Returns a representation of [duration] as `MM:SS`.
diff --git a/pkgs/test_core/lib/src/runner/reporter/json.dart b/pkgs/test_core/lib/src/runner/reporter/json.dart
index 1509099..2a2bd12 100644
--- a/pkgs/test_core/lib/src/runner/reporter/json.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/json.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'dart:io' show pid;
+import 'dart:io' show IOSink, pid;
 
 import 'package:path/path.dart' as p;
 
@@ -33,6 +33,8 @@
   /// The engine used to run the tests.
   final Engine _engine;
 
+  final IOSink _sink;
+
   /// A stopwatch that tracks the duration of the full run.
   final _stopwatch = Stopwatch();
 
@@ -61,9 +63,10 @@
   var _nextID = 0;
 
   /// Watches the tests run by [engine] and prints their results as JSON.
-  static JsonReporter watch(Engine engine) => JsonReporter._(engine);
+  static JsonReporter watch(Engine engine, IOSink sink) =>
+      JsonReporter._(engine, sink);
 
-  JsonReporter._(this._engine) : _config = Configuration.current {
+  JsonReporter._(this._engine, this._sink) : _config = Configuration.current {
     _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted));
 
     /// Convert the future to a stream so that the subscription can be paused or
@@ -283,7 +286,7 @@
   void _emit(String type, Map attributes) {
     attributes["type"] = type;
     attributes["time"] = _stopwatch.elapsed.inMilliseconds;
-    print(jsonEncode(attributes));
+    _sink.writeln(jsonEncode(attributes));
   }
 
   /// Modifies [map] to include line, column, and URL information from the first
diff --git a/pkgs/test_core/pubspec.yaml b/pkgs/test_core/pubspec.yaml
index 5eb1cc4..54b904c 100644
--- a/pkgs/test_core/pubspec.yaml
+++ b/pkgs/test_core/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test_core
-version: 0.2.14
+version: 0.2.15-dev
 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