Default to eagerly closing hybrid isolates. (#513)

Users can pass stayAlive: true to keep them open, but this helps ensure
that they won't accidentally have a bunch of isolates hanging around
between tests.

Closes #109
diff --git a/lib/src/frontend/spawn_hybrid.dart b/lib/src/frontend/spawn_hybrid.dart
index 8e25b4d..8c9b85d 100644
--- a/lib/src/frontend/spawn_hybrid.dart
+++ b/lib/src/frontend/spawn_hybrid.dart
@@ -9,6 +9,7 @@
 import 'package:path/path.dart' as p;
 import 'package:stream_channel/stream_channel.dart';
 
+import '../../test.dart';
 import '../backend/invoker.dart';
 import '../util/remote_exception.dart';
 import '../utils.dart';
@@ -79,9 +80,13 @@
 /// context, so it can't access test functions like `expect()` and
 /// `expectAsync()`.
 ///
+/// By default, the hybrid isolate is automatically killed when the test
+/// finishes running. If [stayAlive] is `true`, it won't be killed until the
+/// entire test suite finishes running.
+///
 /// **Note**: If you use this API, be sure to add a dependency on the
 /// **`stream_channel` package, since you're using its API as well!
-StreamChannel spawnHybridUri(uri, {Object message}) {
+StreamChannel spawnHybridUri(uri, {Object message, bool stayAlive: false}) {
   Uri parsedUrl;
   if (uri is Uri) {
     parsedUrl = uri;
@@ -101,7 +106,7 @@
     absoluteUri = uri.toString();
   }
 
-  return _spawn(absoluteUri, message);
+  return _spawn(absoluteUri, message, stayAlive: stayAlive);
 }
 
 /// Spawns a VM isolate that runs the given [dartCode], which is loaded as the
@@ -138,17 +143,22 @@
 /// context, so it can't access test functions like `expect()` and
 /// `expectAsync()`.
 ///
+/// By default, the hybrid isolate is automatically killed when the test
+/// finishes running. If [stayAlive] is `true`, it won't be killed until the
+/// entire test suite finishes running.
+///
 /// **Note**: If you use this API, be sure to add a dependency on the
 /// **`stream_channel` package, since you're using its API as well!
-StreamChannel spawnHybridCode(String dartCode, {Object message}) {
+StreamChannel spawnHybridCode(String dartCode, {Object message,
+    bool stayAlive: false}) {
   var uri = new Uri.dataFromString(dartCode,
       encoding: UTF8, mimeType: 'application/dart');
-  return _spawn(uri.toString(), message);
+  return _spawn(uri.toString(), message, stayAlive: stayAlive);
 }
 
 /// Like [spawnHybridUri], but doesn't take [Uri] objects and doesn't handle
 /// relative URLs.
-StreamChannel _spawn(String uri, Object message) {
+StreamChannel _spawn(String uri, Object message, {bool stayAlive: false}) {
   var channel = Zone.current[#test.runner.test_channel] as MultiChannel;
   if (channel == null) {
     // TODO(nweiz): Link to an issue tracking support when running the test file
@@ -168,5 +178,11 @@
     "channel": isolateChannel.id
   });
 
+  if (!stayAlive) {
+    var disconnector = new Disconnector();
+    addTearDown(() => disconnector.disconnect());
+    isolateChannel = isolateChannel.transform(disconnector);
+  }
+
   return isolateChannel.transform(_transformer);
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index 0f8b41e..927a8d3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test
-version: 0.12.18-dev
+version: 0.12.18
 author: Dart Team <misc@dartlang.org>
 description: A library for writing dart unit tests.
 homepage: https://github.com/dart-lang/test
diff --git a/test/runner/hybrid_test.dart b/test/runner/hybrid_test.dart
index e455116..a118564 100644
--- a/test/runner/hybrid_test.dart
+++ b/test/runner/hybrid_test.dart
@@ -11,10 +11,15 @@
 import 'package:scheduled_test/descriptor.dart' as d;
 import 'package:scheduled_test/scheduled_test.dart';
 
-
 import '../io.dart';
 
 void main() {
+  String packageRoot;
+  setUpAll(() async {
+    packageRoot = p.absolute(p.dirname(p.fromUri(
+        await Isolate.resolvePackageUri(Uri.parse("package:test/")))));
+  });
+
   useSandbox();
 
   group("spawnHybridUri():", () {
@@ -250,53 +255,6 @@
           completion(equals({"a": "b"})));
     });
 
-    test("persists across multiple tests", () {
-      d.file("test.dart", """
-        import "dart:async";
-
-        import "package:async/async.dart";
-        import "package:stream_channel/stream_channel.dart";
-
-        import "package:test/test.dart";
-
-        void main() {
-          StreamQueue queue;
-          StreamSink sink;
-          setUpAll(() {
-            var channel = spawnHybridCode('''
-              import "package:stream_channel/stream_channel.dart";
-
-              void hybridMain(StreamChannel channel) {
-                channel.stream.listen((message) {
-                  channel.sink.add(message);
-                });
-              }
-            ''');
-            queue = new StreamQueue(channel.stream);
-            sink = channel.sink;
-          });
-
-          test("echoes a number", () {
-            expect(queue.next, completion(equals(123)));
-            sink.add(123);
-          });
-
-          test("echoes a string", () {
-            expect(queue.next, completion(equals("wow")));
-            sink.add("wow");
-          });
-        }
-      """).create();
-
-      var test = runTest(["-p", "content-shell", "test.dart"]);
-      test.stdout.expect(containsInOrder([
-        "+0: echoes a number",
-        "+1: echoes a string",
-        "+2: All tests passed!"
-      ]));
-      test.shouldExit(0);
-    }, tags: ['content-shell']);
-
     test("allows the hybrid isolate to send errors across the stream channel",
         () {
       var channel = spawnHybridCode("""
@@ -500,5 +458,87 @@
 
       expect(channel.stream.toList(), completion(isEmpty));
     });
+
+    test("closes the channel when the test finishes by default", () {
+      d.file("test.dart", """
+        import "package:stream_channel/stream_channel.dart";
+        import "package:test/test.dart";
+
+        import "${p.toUri(packageRoot)}/test/utils.dart";
+
+        void main() {
+          StreamChannel channel;
+          test("test 1", () {
+            channel = spawnHybridCode('''
+              import "package:stream_channel/stream_channel.dart";
+
+              void hybridMain(StreamChannel channel) {}
+            ''');
+          });
+
+          test("test 2", () async {
+            var isDone = false;
+            channel.stream.listen(null, onDone: () => isDone = true);
+            await pumpEventQueue();
+            expect(isDone, isTrue);
+          });
+        }
+      """).create();
+
+      var test = runTest(["test.dart"]);
+      test.stdout.expect(containsInOrder([
+        "+0: test 1",
+        "+1: test 2",
+        "+2: All tests passed!"
+      ]));
+      test.shouldExit(0);
+    });
+
+    test("persists across multiple tests with stayAlive: true", () {
+      d.file("test.dart", """
+        import "dart:async";
+
+        import "package:async/async.dart";
+        import "package:stream_channel/stream_channel.dart";
+
+        import "package:test/test.dart";
+
+        void main() {
+          StreamQueue queue;
+          StreamSink sink;
+          setUpAll(() {
+            var channel = spawnHybridCode('''
+              import "package:stream_channel/stream_channel.dart";
+
+              void hybridMain(StreamChannel channel) {
+                channel.stream.listen((message) {
+                  channel.sink.add(message);
+                });
+              }
+            ''', stayAlive: true);
+            queue = new StreamQueue(channel.stream);
+            sink = channel.sink;
+          });
+
+          test("echoes a number", () {
+            expect(queue.next, completion(equals(123)));
+            sink.add(123);
+          });
+
+          test("echoes a string", () {
+            expect(queue.next, completion(equals("wow")));
+            sink.add("wow");
+          });
+        }
+      """).create();
+
+      var test = runTest(["test.dart"]);
+      test.stdout.expect(containsInOrder([
+        "+0: echoes a number",
+        "+1: echoes a string",
+        "+2: All tests passed!"
+      ]));
+      test.shouldExit(0);
+    });
   });
 }