Support asynchronous main methods.

This also releases 0.12.0-rc.1.

Closes #92

R=kevmoo@google.com

Review URL: https://codereview.chromium.org//1107743002
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae970ae..14c532b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@
 * Remove `handleExternalError`. This was never used in practice and its function
   was unclear.
 
+* If a test suite's `main()` method returns a `Future`, tests may be declared
+  until that `Future` returns.
+
 ### 0.12.0-rc.0
 
 * Tests, groups, and suites can now be configured on a platform-by-platform
diff --git a/lib/src/runner/browser/iframe_listener.dart b/lib/src/runner/browser/iframe_listener.dart
index b345f37..7680451 100644
--- a/lib/src/runner/browser/iframe_listener.dart
+++ b/lib/src/runner/browser/iframe_listener.dart
@@ -56,24 +56,23 @@
     }
 
     var declarer = new Declarer();
-    try {
-      runZoned(main, zoneValues: {#test.declarer: declarer});
-    } catch (error, stackTrace) {
+    runZoned(() => new Future.sync(main), zoneValues: {
+      #test.declarer: declarer
+    }).then((_) {
+      var url = Uri.parse(window.location.href);
+      var message = JSON.decode(Uri.decodeFull(url.fragment));
+      var metadata = new Metadata.deserialize(message['metadata']);
+      var browser = TestPlatform.find(message['browser']);
+
+      var suite = new Suite(declarer.tests, metadata: metadata)
+          .forPlatform(browser);
+      new IframeListener._(suite)._listen(channel);
+    }, onError: (error, stackTrace) {
       channel.sink.add({
         "type": "error",
         "error": RemoteException.serialize(error, stackTrace)
       });
-      return;
-    }
-
-    var url = Uri.parse(window.location.href);
-    var message = JSON.decode(Uri.decodeFull(url.fragment));
-    var metadata = new Metadata.deserialize(message['metadata']);
-    var browser = TestPlatform.find(message['browser']);
-
-    var suite = new Suite(declarer.tests, metadata: metadata)
-        .forPlatform(browser);
-    new IframeListener._(suite)._listen(channel);
+    });
   }
 
   /// Constructs a [MultiChannel] wrapping the `postMessage` communication with
diff --git a/lib/src/runner/vm/isolate_listener.dart b/lib/src/runner/vm/isolate_listener.dart
index ed23a1d..a25c9d6 100644
--- a/lib/src/runner/vm/isolate_listener.dart
+++ b/lib/src/runner/vm/isolate_listener.dart
@@ -51,19 +51,18 @@
     }
 
     var declarer = new Declarer();
-    try {
-      runZoned(main, zoneValues: {#test.declarer: declarer});
-    } catch (error, stackTrace) {
+    runZoned(() => new Future.sync(main), zoneValues: {
+      #test.declarer: declarer
+    }).then((_) {
+      var suite = new Suite(declarer.tests, metadata: metadata)
+          .forPlatform(TestPlatform.vm, os: currentOS);
+      new IsolateListener._(suite)._listen(sendPort);
+    }, onError: (error, stackTrace) {
       sendPort.send({
         "type": "error",
         "error": RemoteException.serialize(error, stackTrace)
       });
-      return;
-    }
-
-    var suite = new Suite(declarer.tests, metadata: metadata)
-        .forPlatform(TestPlatform.vm, os: currentOS);
-    new IsolateListener._(suite)._listen(sendPort);
+    });
   }
 
   /// Sends a message over [sendPort] indicating that the tests failed to load.
diff --git a/pubspec.yaml b/pubspec.yaml
index 6c96002..3e7ab79 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test
-version: 0.12.0-dev
+version: 0.12.0-rc.1
 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/browser/loader_test.dart b/test/runner/browser/loader_test.dart
index fbf549c..9dd4f62 100644
--- a/test/runner/browser/loader_test.dart
+++ b/test/runner/browser/loader_test.dart
@@ -88,6 +88,39 @@
     });
   });
 
+
+  test("loads tests that are defined asynchronously", () {
+    new File(p.join(_sandbox, 'a_test.dart')).writeAsStringSync("""
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+Future main() {
+  return new Future(() {
+    test("success", () {});
+
+    return new Future(() {
+      test("failure", () => throw new TestFailure('oh no'));
+
+      return new Future(() {
+        test("error", () => throw 'oh no');
+      });
+    });
+  });
+}
+""");
+
+    return _loader.loadFile(p.join(_sandbox, 'a_test.dart')).toList()
+        .then((suites) {
+      expect(suites, hasLength(1));
+      var suite = suites.first;
+      expect(suite.tests, hasLength(3));
+      expect(suite.tests[0].name, equals("success"));
+      expect(suite.tests[1].name, equals("failure"));
+      expect(suite.tests[2].name, equals("error"));
+    });
+  });
+
   test("loads a suite both in the browser and the VM", () {
     var loader = new Loader([TestPlatform.vm, TestPlatform.chrome],
         root: _sandbox,
diff --git a/test/runner/isolate_listener_test.dart b/test/runner/isolate_listener_test.dart
index 0960c5a..2952832 100644
--- a/test/runner/isolate_listener_test.dart
+++ b/test/runner/isolate_listener_test.dart
@@ -56,6 +56,21 @@
     });
   });
 
+  test("waits for a returned future sending a response", () {
+    return _spawnIsolate(_asyncTests).then((receivePort) {
+      return receivePort.first;
+    }).then((response) {
+      expect(response, containsPair("type", "success"));
+      expect(response, contains("tests"));
+
+      var tests = response["tests"];
+      expect(tests, hasLength(3));
+      expect(tests[0], containsPair("name", "successful 1"));
+      expect(tests[1], containsPair("name", "successful 2"));
+      expect(tests[2], containsPair("name", "successful 3"));
+    });
+  });
+
   test("sends an error response if loading fails", () {
     return _spawnIsolate(_loadError).then((receivePort) {
       return receivePort.first;
@@ -330,6 +345,23 @@
   });
 }
 
+/// An isolate entrypoint that defines three tests asynchronously.
+void _asyncTests(SendPort sendPort) {
+  IsolateListener.start(sendPort, new Metadata(), () => () {
+    return new Future(() {
+      test("successful 1", () {});
+
+      return new Future(() {
+        test("successful 2", () {});
+
+        return new Future(() {
+          test("successful 3", () {});
+        });
+      });
+    });
+  });
+}
+
 /// An isolate entrypoint that defines a test that fails.
 void _failingTest(SendPort sendPort) {
   IsolateListener.start(sendPort, new Metadata(), () => () {