Switch from IOSink to StringSink (#1105)

Towards #1100

The ExpandedReporter needs to work on the web platform. It worked before
because it only used `print` which works everywhere.

- Change the argument to `StringSink` which can be supported on every
  platform.
- Remove the `dart:io` import from the expanded reporter.
- Make the `sink` argument non-optional for the expanded reporter.
- Add a `PrintSink` implementation that buffers writes until the content
  ends with a newline and then calls `print`.
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md
index 93ea471..01601e2 100644
--- a/pkgs/test_core/CHANGELOG.md
+++ b/pkgs/test_core/CHANGELOG.md
@@ -1,6 +1,6 @@
 ## 0.2.15-dev
 
-* Add an `IOSink` argument to reporters to prepare for reporting to a file.
+* Add a `StringSink` argument to reporters to prepare for reporting to a file.
 
 ## 0.2.14
 
diff --git a/pkgs/test_core/lib/src/runner/configuration/reporters.dart b/pkgs/test_core/lib/src/runner/configuration/reporters.dart
index d9ff380..7fe1656 100644
--- a/pkgs/test_core/lib/src/runner/configuration/reporters.dart
+++ b/pkgs/test_core/lib/src/runner/configuration/reporters.dart
@@ -15,7 +15,7 @@
 
 /// Constructs a reporter for the provided engine with the provided
 /// configuration.
-typedef ReporterFactory = Reporter Function(Configuration, Engine, IOSink);
+typedef ReporterFactory = Reporter Function(Configuration, Engine, StringSink);
 
 /// Container for a reporter description and corresponding factory.
 class ReporterDetails {
@@ -31,12 +31,11 @@
 final _allReporters = <String, ReporterDetails>{
   "expanded": ReporterDetails(
       "A separate line for each update.",
-      (config, engine, sink) => ExpandedReporter.watch(engine,
+      (config, engine, sink) => ExpandedReporter.watch(engine, sink,
           color: config.color,
           printPath: config.paths.length > 1 ||
               Directory(config.paths.single).existsSync(),
-          printPlatform: config.suiteDefaults.runtimes.length > 1,
-          sink: sink)),
+          printPlatform: config.suiteDefaults.runtimes.length > 1)),
   "compact": ReporterDetails("A single line, updated continuously.",
       (_, engine, sink) => CompactReporter.watch(engine, sink)),
   "json": ReporterDetails(
diff --git a/pkgs/test_core/lib/src/runner/reporter/compact.dart b/pkgs/test_core/lib/src/runner/reporter/compact.dart
index d8166ae..7827811 100644
--- a/pkgs/test_core/lib/src/runner/reporter/compact.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/compact.dart
@@ -56,7 +56,7 @@
   /// The engine used to run the tests.
   final Engine _engine;
 
-  final IOSink _sink;
+  final StringSink _sink;
 
   /// A stopwatch that tracks the duration of the full run.
   final _stopwatch = Stopwatch();
@@ -103,7 +103,7 @@
 
   /// Watches the tests run by [engine] and prints their results to the
   /// terminal.
-  static CompactReporter watch(Engine engine, IOSink sink) =>
+  static CompactReporter watch(Engine engine, StringSink sink) =>
       CompactReporter._(engine, sink);
 
   CompactReporter._(this._engine, this._sink) {
diff --git a/pkgs/test_core/lib/src/runner/reporter/expanded.dart b/pkgs/test_core/lib/src/runner/reporter/expanded.dart
index 9e87345..87588d0 100644
--- a/pkgs/test_core/lib/src/runner/reporter/expanded.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/expanded.dart
@@ -3,7 +3,6 @@
 // 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
@@ -85,7 +84,7 @@
   /// The set of all subscriptions to various streams.
   final _subscriptions = Set<StreamSubscription>();
 
-  final IOSink _sink;
+  final StringSink _sink;
 
   // TODO(nweiz): Get configuration from [Configuration.current] once we have
   // cross-platform imports.
@@ -96,26 +95,16 @@
   /// won't. If [printPath] is `true`, this will print the path name as part of
   /// 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,
-      IOSink sink}) {
-    return ExpandedReporter._(engine,
-        color: color,
-        printPath: printPath,
-        printPlatform: printPlatform,
-        sink: sink ?? stdout);
+  static ExpandedReporter watch(Engine engine, StringSink sink,
+      {bool color = true, bool printPath = true, bool printPlatform = true}) {
+    return ExpandedReporter._(engine, sink,
+        color: color, printPath: printPath, printPlatform: printPlatform);
   }
 
-  ExpandedReporter._(this._engine,
-      {bool color = true,
-      bool printPath = true,
-      bool printPlatform = true,
-      IOSink sink})
+  ExpandedReporter._(this._engine, this._sink,
+      {bool color = true, bool printPath = true, bool printPlatform = true})
       : _printPath = printPath,
         _printPlatform = printPlatform,
-        _sink = sink,
         _color = color,
         _green = color ? '\u001b[32m' : '',
         _red = color ? '\u001b[31m' : '',
diff --git a/pkgs/test_core/lib/src/runner/reporter/json.dart b/pkgs/test_core/lib/src/runner/reporter/json.dart
index 2a2bd12..eb3c9fb 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 IOSink, pid;
+import 'dart:io' show pid;
 
 import 'package:path/path.dart' as p;
 
@@ -33,7 +33,7 @@
   /// The engine used to run the tests.
   final Engine _engine;
 
-  final IOSink _sink;
+  final StringSink _sink;
 
   /// A stopwatch that tracks the duration of the full run.
   final _stopwatch = Stopwatch();
@@ -63,7 +63,7 @@
   var _nextID = 0;
 
   /// Watches the tests run by [engine] and prints their results as JSON.
-  static JsonReporter watch(Engine engine, IOSink sink) =>
+  static JsonReporter watch(Engine engine, StringSink sink) =>
       JsonReporter._(engine, sink);
 
   JsonReporter._(this._engine, this._sink) : _config = Configuration.current {
diff --git a/pkgs/test_core/lib/src/util/print_sink.dart b/pkgs/test_core/lib/src/util/print_sink.dart
new file mode 100644
index 0000000..3409770
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/print_sink.dart
@@ -0,0 +1,39 @@
+// Copyright (c) 2019, 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.
+
+class PrintSink implements StringSink {
+  final _buffer = StringBuffer();
+
+  @override
+  void write(Object obj) {
+    _buffer.write(obj);
+    _flush();
+  }
+
+  @override
+  void writeAll(Iterable objects, [String separator = '']) {
+    _buffer.writeAll(objects, separator ?? '');
+    _flush();
+  }
+
+  @override
+  void writeCharCode(int charCode) {
+    _buffer.writeCharCode(charCode);
+    _flush();
+  }
+
+  @override
+  void writeln([Object obj = '']) {
+    _buffer.writeln(obj ?? '');
+    _flush();
+  }
+
+  /// [print] if the content available ends with a newline.
+  void _flush() {
+    if ('$_buffer'.endsWith('\n')) {
+      print(_buffer);
+      _buffer.clear();
+    }
+  }
+}
diff --git a/pkgs/test_core/lib/test_core.dart b/pkgs/test_core/lib/test_core.dart
index bcf3145..3e937c0 100644
--- a/pkgs/test_core/lib/test_core.dart
+++ b/pkgs/test_core/lib/test_core.dart
@@ -22,6 +22,7 @@
 import 'src/runner/reporter/expanded.dart';
 import 'src/runner/runner_suite.dart';
 import 'src/runner/suite.dart';
+import 'src/util/print_sink.dart';
 
 export 'package:matcher/matcher.dart';
 // Hide implementations which don't support being run directly.
@@ -59,7 +60,7 @@
     var engine = Engine();
     engine.suiteSink.add(suite);
     engine.suiteSink.close();
-    ExpandedReporter.watch(engine,
+    ExpandedReporter.watch(engine, PrintSink(),
         color: true, printPath: false, printPlatform: false);
 
     var success = await runZoned(() => Invoker.guard(engine.run),