From 0a098e7f895842db421b7fad29eb1de0d1047ee8 Mon Sep 17 00:00:00 2001
From: Natalie Weizenbaum <nweiz@google.com>
Date: Thu, 19 Feb 2015 15:44:20 -0800
Subject: [PATCH] Replace the existing unittest APIs with the new runner
 infrastructure.

This preserves the shape of the previous API, but stubs out the functionality.
This is a temporary measure designed to make it easier for users to try the
runner out on their existing tests.

R=kevmoo@google.com
Closes #2

Review URL: https://codereview.chromium.org//934413002
---
 .status                                       |  52 +-
 CHANGELOG.md                                  |  22 +-
 lib/compact_vm_config.dart                    | 212 +-------
 lib/coverage_controller.js                    |  28 +-
 lib/html_config.dart                          | 174 +------
 lib/html_enhanced_config.dart                 | 404 +--------------
 lib/html_individual_config.dart               |  51 +-
 lib/src/configuration.dart                    |  54 +-
 ...pected_function.dart => expect_async.dart} |  89 ++--
 lib/src/future_matchers.dart                  |  20 +-
 lib/src/group_context.dart                    |  75 ---
 lib/src/internal_test_case.dart               | 227 --------
 lib/src/prints_matcher.dart                   |  14 +-
 lib/src/simple_configuration.dart             | 113 +---
 lib/src/test_case.dart                        |  42 +-
 lib/src/test_environment.dart                 |  74 ---
 lib/src/throws_matcher.dart                   |  34 +-
 lib/src/utils.dart                            |  40 --
 lib/test_controller.js                        | 230 +-------
 lib/unittest.dart                             | 490 ++++--------------
 lib/vm_config.dart                            |  59 +--
 pubspec.yaml                                  |   4 -
 test/async_exception_test.dart                |  32 --
 test/async_exception_with_future_test.dart    |  42 --
 test/async_setup_teardown_test.dart           |  71 ---
 test/breath_test.dart                         |  51 --
 test/completion_test.dart                     |  33 --
 test/console_reporter_test.dart               |  47 +-
 test/correct_callback_test.dart               |  27 -
 test/exception_test.dart                      |  21 -
 test/excess_callback_test.dart                |  42 --
 test/expect_async_args_test.dart              |  48 --
 test/expect_async_test.dart                   | 348 ++++++++++---
 test/future_matchers_test.dart                |  98 ----
 test/group_name_test.dart                     |  24 -
 test/invalid_ops_test.dart                    |  27 -
 test/io.dart                                  |  16 +-
 test/late_exception_test.dart                 |  37 --
 test/loader_test.dart                         |   9 +-
 test/matcher/completion_test.dart             | 100 ++++
 test/matcher/prints_test.dart                 | 149 ++++++
 test/matcher/throws_test.dart                 | 149 ++++++
 test/matcher/throws_type_test.dart            | 178 +++++++
 test/matchers_minified_test.dart              | 139 -----
 test/matchers_unminified_test.dart            | 136 -----
 test/middle_exception_test.dart               |  32 --
 test/missing_tick_test.dart                   |  27 -
 test/nested_groups_setup_teardown_test.dart   |  64 ---
 test/prints_matcher_test.dart                 | 144 -----
 test/protect_async_test.dart                  |  58 ---
 test/returning_future_test.dart               |  70 ---
 .../returning_future_using_runasync_test.dart |  87 ----
 test/runner_test.dart                         |  29 +-
 test/runtests_without_tests_test.dart         |  19 -
 test/setup_and_teardown_test.dart             |  35 --
 test/setup_test.dart                          |  27 -
 test/single_correct_test.dart                 |  19 -
 test/single_failing_test.dart                 |  19 -
 test/skipped_soloed_nested_test.dart          |  87 ----
 test/teardown_test.dart                       |  30 --
 test/test_utils.dart                          |  81 ---
 test/testcases_immutable_test.dart            |  22 -
 test/throws_matchers_test.dart                |  69 ---
 test/utils.dart                               | 169 +++++-
 test/vm_listener_test.dart                    |  22 +-
 test/with_test_environment_test.dart          |  84 ---
 66 files changed, 1381 insertions(+), 4145 deletions(-)
 rename lib/src/{expected_function.dart => expect_async.dart} (66%)
 delete mode 100644 lib/src/group_context.dart
 delete mode 100644 lib/src/internal_test_case.dart
 delete mode 100644 lib/src/test_environment.dart
 delete mode 100644 test/async_exception_test.dart
 delete mode 100644 test/async_exception_with_future_test.dart
 delete mode 100644 test/async_setup_teardown_test.dart
 delete mode 100644 test/breath_test.dart
 delete mode 100644 test/completion_test.dart
 delete mode 100644 test/correct_callback_test.dart
 delete mode 100644 test/exception_test.dart
 delete mode 100644 test/excess_callback_test.dart
 delete mode 100644 test/expect_async_args_test.dart
 delete mode 100644 test/future_matchers_test.dart
 delete mode 100644 test/group_name_test.dart
 delete mode 100644 test/invalid_ops_test.dart
 delete mode 100644 test/late_exception_test.dart
 create mode 100644 test/matcher/completion_test.dart
 create mode 100644 test/matcher/prints_test.dart
 create mode 100644 test/matcher/throws_test.dart
 create mode 100644 test/matcher/throws_type_test.dart
 delete mode 100644 test/matchers_minified_test.dart
 delete mode 100644 test/matchers_unminified_test.dart
 delete mode 100644 test/middle_exception_test.dart
 delete mode 100644 test/missing_tick_test.dart
 delete mode 100644 test/nested_groups_setup_teardown_test.dart
 delete mode 100644 test/prints_matcher_test.dart
 delete mode 100644 test/protect_async_test.dart
 delete mode 100644 test/returning_future_test.dart
 delete mode 100644 test/returning_future_using_runasync_test.dart
 delete mode 100644 test/runtests_without_tests_test.dart
 delete mode 100644 test/setup_and_teardown_test.dart
 delete mode 100644 test/setup_test.dart
 delete mode 100644 test/single_correct_test.dart
 delete mode 100644 test/single_failing_test.dart
 delete mode 100644 test/skipped_soloed_nested_test.dart
 delete mode 100644 test/teardown_test.dart
 delete mode 100644 test/test_utils.dart
 delete mode 100644 test/testcases_immutable_test.dart
 delete mode 100644 test/throws_matchers_test.dart
 delete mode 100644 test/with_test_environment_test.dart

diff --git a/.status b/.status
index b43df788..ff771caf 100644
--- a/.status
+++ b/.status
@@ -31,54 +31,4 @@ test/runner_test: SkipByDesign
 test/vm_listener_test: SkipByDesign
 
 [ $runtime == safari ]
-test/prints_matcher_test: Fail # Issue 4
-
-[ $runtime == jsshell ]
-test/missing_tick_test: Fail # Timer interface not supported: dartbug.com/7728
-test/nested_groups_setup_teardown_test: RuntimeError # http://dartbug.com/10109
-
-[ $compiler == none && ( $runtime == dartium || $runtime == drt || $runtime == ContentShellOnAndroid) ]
-# Skip serialization test that explicitly has no library declaration in the
-# test on Dartium, which requires all tests to have a library.
-test/async_exception_test: RuntimeError # 13921
-test/async_exception_with_future_test: RuntimeError # 13921
-test/async_setup_teardown_test: RuntimeError # 13921
-test/completion_test: RuntimeError # 13921
-test/correct_callback_test: RuntimeError # 13921
-test/exception_test: RuntimeError # 13921
-test/excess_callback_test: RuntimeError # 13921
-test/expect_async_args_test: RuntimeError # 13921
-test/expect_async_test: RuntimeError # 13921
-test/future_matchers_test: RuntimeError # 13921
-test/group_name_test: RuntimeError # 13921
-test/invalid_ops_test: RuntimeError # 13921
-test/late_exception_test: RuntimeError # 13921
-test/middle_exception_test: RuntimeError # 13921
-test/nested_groups_setup_teardown_test: RuntimeError # 13921
-test/prints_matcher_test: RuntimeError # 13921
-test/protect_async_test: RuntimeError # 13921
-test/returning_future_test: RuntimeError # 13921
-test/returning_future_using_runasync_test: RuntimeError # 13921
-test/runtests_without_tests_test: RuntimeError # 13921
-test/setup_and_teardown_test: RuntimeError # 13921
-test/setup_test: RuntimeError # 13921
-test/single_correct_test: RuntimeError # 13921
-test/single_failing_test: RuntimeError # 13921
-test/skipped_soloed_nested_test: RuntimeError # 13921
-test/teardown_test: RuntimeError # 13921
-test/testcases_immutable_test: RuntimeError # 13921
-
-[ $compiler == none && $browser ]
-test/missing_tick_test: RuntimeError # Expected to fail, due to timeout.
-
-[ $compiler == dart2js && $minified ]
-# The unminified matcher tests test that the real names of Dart types are
-# printed. Minified versions of these tests exist that test the behavior when
-# minified.
-test/*_unminified_test: SkipByDesign # DO NOT COPY THIS UNLESS YOU WORK ON DART2JS
-
-[ $minified == false ]
-# The minified matcher tests test that the minified names of Dart types are
-# printed. Unminified versions of these tests exist that test the behavior when
-# not minified.
-test/*_minified_test: SkipByDesign # DO NOT COPY THIS UNLESS YOU WORK ON DART2JS
+test/matcher/prints_test: Fail # Issue 4
diff --git a/CHANGELOG.md b/CHANGELOG.md
index be183d2c..c5c1362b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,24 @@
-##0.12.0
+##0.12.0-alpha.0
+
+* Added support for a test runner, which can be run via `pub run
+  unittest:unittest`. By default it runs all files recursively in the `test/`
+  directory that end in `_test.dart` and aren't in a `packages/` directory.
+
+* As part of moving to a runner-based model, most test configuration is moving
+  out of the test file and into the runner. As such, many ancillary APIs are
+  stubbed out and marked as deprecated. They still exist to make adoption
+  easier, but they're now no-ops and will be removed before the stable 0.12.0
+  release. These APIs include `skip_` and `solo_` functions, `Configuration` and
+  all its subclasses, `TestCase`, `TestFunction`, `unittestConfiguration`,
+  `formatStacks`, `filterStacks`, `groupSep`, `logMessage`, `testCases`,
+  `BREATH_INTERVAL`, `currentTestCase`, `PASS`, `FAIL`, `ERROR`, `filterTests`,
+  `runTests`, `ensureInitialized`, `setSoloTest`, `enableTest`, `disableTest`,
+  and `withTestEnvironment`.
 
 * Removed `FailureHandler`, `DefaultFailureHandler`,
   `configureExpectFailureHandler`, and `getOrCreateExpectFailureHandler` which
-  are exported from the `matcher` package and will be removed. They existed
-  to enable integration between `unittest` and `matcher` that is being
-  streamlined.
+  used to be exported from the `matcher` package. They existed to enable
+  integration between `unittest` and `matcher` that has been streamlined.
 
 * Moved a number of APIs from `matcher` into `unittest`, including:
   `completes`, `completion`, `ErrorFormatter`, `expect`,`fail`, `prints`,
diff --git a/lib/compact_vm_config.dart b/lib/compact_vm_config.dart
index 1fadfc1c..fdc42e10 100644
--- a/lib/compact_vm_config.dart
+++ b/lib/compact_vm_config.dart
@@ -2,213 +2,19 @@
 // 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.
 
-/// A test configuration that generates a compact 1-line progress bar. The bar
-/// is updated in-place before and after each test is executed. If all tests
-/// pass, only a couple of lines are printed in the terminal. If a test fails,
-/// the failure is shown and the progress bar continues to be updated below it.
+@deprecated
 library unittest.compact_vm_config;
 
-import 'dart:async';
-import 'dart:io';
-import 'dart:isolate';
-
-import 'unittest.dart';
-import 'src/utils.dart';
 import 'vm_config.dart';
 
-const String _GREEN = '\u001b[32m';
-const String _RED = '\u001b[31m';
-const String _NONE = '\u001b[0m';
-
+@deprecated
 const int MAX_LINE = 80;
 
-class CompactVMConfiguration extends VMConfiguration {
-  // The VM won't shut down if a receive port is open. Use this to make sure
-  // we correctly wait for asynchronous tests.
-  ReceivePort _receivePort;
-
-  DateTime _start;
-  Set<int> _passing = new Set();
-  Set<int> _failing = new Set();
-  int get _pass => _passing.length;
-  int get _fail => _failing.length;
-
-  void onInit() {
-    _receivePort = new ReceivePort();
-    // Override and don't call the superclass onInit() to avoid printing the
-    // "unittest-suite-..." boilerplate.
-  }
-
-  void onStart() {
-    _start = new DateTime.now();
-  }
-
-  void onTestStart(TestCase test) {
-    super.onTestStart(test);
-    _progressLine(test.description);
-  }
-
-  void onTestResult(TestCase test) {
-    super.onTestResult(test);
-    if (test.result == PASS) {
-      _passing.add(test.id);
-      _progressLine(test.description);
-    } else {
-      _failing.add(test.id);
-      _progressLine(test.description);
-      _print();
-      if (test.message != '') {
-        _print(indent(test.message));
-      }
-
-      if (test.stackTrace != null) {
-        _print(indent(test.stackTrace.toString()));
-      }
-    }
-  }
-
-  void onTestResultChanged(TestCase test) {
-    _passing.remove(test.id);
-    _failing.add(test.id);
-    _progressLine(test.description);
-    _print();
-    if (test.message != '') {
-      _print(indent(test.message));
-    }
-
-    if (test.stackTrace != null) {
-      _print(indent(test.stackTrace.toString()));
-    }
-  }
-
-  void onDone(bool success) {
-    // Override and don't call the superclass onDone() to avoid printing the
-    // "unittest-suite-..." boilerplate.
-    Future.wait([stdout.close(), stderr.close()]).then((_) {
-      _receivePort.close();
-      exit(success ? 0 : 1);
-    });
-  }
-
-  void onSummary(int passed, int failed, int errors, List<TestCase> results,
-      String uncaughtError) {
-    if (passed == 0 && failed == 0 && errors == 0 && uncaughtError == null) {
-      _print('\nNo tests ran.');
-    } else if (failed == 0 && errors == 0 && uncaughtError == null) {
-      _progressLine('All tests passed!', _NONE);
-      _print();
-    } else {
-      _progressLine('Some tests failed.', _RED);
-      _print();
-      if (uncaughtError != null) {
-        _print('Top-level uncaught error: $uncaughtError');
-      }
-      _print('$passed PASSED, $failed FAILED, $errors ERRORS');
-    }
-  }
-
-  int _lastLength = 0;
-
-  final int _nonVisiblePrefix = 1 + _GREEN.length + _NONE.length;
-
-  void _progressLine(String message, [String color = _NONE]) {
-    var duration = (new DateTime.now()).difference(_start);
-    var buffer = new StringBuffer();
-    // \r moves back to the beginning of the current line.
-    buffer.write('\r${_timeString(duration)} ');
-    buffer.write(_GREEN);
-    buffer.write('+');
-    buffer.write(_pass);
-    buffer.write(_NONE);
-    if (_fail != 0) {
-      buffer.write(_RED);
-      buffer.write(' -');
-      buffer.write(_fail);
-      buffer.write(_NONE);
-    }
-    buffer.write(': ');
-    buffer.write(color);
-
-    // Ensure the line fits under MAX_LINE. [buffer] includes the color escape
-    // sequences too. Because these sequences are not visible characters, we
-    // make sure they are not counted towards the limit.
-    int nonVisible = _nonVisiblePrefix +
-        color.length +
-        (_fail != 0 ? (_RED.length + _NONE.length) : 0);
-    int len = buffer.length - nonVisible;
-    buffer.write(_snippet(message, MAX_LINE - len));
-    buffer.write(_NONE);
-
-    // Pad the rest of the line so that it looks erased.
-    len = buffer.length - nonVisible - _NONE.length;
-    if (len > _lastLength) {
-      _lastLength = len;
-    } else {
-      while (len < _lastLength) {
-        buffer.write(' ');
-        _lastLength--;
-      }
-    }
-    stdout.write(buffer.toString());
-  }
-
-  String _padTime(int time) =>
-      (time == 0) ? '00' : ((time < 10) ? '0$time' : '$time');
-
-  String _timeString(Duration duration) {
-    var min = duration.inMinutes;
-    var sec = duration.inSeconds % 60;
-    return '${_padTime(min)}:${_padTime(sec)}';
-  }
-
-  String _snippet(String text, int maxLength) {
-    // Return the full message if it fits
-    if (text.length <= maxLength) return text;
-
-    // If we can fit the first and last three words, do so.
-    var words = text.split(' ');
-    if (words.length > 1) {
-      int i = words.length;
-      var len = words.first.length + 4;
-      do {
-        len += 1 + words[--i].length;
-      } while (len <= maxLength && i > 0);
-      if (len > maxLength || i == 0) i++;
-      if (i < words.length - 4) {
-        // Require at least 3 words at the end.
-        var buffer = new StringBuffer();
-        buffer.write(words.first);
-        buffer.write(' ...');
-        for ( ; i < words.length; i++) {
-          buffer.write(' ');
-          buffer.write(words[i]);
-        }
-        return buffer.toString();
-      }
-    }
-
-    // Otherwise truncate to return the trailing text, but attempt to start at
-    // the beginning of a word.
-    var res = text.substring(text.length - maxLength + 4);
-    var firstSpace = res.indexOf(' ');
-    if (firstSpace > 0) {
-      res = res.substring(firstSpace);
-    }
-    return '...$res';
-  }
-}
-
-// TODO(sigmund): delete when dartbug.com/17269 is fixed (use `print` instead).
-_print([value = '']) => stdout.write('$value\n');
-
-void useCompactVMConfiguration() {
-  // If the test is running on the Dart buildbots, we don't want to use this
-  // config since it's output may not be what the bots expect.
-  if (Platform.environment['LOGNAME'] == 'chrome-bot') {
-    return;
-  }
-
-  unittestConfiguration = _singleton;
-}
+/// This is a stub class used to preserve compatibility with unittest 0.11.*.
+///
+/// It will be removed before the next version is released.
+@deprecated
+class CompactVMConfiguration extends VMConfiguration {}
 
-final _singleton = new CompactVMConfiguration();
+@deprecated
+void useCompactVMConfiguration() {}
diff --git a/lib/coverage_controller.js b/lib/coverage_controller.js
index 1cf21ae1..11822790 100644
--- a/lib/coverage_controller.js
+++ b/lib/coverage_controller.js
@@ -2,30 +2,4 @@
 // 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.
 
-/**
- * Coverage controller logic - used by coverage test harness to embed tests in
- * content shell and extract coverage information.
- */
-
-var LONG_LINE = 60000;
-
-function onReceive(e) {
-  if (e.data == 'unittest-suite-done') {
-    var s = JSON.stringify(top._$jscoverage);
-    var res = '';
-    // conent shell has a bug on lines longer than 2^16, so we split them
-    while (s.length > LONG_LINE) {
-      res += s.substr(0, LONG_LINE) + '<br>\n';
-      s = s.substr(LONG_LINE);
-    }
-    res += s;
-    window.document.body.innerHTML = res;
-    window.layoutTestController.notifyDone();
-  }
-}
-
-if (window.layoutTestController) {
-  window.layoutTestController.dumpAsText();
-  window.layoutTestController.waitUntilDone();
-  window.addEventListener("message", onReceive, false);
-}
+/** This file is deprecated and will be removed before the next release. */
diff --git a/lib/html_config.dart b/lib/html_config.dart
index 210f0f5e..4b8a32f2 100644
--- a/lib/html_config.dart
+++ b/lib/html_config.dart
@@ -2,174 +2,20 @@
 // 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.
 
-/// A simple unit test library for running tests in a browser.
+@deprecated
 library unittest.html_config;
 
-import 'dart:async';
-import 'dart:convert';
-import 'dart:html';
-import 'dart:js' as js;
-import 'unittest.dart';
-
-/// Creates a table showing tests results in HTML.
-void _showResultsInPage(int passed, int failed, int errors,
-    List<TestCase> results, bool isLayoutTest, String uncaughtError) {
-  if (isLayoutTest && (passed == results.length) && uncaughtError == null) {
-    document.body.innerHtml = "PASS";
-  } else {
-    var newBody = new StringBuffer();
-    newBody.write("<table class='unittest-table'><tbody>");
-    newBody.write(passed == results.length && uncaughtError == null
-        ? "<tr><td colspan='3' class='unittest-pass'>PASS</td></tr>"
-        : "<tr><td colspan='3' class='unittest-fail'>FAIL</td></tr>");
-
-    for (final test_ in results) {
-      newBody.write(_toHtml(test_));
-    }
-
-    if (uncaughtError != null) {
-      newBody.write('''<tr>
-          <td>--</td>
-          <td class="unittest-error">ERROR</td>
-          <td>Uncaught error: $uncaughtError</td>
-        </tr>''');
-    }
-
-    if (passed == results.length && uncaughtError == null) {
-      newBody.write("""
-          <tr><td colspan='3' class='unittest-pass'>
-            All ${passed} tests passed
-          </td></tr>""");
-    } else {
-      newBody.write("""
-          <tr><td colspan='3'>Total
-            <span class='unittest-pass'>${passed} passed</span>,
-            <span class='unittest-fail'>${failed} failed</span>
-            <span class='unittest-error'>
-            ${errors + (uncaughtError == null ? 0 : 1)} errors</span>
-          </td></tr>""");
-    }
-    newBody.write("</tbody></table>");
-    document.body.innerHtml = newBody.toString();
-
-    window.onHashChange.listen((_) {
-      // Location may change from individual tests setting the hash tag.
-      if (window.location.hash != null &&
-          window.location.hash.contains('testFilter')) {
-        window.location.reload();
-      }
-    });
-  }
-}
-
-String _toHtml(TestCase test_) {
-  if (!test_.isComplete) {
-    return '''
-        <tr>
-          <td>${test_.id}</td>
-          <td class="unittest-error">NO STATUS</td>
-          <td>Test did not complete</td>
-        </tr>''';
-  }
-
-  var html = '''
-      <tr>
-        <td>${test_.id}</td>
-        <td class="unittest-${test_.result}">${test_.result.toUpperCase()}</td>
-        <td>Expectation: <a href="#testFilter=${test_.description}">${test_.description}</a>. ${HTML_ESCAPE.convert(test_.message)}</td>
-      </tr>''';
-
-  if (test_.stackTrace != null) {
-    html = '$html<tr><td></td><td colspan="2"><pre>' +
-        HTML_ESCAPE.convert(test_.stackTrace.toString()) +
-        '</pre></td></tr>';
-  }
-
-  return html;
-}
+import 'src/simple_configuration.dart';
 
+/// This is a stub class used to preserve compatibility with unittest 0.11.*.
+///
+/// It will be removed before the next version is released.
+@deprecated
 class HtmlConfiguration extends SimpleConfiguration {
-  /// Whether this is run within dartium layout tests.
-  final bool _isLayoutTest;
-  HtmlConfiguration(this._isLayoutTest);
-
-  StreamSubscription<Event> _onErrorSubscription;
-  StreamSubscription<Event> _onMessageSubscription;
-
-  void _installHandlers() {
-    if (_onErrorSubscription == null) {
-      _onErrorSubscription = window.onError.listen((e) {
-        // Some tests may expect this and have no way to suppress the error.
-        if (js.context['testExpectsGlobalError'] != true) {
-          handleExternalError(e, '(DOM callback has errors)');
-        }
-      });
-    }
-    if (_onMessageSubscription == null) {
-      _onMessageSubscription =
-          window.onMessage.listen((e) => processMessage(e));
-    }
-  }
-
-  void _uninstallHandlers() {
-    if (_onErrorSubscription != null) {
-      _onErrorSubscription.cancel();
-      _onErrorSubscription = null;
-    }
-    if (_onMessageSubscription != null) {
-      _onMessageSubscription.cancel();
-      _onMessageSubscription = null;
-    }
-  }
-
-  void processMessage(e) {
-    if ('unittest-suite-external-error' == e.data) {
-      handleExternalError('<unknown>', '(external error detected)');
-    }
-  }
-
-  void onInit() {
-    // For Dart internal tests, we want to turn off stack frame
-    // filtering, which we do with this meta-header.
-    var meta = querySelector('meta[name="dart.unittest"]');
-    filterStacks =
-        meta == null ? true : !meta.content.contains('full-stack-traces');
-    _installHandlers();
-    window.postMessage('unittest-suite-wait-for-done', '*');
-  }
-
-  void onStart() {
-    // If the URL has a #testFilter=testName then filter tests to that.
-    // This is used to make it easy to run a single test- but is only intended
-    // for interactive debugging scenarios.
-    var hash = window.location.hash;
-    if (hash != null && hash.length > 1) {
-      var params = hash.substring(1).split('&');
-      for (var param in params) {
-        var parts = param.split('=');
-        if (parts.length == 2 && parts[0] == 'testFilter') {
-          filterTests('^${parts[1]}');
-        }
-      }
-    }
-    super.onStart();
-  }
-
-  void onSummary(int passed, int failed, int errors, List<TestCase> results,
-      String uncaughtError) {
-    _showResultsInPage(
-        passed, failed, errors, results, _isLayoutTest, uncaughtError);
-  }
-
-  void onDone(bool success) {
-    _uninstallHandlers();
-    window.postMessage('unittest-suite-done', '*');
-  }
-}
+  HtmlConfiguration(bool isLayoutTest);
 
-void useHtmlConfiguration([bool isLayoutTest = false]) {
-  unittestConfiguration = isLayoutTest ? _singletonLayout : _singletonNotLayout;
+  void processMessage(e) {}
 }
 
-final _singletonLayout = new HtmlConfiguration(true);
-final _singletonNotLayout = new HtmlConfiguration(false);
+@deprecated
+void useHtmlConfiguration([bool isLayoutTest]) {}
diff --git a/lib/html_enhanced_config.dart b/lib/html_enhanced_config.dart
index 0178ccb0..65e14194 100644
--- a/lib/html_enhanced_config.dart
+++ b/lib/html_enhanced_config.dart
@@ -2,404 +2,20 @@
 // 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.
 
-/// A simple unit test library for running tests in a browser.
-///
-/// Provides enhanced HTML output with collapsible group headers
-/// and other at-a-glance information about the test results.
+@deprecated
 library unittest.html_enhanced_config;
 
-import 'dart:collection' show LinkedHashMap;
-import 'dart:convert';
-import 'dart:html';
-import 'unittest.dart';
+import 'src/simple_configuration.dart';
 
+/// This is a stub class used to preserve compatibility with unittest 0.11.*.
+///
+/// It will be removed before the next version is released.
+@deprecated
 class HtmlEnhancedConfiguration extends SimpleConfiguration {
-  /// Whether this is run within dartium layout tests.
-  final bool _isLayoutTest;
-  HtmlEnhancedConfiguration(this._isLayoutTest);
-
-  var _onErrorSubscription = null;
-  var _onMessageSubscription = null;
-
-  void _installOnErrorHandler() {
-    if (_onErrorSubscription == null) {
-      // Listen for uncaught errors.
-      _onErrorSubscription = window.onError
-          .listen((e) => handleExternalError(e, '(DOM callback has errors)'));
-    }
-  }
-
-  void _installOnMessageHandler() {
-    if (_onMessageSubscription == null) {
-      // Listen for errors from JS.
-      _onMessageSubscription =
-          window.onMessage.listen((e) => processMessage(e));
-    }
-  }
-
-  void _installHandlers() {
-    _installOnErrorHandler();
-    _installOnMessageHandler();
-  }
-
-  void _uninstallHandlers() {
-    if (_onErrorSubscription != null) {
-      _onErrorSubscription.cancel();
-      _onErrorSubscription = null;
-    }
-    if (_onMessageSubscription != null) {
-      _onMessageSubscription.cancel();
-      _onMessageSubscription = null;
-    }
-  }
-
-  void processMessage(e) {
-    if ('unittest-suite-external-error' == e.data) {
-      handleExternalError('<unknown>', '(external error detected)');
-    }
-  }
-
-  void onInit() {
-    _installHandlers();
-    //initialize and load CSS
-    final String _CSSID = '_unittestcss_';
-
-    var cssElement = document.head.querySelector('#${_CSSID}');
-    if (cssElement == null) {
-      cssElement = new StyleElement();
-      cssElement.id = _CSSID;
-      document.head.append(cssElement);
-    }
-
-    cssElement.text = _htmlTestCSS;
-    window.postMessage('unittest-suite-wait-for-done', '*');
-  }
-
-  void onStart() {
-    // Listen for uncaught errors.
-    _installOnErrorHandler();
-  }
-
-  void onSummary(int passed, int failed, int errors, List<TestCase> results,
-      String uncaughtError) {
-    _showInteractiveResultsInPage(
-        passed, failed, errors, results, _isLayoutTest, uncaughtError);
-  }
-
-  void onDone(bool success) {
-    _uninstallHandlers();
-    window.postMessage('unittest-suite-done', '*');
-  }
-
-  void _showInteractiveResultsInPage(int passed, int failed, int errors,
-      List<TestCase> results, bool isLayoutTest, String uncaughtError) {
-    if (isLayoutTest && passed == results.length) {
-      document.body.innerHtml = "PASS";
-    } else {
-      // changed the StringBuffer to an Element fragment
-      Element te = new Element.html('<div class="unittest-table"></div>');
-
-      te.children.add(new Element.html(passed == results.length
-          ? "<div class='unittest-overall unittest-pass'>PASS</div>"
-          : "<div class='unittest-overall unittest-fail'>FAIL</div>"));
-
-      // moved summary to the top since web browsers
-      // don't auto-scroll to the bottom like consoles typically do.
-      if (passed == results.length && uncaughtError == null) {
-        te.children.add(new Element.html("""
-          <div class='unittest-pass'>All ${passed} tests passed</div>"""));
-      } else {
-        if (uncaughtError != null) {
-          te.children.add(new Element.html("""
-            <div class='unittest-summary'>
-              <span class='unittest-error'>Uncaught error: $uncaughtError</span>
-            </div>"""));
-        }
-
-        te.children.add(new Element.html("""
-          <div class='unittest-summary'>
-            <span class='unittest-pass'>Total ${passed} passed</span>,
-            <span class='unittest-fail'>${failed} failed</span>,
-            <span class='unittest-error'>
-            ${errors + (uncaughtError == null ? 0 : 1)} errors</span>
-          </div>"""));
-      }
-
-      te.children.add(new Element.html("""
-        <div><button id='btnCollapseAll'>Collapse All</button></div>
-       """));
-
-      // handle the click event for the collapse all button
-      te.querySelector('#btnCollapseAll').onClick.listen((_) {
-        document
-            .querySelectorAll('.unittest-row')
-            .forEach((el) => el.attributes['class'] = el.attributes['class']
-                .replaceAll('unittest-row ', 'unittest-row-hidden '));
-      });
-
-      var previousGroup = '';
-      var groupPassFail = true;
-
-      // order by group and sort numerically within each group
-      var groupedBy = new LinkedHashMap<String, List<TestCase>>();
-
-      for (final t in results) {
-        if (!groupedBy.containsKey(t.currentGroup)) {
-          groupedBy[t.currentGroup] = new List<TestCase>();
-        }
-
-        groupedBy[t.currentGroup].add(t);
-      }
-
-      // flatten the list again with tests ordered
-      List<TestCase> flattened = new List<TestCase>();
-
-      groupedBy.values.forEach((tList) {
-        tList.sort((tcA, tcB) => tcA.id - tcB.id);
-        flattened.addAll(tList);
-      });
-
-      var nonAlphanumeric = new RegExp('[^a-z0-9A-Z]');
-
-      // output group headers and test rows
-      for (final test_ in flattened) {
-
-        // replace everything but numbers and letters from the group name with
-        // '_' so we can use in id and class properties.
-        var safeGroup = test_.currentGroup.replaceAll(nonAlphanumeric, '_');
-
-        if (test_.currentGroup != previousGroup) {
-          previousGroup = test_.currentGroup;
-
-          var testsInGroup = results
-              .where((TestCase t) => t.currentGroup == previousGroup)
-              .toList();
-          var groupTotalTestCount = testsInGroup.length;
-          var groupTestPassedCount =
-              testsInGroup.where((TestCase t) => t.result == 'pass').length;
-          groupPassFail = groupTotalTestCount == groupTestPassedCount;
-          var passFailClass = "unittest-group-status unittest-group-"
-              "status-${groupPassFail ? 'pass' : 'fail'}";
-
-          te.children.add(new Element.html("""
-            <div>
-              <div id='${safeGroup}'
-                   class='unittest-group ${safeGroup} test${safeGroup}'>
-                <div ${_isIE ? "style='display:inline-block' ": ""}
-                     class='unittest-row-status'>
-                  <div class='$passFailClass'></div>
-                </div>
-                <div ${_isIE ? "style='display:inline-block' ": ""}>
-                    ${test_.currentGroup}</div>
-                &nbsp;
-                <div ${_isIE ? "style='display:inline-block' ": ""}>
-                    (${groupTestPassedCount}/${groupTotalTestCount})</div>
-              </div>
-            </div>"""));
-
-          // 'safeGroup' could be empty
-          var grp =
-              (safeGroup == '') ? null : te.querySelector('#${safeGroup}');
-          if (grp != null) {
-            grp.onClick.listen((_) {
-              var row = document.querySelector('.unittest-row-${safeGroup}');
-              if (row.attributes['class'].contains('unittest-row ')) {
-                document.querySelectorAll('.unittest-row-${safeGroup}').forEach(
-                    (e) => e.attributes['class'] = e.attributes['class']
-                        .replaceAll('unittest-row ', 'unittest-row-hidden '));
-              } else {
-                document.querySelectorAll('.unittest-row-${safeGroup}').forEach(
-                    (e) => e.attributes['class'] = e.attributes['class']
-                        .replaceAll('unittest-row-hidden', 'unittest-row'));
-              }
-            });
-          }
-        }
-
-        _buildRow(test_, te, safeGroup, !groupPassFail);
-      }
-
-      document.body.children.clear();
-      document.body.children.add(te);
-    }
-  }
-
-  void _buildRow(TestCase test_, Element te, String groupID, bool isVisible) {
-    var background = 'unittest-row-${test_.id % 2 == 0 ? "even" : "odd"}';
-    var display = '${isVisible ? "unittest-row" : "unittest-row-hidden"}';
-
-    addRowElement(id, status, description) {
-      te.children.add(new Element.html(''' <div>
-                <div class='$display unittest-row-${groupID} $background'>
-                  <div ${_isIE ? "style='display:inline-block' ": ""}
-                       class='unittest-row-id'>$id</div>
-                  <div ${_isIE ? "style='display:inline-block' ": ""}
-                       class="unittest-row-status unittest-${test_.result}">
-                       $status</div>
-                  <div ${_isIE ? "style='display:inline-block' ": ""}
-                       class='unittest-row-description'>$description</div>
-                </div>
-              </div>'''));
-    }
-
-    if (!test_.isComplete) {
-      addRowElement('${test_.id}', 'NO STATUS', 'Test did not complete.');
-      return;
-    }
-
-    addRowElement('${test_.id}', '${test_.result.toUpperCase()}',
-        '${test_.description}. ${HTML_ESCAPE.convert(test_.message)}');
-
-    if (test_.stackTrace != null) {
-      addRowElement('', '',
-          '<pre>${HTML_ESCAPE.convert(test_.stackTrace.toString())}</pre>');
-    }
-  }
-
-  static bool get _isIE => window.navigator.userAgent.contains('MSIE');
-
-  String get _htmlTestCSS => '''
-  body{
-    font-size: 14px;
-    font-family: 'Open Sans', 'Lucida Sans Unicode', 'Lucida Grande','''
-      ''' sans-serif;
-    background: WhiteSmoke;
-  }
-
-  .unittest-group
-  {
-    background: rgb(75,75,75);
-    width:98%;
-    color: WhiteSmoke;
-    font-weight: bold;
-    padding: 6px;
-    cursor: pointer;
-
-    /* Provide some visual separation between groups for IE */
-    ${_isIE ? "border-bottom:solid black 1px;": ""}
-    ${_isIE ? "border-top:solid #777777 1px;": ""}
-
-    background-image: -webkit-linear-gradient(bottom, rgb(50,50,50) 0%, '''
-      '''rgb(100,100,100) 100%);
-    background-image: -moz-linear-gradient(bottom, rgb(50,50,50) 0%, '''
-      '''rgb(100,100,100) 100%);
-    background-image: -ms-linear-gradient(bottom, rgb(50,50,50) 0%, '''
-      '''rgb(100,100,100) 100%);
-    background-image: linear-gradient(bottom, rgb(50,50,50) 0%, '''
-      '''rgb(100,100,100) 100%);
-
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-box;
-    display: box;
-
-    -webkit-box-orient: horizontal;
-    -moz-box-orient: horizontal;
-    -ms-box-orient: horizontal;
-    box-orient: horizontal;
-
-    -webkit-box-align: center;
-    -moz-box-align: center;
-    -ms-box-align: center;
-    box-align: center;
-   }
-
-  .unittest-group-status
-  {
-    width: 20px;
-    height: 20px;
-    border-radius: 20px;
-    margin-left: 10px;
-  }
-
-  .unittest-group-status-pass{
-    background: Green;
-    background: '''
-      '''-webkit-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%);
-    background: '''
-      '''-moz-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%);
-    background: '''
-      '''-ms-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%);
-    background: '''
-      '''radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%);
-  }
-
-  .unittest-group-status-fail{
-    background: Red;
-    background: '''
-      '''-webkit-radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%);
-    background: '''
-      '''-moz-radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%);
-    background: '''
-      '''-ms-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%);
-    background: radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%);
-  }
-
-  .unittest-overall{
-    font-size: 20px;
-  }
-
-  .unittest-summary{
-    font-size: 18px;
-  }
-
-  .unittest-pass{
-    color: Green;
-  }
-
-  .unittest-fail, .unittest-error
-  {
-    color: Red;
-  }
-
-  .unittest-row
-  {
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-box;
-    display: box;
-    -webkit-box-orient: horizontal;
-    -moz-box-orient: horizontal;
-    -ms-box-orient: horizontal;
-    box-orient: horizontal;
-    width: 100%;
-  }
-
-  .unittest-row-hidden
-  {
-    display: none;
-  }
-
-  .unittest-row-odd
-  {
-    background: WhiteSmoke;
-  }
-
-  .unittest-row-even
-  {
-    background: #E5E5E5;
-  }
-
-  .unittest-row-id
-  {
-    width: 3em;
-  }
-
-  .unittest-row-status
-  {
-    width: 4em;
-  }
-
-  .unittest-row-description
-  {
-  }
-
-  ''';
-}
+  HtmlEnhancedConfiguration(bool isLayoutTest);
 
-void useHtmlEnhancedConfiguration([bool isLayoutTest = false]) {
-  unittestConfiguration = isLayoutTest ? _singletonLayout : _singletonNotLayout;
+  void processMessage(e) {}
 }
 
-final _singletonLayout = new HtmlEnhancedConfiguration(true);
-final _singletonNotLayout = new HtmlEnhancedConfiguration(false);
+@deprecated
+void useHtmlEnhancedConfiguration([bool isLayoutTest]) {}
diff --git a/lib/html_individual_config.dart b/lib/html_individual_config.dart
index 263adf98..bc2a0403 100644
--- a/lib/html_individual_config.dart
+++ b/lib/html_individual_config.dart
@@ -2,51 +2,18 @@
 // 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.
 
-/// A unit test library for running groups of tests in a browser, instead of the
-/// entire test file. This is especially used for large tests files that have
-/// many subtests, so we can mark groups as failing at a finer granularity than
-/// the entire test file.
-///
-/// To use, import this file, and call [useHtmlIndividualConfiguration] at the
-/// start of your set sequence. Important constraint: your group descriptions
-/// MUST NOT contain spaces.
+@deprecated
 library unittest.html_individual_config;
 
-import 'dart:html';
-import 'unittest.dart' as unittest;
-import 'html_config.dart' as htmlconfig;
+import 'html_config.dart';
 
-class HtmlIndividualConfiguration extends htmlconfig.HtmlConfiguration {
+/// This is a stub class used to preserve compatibility with unittest 0.11.*.
+///
+/// It will be removed before the next version is released.
+@deprecated
+class HtmlIndividualConfiguration extends HtmlConfiguration {
   HtmlIndividualConfiguration(bool isLayoutTest) : super(isLayoutTest);
-
-  void onStart() {
-    var search = window.location.search;
-    if (search != '') {
-      var groups = search
-          .substring(1)
-          .split('&')
-          .where((p) => p.startsWith('group='))
-          .toList();
-
-      if (!groups.isEmpty) {
-        if (groups.length > 1) {
-          throw new ArgumentError('More than one "group" parameter provided.');
-        }
-
-        var testGroupName = groups.single.split('=')[1];
-        var startsWith = "$testGroupName${unittest.groupSep}";
-        unittest.filterTests(
-            (unittest.TestCase tc) => tc.description.startsWith(startsWith));
-      }
-    }
-    super.onStart();
-  }
-}
-
-void useHtmlIndividualConfiguration([bool isLayoutTest = false]) {
-  unittest.unittestConfiguration =
-      isLayoutTest ? _singletonLayout : _singletonNotLayout;
 }
 
-final _singletonLayout = new HtmlIndividualConfiguration(true);
-final _singletonNotLayout = new HtmlIndividualConfiguration(false);
+@deprecated
+void useHtmlIndividualConfiguration([bool isLayoutTest]) {}
diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart
index d3759667..27969bdc 100644
--- a/lib/src/configuration.dart
+++ b/lib/src/configuration.dart
@@ -4,67 +4,25 @@
 
 library unittest.configuration;
 
-import 'simple_configuration.dart';
 import 'test_case.dart';
 
-/// Describes the interface used by the unit test system for communicating the
-/// results of a test run.
-abstract class Configuration {
-  /// Creates an instance of [SimpleConfiguration].
-  factory Configuration() => new SimpleConfiguration();
-
-  /// Creates an [Configuration] instances that does nothing.
-  ///
-  /// For use by subclasses which wish to implement only a subset of features.
+/// This is a stub class used to preserve compatibility with unittest 0.11.*.
+///
+/// It will be removed before the next version is released.
+@deprecated
+class Configuration {
+  Configuration();
   Configuration.blank();
 
-  /// If `true`, tests are started automatically once they're finished being
-  /// defined.
-  ///
-  /// Otherwise, [runTests] must be called explicitly after tests are set up.
   final autoStart = true;
-
-  /// How long a [TestCase] can run before it is considered an error.
-  /// A [timeout] value of [:null:] means that the limit is infinite.
   Duration timeout = const Duration(minutes: 2);
-
-  /// Called as soon as the unittest framework becomes initialized.
-  ///
-  /// This is done even before tests are added to the test framework. It might
-  /// be used to determine/debug errors that occur before the test harness
-  /// starts executing. It is also used to tell the vm or browser that tests are
-  /// going to be run asynchronously and that the process should wait until they
-  /// are done.
   void onInit() {}
-
-  /// Called as soon as the unittest framework starts running.
   void onStart() {}
-
-  /// Called when each test starts. Useful to show intermediate progress on
-  /// a test suite.
   void onTestStart(TestCase testCase) {}
-
-  /// Called when each test is first completed. Useful to show intermediate
-  /// progress on a test suite.
   void onTestResult(TestCase testCase) {}
-
-  /// Called when an already completed test changes state. For example: a test
-  /// that was marked as passing may later be marked as being in error because
-  /// it still had callbacks being invoked.
   void onTestResultChanged(TestCase testCase) {}
-
-  /// Handles the logging of messages by a test case.
   void onLogMessage(TestCase testCase, String message) {}
-
-  /// Called when the unittest framework is done running. [success] indicates
-  /// whether all tests passed successfully.
   void onDone(bool success) {}
-
-  /// Called with the result of all test cases. Browser tests commonly override
-  /// this to reformat the output.
-  ///
-  /// When [uncaughtError] is not null, it contains an error that occured outside
-  /// of tests (e.g. setting up the test).
   void onSummary(int passed, int failed, int errors, List<TestCase> results,
       String uncaughtError) {}
 }
diff --git a/lib/src/expected_function.dart b/lib/src/expect_async.dart
similarity index 66%
rename from lib/src/expected_function.dart
rename to lib/src/expect_async.dart
index 7373c755..0179f851 100644
--- a/lib/src/expected_function.dart
+++ b/lib/src/expect_async.dart
@@ -2,11 +2,11 @@
 // 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.
 
-library unittest.expected_function;
+library unittest.expect_async;
 
-import '../unittest.dart';
-
-import 'internal_test_case.dart';
+import 'expect.dart';
+import 'invoker.dart';
+import 'state.dart';
 
 /// An object used to detect unpassed arguments.
 const _PLACEHOLDER = const Object();
@@ -30,7 +30,7 @@ typedef bool _IsDoneCallback();
 ///
 /// The wrapper function is accessible via [func]. It supports up to six
 /// optional and/or required positional arguments, but no named arguments.
-class ExpectedFunction {
+class _ExpectedFunction {
   /// The wrapped callback.
   final Function _callback;
 
@@ -65,8 +65,8 @@ class ExpectedFunction {
   /// The number of times the function has been called.
   int _actualCalls = 0;
 
-  /// The test case in which this function was wrapped.
-  final InternalTestCase _testCase;
+  /// The test invoker in which this function was wrapped.
+  final Invoker _invoker;
 
   /// Whether this function has been called the requisite number of times.
   bool _complete;
@@ -77,7 +77,7 @@ class ExpectedFunction {
   /// If passed, [id] is used as a descriptive name fo the function and [reason]
   /// as a reason it's expected to be called. If [isDone] is passed, the test
   /// won't be allowed to complete until it returns `true`.
-  ExpectedFunction(Function callback, int minExpected, int maxExpected,
+  _ExpectedFunction(Function callback, int minExpected, int maxExpected,
       {String id, String reason, bool isDone()})
       : this._callback = callback,
         _minExpectedCalls = minExpected,
@@ -86,16 +86,17 @@ class ExpectedFunction {
             : maxExpected,
         this._isDone = isDone,
         this._reason = reason == null ? '' : '\n$reason',
-        this._testCase = currentTestCase as InternalTestCase,
+        this._invoker = Invoker.current,
         this._id = _makeCallbackId(id, callback) {
-    ensureInitialized();
-    if (_testCase == null) {
-      throw new StateError("No valid test. Did you forget to run your test "
-          "inside a call to test()?");
+    if (_invoker == null) {
+      throw new StateError("[expectAsync] was called outside of a test.");
+    } else if (maxExpected > 0 && minExpected > maxExpected) {
+      throw new ArgumentError("max ($maxExpected) may not be less than count "
+          "($minExpected).");
     }
 
     if (isDone != null || minExpected > 0) {
-      _testCase.callbackFunctionsOutstanding++;
+      _invoker.addOutstandingCallback();
       _complete = false;
     } else {
       _complete = true;
@@ -133,6 +134,7 @@ class ExpectedFunction {
     if (_callback is _Func1) return _max1;
     if (_callback is _Func0) return _max0;
 
+    _invoker.removeOutstandingCallback();
     throw new ArgumentError(
         'The wrapped function has more than 6 required arguments');
   }
@@ -159,22 +161,16 @@ class ExpectedFunction {
       _run([a0, a1, a2, a3, a4, a5].where((a) => a != _PLACEHOLDER));
 
   /// Runs the wrapped function with [args] and returns its return value.
-  ///
-  /// This will pass any errors on to [_testCase] and return `null`.
   _run(Iterable args) {
+    // Note that in the old unittest, this returned `null` if it encountered an
+    // error, where now it just re-throws that error because Zone machinery will
+    // pass it to the invoker anyway.
     try {
       _actualCalls++;
-      if (_testCase.isComplete) {
-        // Don't run the callback if the test is done. We don't throw here as
-        // this is not the current test, but we do mark the old test as having
-        // an error if it previously passed.
-        if (_testCase.result == PASS) {
-          _testCase.error(
-              'Callback ${_id}called ($_actualCalls) after test case '
-              '${_testCase.description} had already been marked as '
-              '${_testCase.result}.$_reason');
-        }
-        return null;
+      if (_invoker.liveTest.isComplete &&
+          _invoker.liveTest.state.result == Result.success) {
+        throw 'Callback ${_id}called ($_actualCalls) after test case '
+              '${_invoker.liveTest.test.name} had already completed.$_reason';
       } else if (_maxExpectedCalls >= 0 && _actualCalls > _maxExpectedCalls) {
         throw new TestFailure('Callback ${_id}called more times than expected '
                               '($_maxExpectedCalls).$_reason');
@@ -182,7 +178,7 @@ class ExpectedFunction {
 
       return Function.apply(_callback, args.toList());
     } catch (error, stackTrace) {
-      _testCase.registerException(error, stackTrace);
+      _invoker.handleError(error, stackTrace);
       return null;
     } finally {
       _afterRun();
@@ -198,6 +194,41 @@ class ExpectedFunction {
     // Mark this callback as complete and remove it from the test case's
     // oustanding callback count; if that hits zero the test is done.
     _complete = true;
-    _testCase.markCallbackComplete();
+    _invoker.removeOutstandingCallback();
   }
 }
+
+/// Indicate that [callback] is expected to be called [count] number of times
+/// (by default 1).
+///
+/// The unittest framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete. [callback] may take up
+/// to six optional or required positional arguments; named arguments are not
+/// supported.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+Function expectAsync(Function callback,
+        {int count: 1, int max: 0, String id, String reason}) =>
+    new _ExpectedFunction(callback, count, max, id: id, reason: reason).func;
+
+/// Indicate that [callback] is expected to be called until [isDone] returns
+/// true.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete. [callback] may take up to six
+/// optional or required positional arguments; named arguments are not
+/// supported.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+Function expectAsyncUntil(Function callback, bool isDone(),
+    {String id, String reason}) => new _ExpectedFunction(callback, 0, -1,
+        id: id, reason: reason, isDone: isDone).func;
diff --git a/lib/src/future_matchers.dart b/lib/src/future_matchers.dart
index 5d873383..2ae13eeb 100644
--- a/lib/src/future_matchers.dart
+++ b/lib/src/future_matchers.dart
@@ -8,7 +8,9 @@ import 'dart:async';
 
 import 'package:matcher/matcher.dart' hide throws, throwsA, expect, fail;
 
-import '../unittest.dart';
+import 'expect.dart';
+import 'invoker.dart';
+import 'utils.dart';
 
 /// Matches a [Future] that completes successfully with a value.
 ///
@@ -43,22 +45,26 @@ class _Completes extends Matcher {
 
   bool matches(item, Map matchState) {
     if (item is! Future) return false;
-    var done = expectAsync((fn) => fn(), id: _id);
+    Invoker.current.addOutstandingCallback();
 
     item.then((value) {
-      done(() {
-        if (_matcher != null) expect(value, _matcher);
-      });
+      if (_matcher != null) expect(value, _matcher);
+      Invoker.current.removeOutstandingCallback();
     }, onError: (error, trace) {
+      if (error is TestFailure) {
+        Invoker.current.handleError(error, trace);
+        return;
+      }
+
       var id = _id == '' ? '' : '${_id} ';
       var reason = 'Expected future ${id}to complete successfully, '
           'but it failed with ${error}';
       if (trace != null) {
-        var stackTrace = trace.toString();
+        var stackTrace = terseChain(trace).toString();
         stackTrace = '  ${stackTrace.replaceAll('\n', '\n  ')}';
         reason = '$reason\nStack trace:\n$stackTrace';
       }
-      done(() => fail(reason));
+      fail(reason);
     });
 
     return true;
diff --git a/lib/src/group_context.dart b/lib/src/group_context.dart
deleted file mode 100644
index 78f347b4..00000000
--- a/lib/src/group_context.dart
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (c) 2015, 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.
-
-library unittest.group_context;
-
-import 'dart:async';
-
-import '../unittest.dart';
-
-/// Setup and teardown functions for a group and its parents, the latter
-/// for chaining.
-class GroupContext {
-  /// The parent context, or `null`.
-  final GroupContext parent;
-
-  /// Whether this is the root context.
-  bool get isRoot => parent == null;
-
-  /// Description text of the current test group.
-  final String _name;
-
-  /// The set-up function called before each test in a group.
-  Function get testSetUp => _testSetUp;
-  Function _testSetUp;
-
-  set testSetUp(Function setUp) {
-    if (parent == null || parent.testSetUp == null) {
-      _testSetUp = setUp;
-      return;
-    }
-
-    _testSetUp = () {
-      var f = parent.testSetUp();
-      if (f is Future) {
-        return f.then((_) => setUp());
-      } else {
-        return setUp();
-      }
-    };
-  }
-
-  /// The tear-down function called after each test in a group.
-  Function get testTearDown => _testTearDown;
-  Function _testTearDown;
-
-  set testTearDown(Function tearDown) {
-    if (parent == null || parent.testTearDown == null) {
-      _testTearDown = tearDown;
-      return;
-    }
-
-    _testTearDown = () {
-      var f = tearDown();
-      if (f is Future) {
-        return f.then((_) => parent.testTearDown());
-      } else {
-        return parent.testTearDown();
-      }
-    };
-  }
-
-  /// Returns the fully-qualified name of this context.
-  String get fullName =>
-      (isRoot || parent.isRoot) ? _name : "${parent.fullName}$groupSep$_name";
-
-  GroupContext.root()
-      : parent = null,
-        _name = '';
-
-  GroupContext(this.parent, this._name) {
-    _testSetUp = parent.testSetUp;
-    _testTearDown = parent.testTearDown;
-  }
-}
diff --git a/lib/src/internal_test_case.dart b/lib/src/internal_test_case.dart
deleted file mode 100644
index 97636661..00000000
--- a/lib/src/internal_test_case.dart
+++ /dev/null
@@ -1,227 +0,0 @@
-// Copyright (c) 2014, 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.
-
-library unittest.internal_test_case;
-
-import 'dart:async';
-
-import '../unittest.dart';
-import 'test_environment.dart';
-import 'utils.dart';
-
-/// An implementation of [TestCase] that exposes internal properties for other
-/// unittest use.
-class InternalTestCase implements TestCase {
-  final int id;
-  final String description;
-
-  /// The setup function to call before the test, if any.
-  Function _setUp;
-
-  /// The teardown function to call after the test, if any.
-  Function _tearDown;
-
-  /// The body of the test case.
-  TestFunction _testFunction;
-
-  /// Remaining number of callback functions that must reach a 'done' state
-  /// before the test completes.
-  int callbackFunctionsOutstanding = 0;
-
-  /// The error or failure message for the tests.
-  ///
-  /// Initially an empty string.
-  String message = '';
-
-  /// The result of the test case.
-  ///
-  /// If the test case has is completed, this will be one of [PASS], [FAIL], or
-  /// [ERROR]. Otherwise, it will be `null`.
-  String result;
-
-  /// Returns whether this test case passed.
-  bool get passed => result == PASS;
-
-  /// The stack trace for the error that caused this test case to fail, or
-  /// `null` if it succeeded.
-  StackTrace stackTrace;
-
-  /// The name of the group within which this test is running.
-  final String currentGroup;
-
-  /// The time the test case started running.
-  ///
-  /// `null` if the test hasn't yet begun running.
-  DateTime get startTime => _startTime;
-  DateTime _startTime;
-
-  /// The amount of time the test case took.
-  ///
-  /// `null` if the test hasn't finished running.
-  Duration get runningTime => _runningTime;
-  Duration _runningTime;
-
-  /// Whether this test is enabled.
-  ///
-  /// Disabled tests won't be run.
-  bool enabled = true;
-
-  /// A completer that will complete when the test is finished.
-  ///
-  /// This is only non-`null` when outstanding callbacks exist.
-  Completer _testComplete;
-
-  /// Whether this test case has finished running.
-  bool get isComplete => !enabled || result != null;
-
-  InternalTestCase(this.id, this.description, this._testFunction)
-      : currentGroup = environment.currentContext.fullName,
-        _setUp = environment.currentContext.testSetUp,
-        _tearDown = environment.currentContext.testTearDown;
-
-  /// A function that returns another function to handle errors from [Future]s.
-  ///
-  /// [stage] is a string description of the stage of testing that failed.
-  Function _errorHandler(String stage) => (e, stack) {
-    if (stack == null && e is Error) {
-      stack = e.stackTrace;
-    }
-    if (result == null || result == PASS) {
-      if (e is TestFailure) {
-        fail("$e", stack);
-      } else {
-        error("$stage failed: Caught $e", stack);
-      }
-    }
-  };
-
-  /// Performs any associated [_setUp] function and runs the test.
-  ///
-  /// Returns a [Future] that can be used to schedule the next test. If the test
-  /// runs to completion synchronously, or is disabled, null is returned, to
-  /// tell unittest to schedule the next test immediately.
-  Future run() {
-    if (!enabled) return new Future.value();
-
-    result = stackTrace = null;
-    message = '';
-
-    // Avoid calling [new Future] to avoid issue 11911.
-    return new Future.value().then((_) {
-      if (_setUp != null) return _setUp();
-    }).catchError(_errorHandler('Setup')).then((_) {
-      // Skip the test if setup failed.
-      if (result != null) return new Future.value();
-      config.onTestStart(this);
-      _startTime = new DateTime.now();
-      _runningTime = null;
-      callbackFunctionsOutstanding++;
-      var testReturn = _testFunction();
-      // If _testFunction() returned a future, we want to wait for it like we
-      // would a callback, so if a failure occurs while waiting, we can abort.
-      if (testReturn is Future) {
-        callbackFunctionsOutstanding++;
-        testReturn
-            .catchError(_errorHandler('Test'))
-            .whenComplete(markCallbackComplete);
-      }
-    }).catchError(_errorHandler('Test')).then((_) {
-      markCallbackComplete();
-      if (result == null) {
-        // Outstanding callbacks exist; we need to return a Future.
-        _testComplete = new Completer();
-        return _testComplete.future.whenComplete(() {
-          if (_tearDown != null) {
-            return _tearDown();
-          }
-        }).catchError(_errorHandler('Teardown'));
-      } else if (_tearDown != null) {
-        return _tearDown();
-      }
-    }).catchError(_errorHandler('Teardown')).whenComplete(() {
-      _setUp = null;
-      _tearDown = null;
-      _testFunction = null;
-    });
-  }
-
-  /// Marks the test as having completed with [testResult], which should be one
-  /// of [PASS], [FAIL], or [ERROR].
-  void _complete(String testResult,
-      [String messageText = '', StackTrace stack]) {
-    if (runningTime == null) {
-      // The startTime can be `null` if an error happened during setup. In this
-      // case we simply report a running time of 0.
-      if (startTime != null) {
-        _runningTime = new DateTime.now().difference(startTime);
-      } else {
-        _runningTime = const Duration(seconds: 0);
-      }
-    }
-    _setResult(testResult, messageText, stack);
-    if (_testComplete != null) {
-      var t = _testComplete;
-      _testComplete = null;
-      t.complete(this);
-    }
-  }
-
-  // Sets [this]'s fields to reflect the test result, and notifies the current
-  // configuration that the test has completed.
-  //
-  // Returns true if this is the first time the result has been set.
-  void _setResult(String testResult, String messageText, StackTrace stack) {
-    message = messageText;
-    stackTrace = getTrace(stack, formatStacks, filterStacks);
-    if (stackTrace == null) stackTrace = stack;
-    if (result == null) {
-      result = testResult;
-      config.onTestResult(this);
-    } else {
-      result = testResult;
-      config.onTestResultChanged(this);
-    }
-  }
-
-  /// Marks the test as having passed.
-  void pass() {
-    _complete(PASS);
-  }
-
-  void registerException(error, [StackTrace stackTrace]) {
-    var message = error is TestFailure ? error.message : 'Caught $error';
-    if (result == null) {
-      fail(message, stackTrace);
-    } else {
-      this.error(message, stackTrace);
-    }
-  }
-
-  /// Marks the test as having failed.
-  void fail(String messageText, [StackTrace stack]) {
-    if (result != null) {
-      var newMessage = result == PASS
-          ? 'Test failed after initially passing: $messageText'
-          : 'Test failed more than once: $messageText';
-      // TODO(gram): Should we combine the stack with the old one?
-      _complete(ERROR, newMessage, stack);
-    } else {
-      _complete(FAIL, messageText, stack);
-    }
-  }
-
-  /// Marks the test as having had an unexpected error.
-  void error(String messageText, [StackTrace stack]) {
-    _complete(ERROR, messageText, stack);
-  }
-
-  /// Indicates that an asynchronous callback has completed, and marks the test
-  /// as passing if all outstanding callbacks are complete.
-  void markCallbackComplete() {
-    callbackFunctionsOutstanding--;
-    if (callbackFunctionsOutstanding == 0 && !isComplete) pass();
-  }
-
-  String toString() => result != null ? "$description: $result" : description;
-}
diff --git a/lib/src/prints_matcher.dart b/lib/src/prints_matcher.dart
index 92efdcac..4a7c0179 100644
--- a/lib/src/prints_matcher.dart
+++ b/lib/src/prints_matcher.dart
@@ -8,7 +8,8 @@ import 'dart:async';
 
 import 'package:matcher/matcher.dart' hide completes, expect;
 
-import '../unittest.dart';
+import 'future_matchers.dart';
+import 'expect.dart';
 
 /// Matches a [Function] that prints text that matches [matcher].
 ///
@@ -41,7 +42,16 @@ class _Prints extends Matcher {
     }
 
     return completes.matches(result.then((_) {
-      expect(buffer.toString(), _matcher);
+      // Re-run expect() so we get the same formatting as we would without being
+      // asynchronous.
+      expect(() {
+        var actual = buffer.toString();
+        if (actual.isEmpty) return;
+
+        // Strip off the final newline because [print] will re-add it.
+        actual = actual.substring(0, actual.length - 1);
+        print(actual);
+      }, this);
     }), matchState);
   }
 
diff --git a/lib/src/simple_configuration.dart b/lib/src/simple_configuration.dart
index c95550ce..470b10f9 100644
--- a/lib/src/simple_configuration.dart
+++ b/lib/src/simple_configuration.dart
@@ -4,119 +4,16 @@
 
 library unittest.simple_configuration;
 
-import 'dart:isolate';
-
-import '../unittest.dart';
+import 'test_case.dart';
 import 'configuration.dart';
-import 'utils.dart';
 
-/// A configuration that provides hooks to configure the unittest library for
-/// different platforms.
+/// This is a stub class used to preserve compatibility with unittest 0.11.*.
 ///
-/// This class implements the [Configuration] API in a platform-independent way.
-/// Tests that want to take advantage of the platform can create a subclass and
-/// override methods from this class.
+/// It will be removed before the next version is released.
+@deprecated
 class SimpleConfiguration extends Configuration {
-  /// A port that keeps the VM alive while we wait for asynchronous tests to
-  /// finish.
-  ///
-  /// The VM won't shut down as long as there's an open receive port.
-  ReceivePort _receivePort;
-
-  /// If true (the default), throw an exception at the end if any tests failed.
   bool throwOnTestFailures = true;
-
-  /// The constructor sets up a failure handler for [expect] that redirects
-  /// [expect] failures to [onExpectFailure].
   SimpleConfiguration() : super.blank();
 
-  void onInit() {
-    // For Dart internal tests, we don't want stack frame filtering.
-    // We turn it off here in the default config, but by default turn
-    // it back on in the vm and html configs.
-    filterStacks = false;
-    _receivePort = new ReceivePort();
-    _postMessage('unittest-suite-wait-for-done');
-  }
-
-  /// Called when each test starts. Useful to show intermediate progress on
-  /// a test suite. Derived classes should call this first before their own
-  /// override code.
-  void onTestStart(TestCase testCase) {}
-
-  /// Handles the logging of messages by a test case.
-  ///
-  /// The default in this base configuration is to call [print].
-  void onLogMessage(TestCase testCase, String message) {
-    print(message);
-  }
-
-  /// Format a test result.
-  String formatResult(TestCase testCase) {
-    var result = new StringBuffer();
-    result.write(testCase.result.toUpperCase());
-    result.write(": ");
-    result.write(testCase.description);
-    result.write("\n");
-
-    if (testCase.message != '') {
-      result.write(indent(testCase.message));
-      result.write("\n");
-    }
-
-    if (testCase.stackTrace != null) {
-      result.write(indent(testCase.stackTrace.toString()));
-      result.write("\n");
-    }
-    return result.toString();
-  }
-
-  /// Called with the result of all test cases.
-  ///
-  /// The default implementation prints the result summary using [print],
-  /// formatted with [formatResult]. Browser tests commonly override this to
-  /// reformat the output.
-  ///
-  /// When [uncaughtError] is not null, it contains an error that occured
-  /// outside of tests (e.g. setting up the test).
-  void onSummary(int passed, int failed, int errors, List<TestCase> results,
-      String uncaughtError) {
-    // Print each test's result.
-    for (var test in results) {
-      print(formatResult(test).trim());
-    }
-
-    // Show the summary.
-    print('');
-
-    if (passed == 0 && failed == 0 && errors == 0 && uncaughtError == null) {
-      print('No tests found.');
-      // This is considered a failure too.
-    } else if (failed == 0 && errors == 0 && uncaughtError == null) {
-      print('All $passed tests passed.');
-    } else {
-      if (uncaughtError != null) {
-        print('Top-level uncaught error: $uncaughtError');
-      }
-      print('$passed PASSED, $failed FAILED, $errors ERRORS');
-    }
-  }
-
-  void onDone(bool success) {
-    if (success) {
-      _postMessage('unittest-suite-success');
-      _receivePort.close();
-    } else {
-      _receivePort.close();
-      if (throwOnTestFailures) {
-        throw new Exception('Some tests failed.');
-      }
-    }
-  }
-
-  void _postMessage(String message) {
-    // In dart2js browser tests, the JavaScript-based test controller
-    // intercepts calls to print and listens for "secret" messages.
-    print(message);
-  }
+  String formatResult(TestCase testCase) => "";
 }
diff --git a/lib/src/test_case.dart b/lib/src/test_case.dart
index 37596ff5..5a99d1ea 100644
--- a/lib/src/test_case.dart
+++ b/lib/src/test_case.dart
@@ -4,52 +4,20 @@
 
 library unittest.test_case;
 
-import '../unittest.dart';
-
-/// An individual unit test.
+/// This is a stub class used to preserve compatibility with unittest 0.11.*.
+///
+/// It will be removed before the next version is released.
+@deprecated
 abstract class TestCase {
-  /// A unique numeric identifier for this test case.
   int get id;
-
-  /// A description of what the test is specifying.
   String get description;
-
-  /// The error or failure message for the tests.
-  ///
-  /// Initially an empty string.
   String get message;
-
-  /// The result of the test case.
-  ///
-  /// If the test case has is completed, this will be one of [PASS], [FAIL], or
-  /// [ERROR]. Otherwise, it will be `null`.
   String get result;
-
-  /// Returns whether this test case passed.
   bool get passed;
-
-  /// The stack trace for the error that caused this test case to fail, or
-  /// `null` if it succeeded.
   StackTrace get stackTrace;
-
-  /// The name of the group within which this test is running.
   String get currentGroup;
-
-  /// The time the test case started running.
-  ///
-  /// `null` if the test hasn't yet begun running.
   DateTime get startTime;
-
-  /// The amount of time the test case took.
-  ///
-  /// `null` if the test hasn't finished running.
   Duration get runningTime;
-
-  /// Whether this test is enabled.
-  ///
-  /// Disabled tests won't be run.
   bool get enabled;
-
-  /// Whether this test case has finished running.
-  bool get isComplete => !enabled || result != null;
+  final isComplete = false;
 }
diff --git a/lib/src/test_environment.dart b/lib/src/test_environment.dart
deleted file mode 100644
index 6c205a9f..00000000
--- a/lib/src/test_environment.dart
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (c) 2014, 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.
-
-library unittest.test_environment;
-
-import 'dart:async';
-
-import 'configuration.dart';
-import 'group_context.dart';
-import 'internal_test_case.dart';
-
-/// The default unittest environment.
-final _defaultEnvironment = new TestEnvironment();
-
-/// The current unittest environment.
-TestEnvironment get environment {
-  var environment = Zone.current[#unittest.environment];
-  return environment == null ? _defaultEnvironment : environment;
-}
-
-// The current environment's configuration.
-Configuration get config => environment.config;
-
-/// Encapsulates the state of the test environment.
-///
-/// This is used by the [withTestEnvironment] method to support multiple
-/// invocations of the unittest library within the same application
-/// instance.
-class TestEnvironment {
-  /// The environment's configuration.
-  Configuration config;
-
-  /// The top-level group context.
-  ///
-  /// We use a 'dummy' context for the top level to eliminate null checks when
-  /// querying the context. This allows us to easily support top-level
-  /// [setUp]/[tearDown] functions as well.
-  final rootContext = new GroupContext.root();
-
-  /// The current group context.
-  GroupContext currentContext;
-
-  /// The [currentTestCaseIndex] represents the index of the currently running
-  /// test case.
-  ///
-  /// If this is -1 it implies the test system is not running.
-  /// It will be set to [number of test cases] as a short-lived state flagging
-  /// that the last test has completed.
-  int currentTestCaseIndex = -1;
-
-  /// The [initialized] variable specifies whether the framework
-  /// has been initialized.
-  bool initialized = false;
-
-  /// The time since we last gave asynchronous code a chance to be scheduled.
-  int lastBreath = new DateTime.now().millisecondsSinceEpoch;
-
-  /// The number of [solo_group]s deep we are currently.
-  int soloNestingLevel = 0;
-
-  /// Whether we've seen a [solo_test].
-  bool soloTestSeen = false;
-
-  /// The list of test cases to run.
-  final testCases = new List<InternalTestCase>();
-
-  /// The error message that is printed in the test summary.
-  String uncaughtErrorMessage;
-
-  TestEnvironment() {
-    currentContext = rootContext;
-  }
-}
diff --git a/lib/src/throws_matcher.dart b/lib/src/throws_matcher.dart
index 9790d22a..6807e6d2 100644
--- a/lib/src/throws_matcher.dart
+++ b/lib/src/throws_matcher.dart
@@ -8,7 +8,9 @@ import 'dart:async';
 
 import 'package:matcher/matcher.dart' hide fail, expect;
 
-import '../unittest.dart';
+import 'expect.dart';
+import 'invoker.dart';
+import 'utils.dart';
 
 /// This can be used to match two kinds of objects:
 ///
@@ -48,26 +50,24 @@ class Throws extends Matcher {
   bool matches(item, Map matchState) {
     if (item is! Function && item is! Future) return false;
     if (item is Future) {
-      var done = expectAsync((fn) => fn());
-
+      Invoker.current.addOutstandingCallback();
       // Queue up an asynchronous expectation that validates when the future
       // completes.
       item.then((value) {
-        done(() {
-          fail("Expected future to fail, but succeeded with '$value'.");
-        });
+        fail("Expected future to fail, but succeeded with '$value'.");
       }, onError: (error, trace) {
-        done(() {
-          if (_matcher == null) return;
-          var reason;
-          if (trace != null) {
-            var stackTrace = trace.toString();
-            stackTrace = "  ${stackTrace.replaceAll("\n", "\n  ")}";
-            reason = "Actual exception trace:\n$stackTrace";
-          }
-          expect(error, _matcher, reason: reason);
-        });
-      });
+        if (_matcher == null) return;
+
+        var reason;
+        if (trace != null) {
+          var stackTrace = terseChain(trace).toString();
+          stackTrace = "  ${stackTrace.replaceAll("\n", "\n  ")}";
+          reason = "Actual exception trace:\n$stackTrace";
+        }
+
+        // Re-run [expect] to get the proper formatting.
+        expect(() => throw error, this, reason: reason);
+      }).then((_) => Invoker.current.removeOutstandingCallback());
       // It hasn't failed yet.
       return true;
     }
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 5c12593e..6b0939fe 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -28,23 +28,6 @@ String getErrorMessage(error) =>
 String indent(String str) =>
     str.replaceAll(new RegExp("^", multiLine: true), "  ");
 
-/// A pair of values.
-class Pair<E, F> {
-  final E first;
-  final F last;
-
-  Pair(this.first, this.last);
-
-  String toString() => '($first, $last)';
-
-  bool operator ==(other) {
-    if (other is! Pair) return false;
-    return other.first == first && other.last == last;
-  }
-
-  int get hashCode => first.hashCode ^ last.hashCode;
-}
-
 /// A regular expression matching the path to a temporary file used to start an
 /// isolate.
 ///
@@ -64,29 +47,6 @@ Chain terseChain(StackTrace stackTrace) {
   }, terse: true);
 }
 
-/// Returns a Trace object from a StackTrace object or a String, or the
-/// unchanged input if formatStacks is false;
-Trace getTrace(stack, bool formatStacks, bool filterStacks) {
-  Trace trace;
-  if (stack == null || !formatStacks) return null;
-  if (stack is String) {
-    trace = new Trace.parse(stack);
-  } else if (stack is StackTrace) {
-    trace = new Trace.from(stack);
-  } else {
-    throw new Exception('Invalid stack type ${stack.runtimeType} for $stack.');
-  }
-
-  if (!filterStacks) return trace;
-
-  // Format the stack trace by removing everything above TestCase._runTest,
-  // which is usually going to be irrelevant. Also fold together unittest and
-  // core library calls so only the function the user called is visible.
-  return new Trace(trace.frames.takeWhile((frame) {
-    return frame.package != 'unittest' || frame.member != 'TestCase._runTest';
-  })).terse.foldFrames((frame) => frame.package == 'unittest' || frame.isCore);
-}
-
 /// Flattens nested [Iterable]s inside an [Iterable] into a single [List]
 /// containing only non-[Iterable] elements.
 List flatten(Iterable nested) {
diff --git a/lib/test_controller.js b/lib/test_controller.js
index 8a62d6ae..11822790 100644
--- a/lib/test_controller.js
+++ b/lib/test_controller.js
@@ -2,232 +2,4 @@
 // 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.
 
-/**
- * Test controller logic - used by unit test harness to embed tests in
- * conent shell.
- */
-
-// Clear the console before every test run - this is Firebug specific code.
-if (typeof console == "object" && typeof console.clear == "function") {
-  console.clear();
-}
-
-// Some tests may expect and have no way to suppress global errors.
-var testExpectsGlobalError = false;
-var testSuppressedGlobalErrors = [];
-
-// Set window onerror to make sure that we catch test harness errors across all
-// browsers.
-window.onerror = function (message, url, lineNumber) {
-  if (testExpectsGlobalError) {
-    testSuppressedGlobalErrors.push({
-      message: message
-    });
-    return;
-  }
-  if (url) {
-    showErrorAndExit(
-        "\n\n" + url + ":" + lineNumber + ":\n" + message + "\n\n");
-  } else {
-    showErrorAndExit(message);
-  }
-  window.postMessage('unittest-suite-external-error', '*');
-};
-
-// Start Dartium/content_shell, unless we are waiting for HTML Imports to load.
-// HTML Imports allows a document to link to other HTMLs documents via
-// <link rel=import>. It also allows for those other documents to contain
-// <script> tags, which must be run before scripts on the main page.
-// We have package:web_components to polyfill this feature, and it will handle
-// starting Dartium/content_shell in that case. HTML Imports is used by Polymer,
-// but it could be used by itself too. See the specification:
-// https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/imports/index.html
-if (navigator.webkitStartDart && !window.HTMLImports) {
-  navigator.webkitStartDart();
-}
-
-// testRunner is provided by content shell.
-// It is not available in browser tests.
-var testRunner = window.testRunner || window.layoutTestController;
-
-var waitForDone = false;
-
-// Returns the driving window object if available
-function getDriverWindow() {
-  if (window != window.parent) {
-    // We're running in an iframe.
-    return window.parent;
-  } else if (window.opener) {
-    // We were opened by another window.
-    return window.opener;
-  }
-  return null;
-}
-
-function notifyStart() {
-  var driver = getDriverWindow();
-  if (driver) {
-    driver.postMessage("STARTING", "*");
-  }
-}
-// We call notifyStart here to notify the encapsulating browser.
-notifyStart();
-
-function notifyDone() {
-  if (testRunner) testRunner.notifyDone();
-
-  // TODO(ricow): REMOVE, debug info, see issue 13292
-  if (!testRunner) {
-    printMessage('Calling notifyDone()');
-  }
-  // To support in browser launching of tests we post back start and result
-  // messages to the window.opener.
-  var driver = getDriverWindow();
-  if (driver) {
-    driver.postMessage(window.document.body.innerHTML, "*");
-  }
-}
-
-function processMessage(msg) {
-  if (typeof msg != 'string') return;
-  // TODO(ricow): REMOVE, debug info, see issue 13292
-  if (!testRunner) {
-    // Filter out ShadowDOM polyfill messages which are random floats.
-    if (msg != parseFloat(msg)) {
-      printMessage('processMessage(): ' + msg);
-    }
-  }
-  if (msg == 'unittest-suite-done') {
-    notifyDone();
-  } else if (msg == 'unittest-suite-wait-for-done') {
-    waitForDone = true;
-    if (testRunner) {
-      testRunner.startedDartTest = true;
-    }
-  } else if (msg == 'dart-calling-main') {
-    if (testRunner) {
-      testRunner.startedDartTest = true;
-    }
-  } else if (msg == 'dart-main-done') {
-    if (!waitForDone) {
-      printMessage('PASS');
-      notifyDone();
-    }
-  } else if (msg == 'unittest-suite-success') {
-    printMessage('PASS');
-    notifyDone();
-  } else if (msg == 'unittest-suite-fail') {
-    showErrorAndExit('Some tests failed.');
-  }
-}
-
-function onReceive(e) {
-  processMessage(e.data);
-}
-
-if (testRunner) {
-  testRunner.dumpAsText();
-  testRunner.waitUntilDone();
-}
-window.addEventListener("message", onReceive, false);
-
-function showErrorAndExit(message) {
-  if (message) {
-    printMessage('Error: ' + String(message));
-  }
-  // dart/tools/testing/test_runner.dart is looking for either PASS or
-  // FAIL in a browser test's output.
-  printMessage('FAIL');
-  notifyDone();
-}
-
-function onLoad(e) {
-  // needed for dartium compilation errors.
-  if (window.compilationError) {
-    showErrorAndExit(window.compilationError);
-  }
-}
-
-window.addEventListener("DOMContentLoaded", onLoad, false);
-
-// Note: before renaming this function, note that it is also included in an
-// inlined error handler in the HTML files that wrap DRT tests.
-// See: tools/testing/dart/browser_test.dart
-function externalError(e) {
-  // needed for dartium compilation errors.
-  showErrorAndExit(e && e.message);
-  window.postMessage('unittest-suite-external-error', '*');
-}
-
-document.addEventListener('readystatechange', function () {
-  if (document.readyState != "loaded") return;
-  // If 'startedDartTest' is not set, that means that the test did not have
-  // a chance to load. This will happen when a load error occurs in the VM.
-  // Give the machine time to start up.
-  setTimeout(function() {
-    // A window.postMessage might have been enqueued after this timeout.
-    // Just sleep another time to give the browser the time to process the
-    // posted message.
-    setTimeout(function() {
-      if (testRunner && !testRunner.startedDartTest) {
-        notifyDone();
-      }
-    }, 0);
-  }, 50);
-});
-
-// dart2js will generate code to call this function to handle the Dart
-// [print] method.
-//
-// dartium will invoke this method for [print] calls if the environment variable
-// "DART_FORWARDING_PRINT" was set when launching dartium.
-//
-// Our tests will be wrapped, so we can detect when [main] is called and when
-// it has ended.
-// The wrapping happens either via "dartMainRunner" (for dart2js) or wrapped
-// tests for dartium.
-//
-// The following messages are handled specially:
-//   dart-calling-main:  signals that the dart [main] function will be invoked
-//   dart-main-done:  signals that the dart [main] function has finished
-//   unittest-suite-wait-for-done:  signals the start of an asynchronous test
-//   unittest-suite-success:  signals the end of an asynchrounous test
-//
-// These messages are used to communicate with the test and will be posted so
-// [processMessage] above can see it.
-function dartPrint(msg) {
-  if ((msg === 'unittest-suite-success')
-      || (msg === 'unittest-suite-done')
-      || (msg === 'unittest-suite-wait-for-done')
-      || (msg === 'dart-calling-main')
-      || (msg === 'dart-main-done')) {
-    window.postMessage(msg, '*');
-    return;
-  }
-  printMessage(msg);
-}
-
-// Prints 'msg' to the console (if available) and to the body of the html
-// document.
-function printMessage(msg) {
-  if (typeof console === 'object') console.warn(msg);
-  var pre = document.createElement('pre');
-  pre.appendChild(document.createTextNode(String(msg)));
-  document.body.appendChild(pre);
-  document.body.appendChild(document.createTextNode('\n'));
-}
-
-// dart2js will generate code to call this function instead of calling
-// Dart [main] directly. The argument is a closure that invokes main.
-function dartMainRunner(main) {
-  dartPrint('dart-calling-main');
-  try {
-    main();
-  } catch (e) {
-    dartPrint(e);
-    if (e.stack) dartPrint(e.stack);
-    window.postMessage('unittest-suite-fail', '*');
-    return;
-  }
-  dartPrint('dart-main-done');
-}
+/** This file is deprecated and will be removed before the next release. */
diff --git a/lib/unittest.dart b/lib/unittest.dart
index d0083270..0e4be92d 100644
--- a/lib/unittest.dart
+++ b/lib/unittest.dart
@@ -5,14 +5,15 @@
 library unittest;
 
 import 'dart:async';
-import 'dart:collection';
+
+import 'package:path/path.dart' as p;
 
 import 'src/configuration.dart';
-import 'src/expected_function.dart';
-import 'src/group_context.dart';
-import 'src/internal_test_case.dart';
+import 'src/declarer.dart';
+import 'src/console_reporter.dart';
+import 'src/invoker.dart';
+import 'src/suite.dart';
 import 'src/test_case.dart';
-import 'src/test_environment.dart';
 
 export 'package:matcher/matcher.dart'
     hide
@@ -44,426 +45,133 @@ export 'package:matcher/matcher.dart'
 
 export 'src/configuration.dart';
 export 'src/expect.dart';
-export 'src/simple_configuration.dart';
+export 'src/expect_async.dart';
 export 'src/future_matchers.dart';
 export 'src/prints_matcher.dart';
+export 'src/simple_configuration.dart';
+export 'src/test_case.dart';
 export 'src/throws_matcher.dart';
 export 'src/throws_matchers.dart';
-export 'src/test_case.dart';
 
-/// The signature for a function passed to [test].
-typedef dynamic TestFunction();
+/// The global declarer.
+///
+/// This is used if a test file is run directly, rather than through the runner.
+Declarer _globalDeclarer;
+
+/// Gets the declarer for the current scope.
+///
+/// When using the runner, this returns the [Zone]-scoped declarer that's set by
+/// [VmListener]. If the test file is run directly, this returns
+/// [_globalDeclarer] (and sets it up on the first call).
+Declarer get _declarer {
+  var declarer = Zone.current[#unittest.declarer];
+  if (declarer != null) return declarer;
+  if (_globalDeclarer != null) return _globalDeclarer;
+
+  // Since there's no Zone-scoped declarer, the test file is being run directly.
+  // In order to run the tests, we set up our own Declarer via
+  // [_globalDeclarer], and schedule a microtask to run the tests once they're
+  // finished being defined.
+  _globalDeclarer = new Declarer();
+  scheduleMicrotask(() {
+    var suite = new Suite(p.prettyUri(Uri.base), _globalDeclarer.tests);
+    // TODO(nweiz): Use a reporter that doesn't import dart:io here.
+    // TODO(nweiz): Set the exit code on the VM when issue 6943 is fixed.
+    new ConsoleReporter([suite]).run();
+  });
+  return _globalDeclarer;
+}
 
-/// [Configuration] used by the unittest library.
-///
-/// Note that if a configuration has not been set, calling this getter will
-/// create a default configuration.
-Configuration get unittestConfiguration {
-  if (config == null) environment.config = new Configuration();
-  return config;
+// TODO(nweiz): This and other top-level functions should throw exceptions if
+// they're called after the declarer has finished declaring.
+void test(String description, body()) => _declarer.test(description, body);
+
+void group(String description, void body()) =>
+    _declarer.group(description, body);
+
+void setUp(callback()) => _declarer.setUp(callback);
+
+void tearDown(callback()) => _declarer.tearDown(callback);
+
+/// Handle an error that occurs outside of any test.
+void handleExternalError(error, String message, [stackTrace]) {
+  // TODO(nweiz): handle this better.
+  registerException(error, stackTrace);
 }
 
-/// If `true`, stack traces are reformatted to be more readable.
+/// Registers an exception that was caught for the current test.
+void registerException(error, [StackTrace stackTrace]) =>
+    Invoker.current.handleError(error, stackTrace);
+
+// What follows are stubs for various top-level names supported by unittest
+// 0.11.*. These are preserved for the time being for ease of migration, but
+// should be removed before this is released as stable.
+
+@deprecated
+typedef dynamic TestFunction();
+
+@deprecated
+Configuration unittestConfiguration = new Configuration();
+
+@deprecated
 bool formatStacks = true;
 
-/// If `true`, irrelevant frames are filtered from the stack trace.
-///
-/// This does nothing if [formatStacks] is false.
+@deprecated
 bool filterStacks = true;
 
-/// Separator used between group names and test names.
+@deprecated
 String groupSep = ' ';
 
-/// Sets the [Configuration] used by the unittest library.
-///
-/// Throws a [StateError] if there is an existing, incompatible value.
-void set unittestConfiguration(Configuration value) {
-  if (identical(config, value)) return;
-  if (config != null) {
-    logMessage('Warning: The unittestConfiguration has already been set. New '
-        'unittestConfiguration ignored.');
-  } else {
-    environment.config = value;
-  }
-}
-
-/// Logs [message] associated with the current test case.
-///
-/// Tests should use this instead of [print].
-void logMessage(String message) =>
-    config.onLogMessage(currentTestCase, message);
+@deprecated
+void logMessage(String message) => print(message);
 
-/// The test cases that have been defined so far.
-List<TestCase> get testCases =>
-    new UnmodifiableListView<TestCase>(environment.testCases);
+@deprecated
+final testCases = [];
 
-/// The interval (in milliseconds) after which a non-microtask asynchronous
-/// delay will be scheduled between tests.
-///
-/// This is used to avoid starving the DOM or other non-microtask events.
+@deprecated
 const int BREATH_INTERVAL = 200;
 
-/// The [TestCase] currently being executed.
-TestCase get currentTestCase => (environment.currentTestCaseIndex >= 0 &&
-        environment.currentTestCaseIndex < testCases.length)
-    ? testCases[environment.currentTestCaseIndex]
-    : null;
+@deprecated
+TestCase get currentTestCase => null;
 
-/// The same as [currentTestCase], but typed as an [InternalTestCase].
-InternalTestCase get _currentTestCase => currentTestCase as InternalTestCase;
-
-/// The result string for a passing test case.
+@deprecated
 const PASS = 'pass';
 
-/// The result string for a failing test case.
+@deprecated
 const FAIL = 'fail';
 
-/// The result string for an test case with an error.
+@deprecated
 const ERROR = 'error';
 
-/// Creates a new test case with the given description and body.
-///
-/// The description will be added to the descriptions of any surrounding
-/// [group]s.
-void test(String description, TestFunction body) {
-  _requireNotRunning();
-  ensureInitialized();
-
-  if (environment.soloTestSeen && environment.soloNestingLevel == 0) return;
-  var testCase = new InternalTestCase(
-      testCases.length + 1, _fullDescription(description), body);
-  environment.testCases.add(testCase);
-}
-
-/// Returns [description] with all of its group prefixes prepended.
-String _fullDescription(String description) {
-  var group = environment.currentContext.fullName;
-  if (description == null) return group;
-  return group != '' ? '$group$groupSep$description' : description;
-}
-
-/// A convenience function for skipping a test.
+@deprecated
 void skip_test(String spec, TestFunction body) {}
 
-/// Creates a new test case with the given description and body.
-///
-/// If [solo_test] is used instead of [test], then all non-solo tests will be
-/// disabled. Note that if [solo_group] is used as well, all tests in the group
-/// will be enabled, regardless of whether they use [test] or [solo_test], or
-/// whether they are in a nested [group] versus [solo_group]. Put another way,
-/// if there are any calls to [solo_test] or [solo_group] in a test file, all
-/// tests that are not inside a [solo_group] will be disabled unless they are
-/// [solo_test]s.
-void solo_test(String spec, TestFunction body) {
-  _requireNotRunning();
-  ensureInitialized();
-  if (!environment.soloTestSeen) {
-    environment.soloTestSeen = true;
-    // This is the first solo-ed test. Discard all tests up to now.
-    environment.testCases.clear();
-  }
-  environment.soloNestingLevel++;
-  try {
-    test(spec, body);
-  } finally {
-    environment.soloNestingLevel--;
-  }
-}
+@deprecated
+void solo_test(String spec, TestFunction body) => test(spec, body);
 
-/// Indicate that [callback] is expected to be called [count] number of times
-/// (by default 1).
-///
-/// The unittest framework will wait for the callback to run the [count] times
-/// before it considers the current test to be complete. Using [expectAsync]
-/// will also ensure that errors that occur within [callback] are tracked and
-/// reported. [callback] may take up to six optional or required positional
-/// arguments; named arguments are not supported.
-///
-/// [max] can be used to specify an upper bound on the number of calls; if this
-/// is exceeded the test will fail. If [max] is `0` (the default), the callback
-/// is expected to be called exactly [count] times. If [max] is `-1`, the
-/// callback is allowed to be called any number of times greater than [count].
-///
-/// Both [id] and [reason] are optional and provide extra information about the
-/// callback when debugging. [id] should be the name of the callback, while
-/// [reason] should be the reason the callback is expected to be called.
-Function expectAsync(Function callback,
-    {int count: 1, int max: 0, String id, String reason}) =>
-        new ExpectedFunction(callback, count, max, id: id, reason: reason).func;
-
-/// Indicate that [callback] is expected to be called until [isDone] returns
-/// true.
-///
-/// [isDone] is called after each time the function is run. Only when it returns
-/// true will the callback be considered complete. Using [expectAsyncUntil] will
-/// also ensure that errors that occur within [callback] are tracked and
-/// reported. [callback] may take up to six optional or required positional
-/// arguments; named arguments are not supported.
-///
-/// Both [id] and [reason] are optional and provide extra information about the
-/// callback when debugging. [id] should be the name of the callback, while
-/// [reason] should be the reason the callback is expected to be called.
-Function expectAsyncUntil(Function callback, bool isDone(),
-    {String id, String reason}) => new ExpectedFunction(callback, 0, -1,
-        id: id, reason: reason, isDone: isDone).func;
-
-/// Creates a group of tests.
-///
-/// A group's description is included in the descriptions of any tests or
-/// sub-groups it contains. [setUp] and [tearDown] are also scoped to the
-/// containing group.
-void group(String description, void body()) {
-  ensureInitialized();
-  _requireNotRunning();
-  environment.currentContext =
-      new GroupContext(environment.currentContext, description);
-  try {
-    body();
-  } catch (e, trace) {
-    var stack = (trace == null) ? '' : ': ${trace.toString()}';
-    environment.uncaughtErrorMessage = "${e.toString()}$stack";
-  } finally {
-    // Now that the group is over, restore the previous one.
-    environment.currentContext = environment.currentContext.parent;
-  }
-}
-
-/// A convenience function for skipping a group of tests.
+@deprecated
 void skip_group(String description, void body()) {}
 
-/// Creates a group of tests.
-///
-/// If [solo_group] is used instead of [group], then all tests not declared with
-/// [solo_test] or in a [solo_group] will be disabled. Note that all tests in a
-/// [solo_group] will be run, regardless of whether they're declared with [test]
-/// or [solo_test].
-///
-/// [skip_test] and [skip_group] take precedence over [solo_group].
-void solo_group(String description, void body()) {
-  _requireNotRunning();
-  ensureInitialized();
-  if (!environment.soloTestSeen) {
-    environment.soloTestSeen = true;
-    // This is the first solo-ed group. Discard all tests up to now.
-    environment.testCases.clear();
-  }
-  ++environment.soloNestingLevel;
-  try {
-    group(description, body);
-  } finally {
-    --environment.soloNestingLevel;
-  }
-}
+@deprecated
+void solo_group(String description, void body()) => group(description, body);
 
-/// Registers a function to be run before tests.
-///
-/// This function will be called before each test is run. [callback] may be
-/// asynchronous; if so, it must return a [Future].
-///
-/// If this is called within a test group, it applies only to tests in that
-/// group. [callback] will be run after any set-up callbacks in parent groups or
-/// at the top level.
-void setUp(Function callback) {
-  _requireNotRunning();
-  environment.currentContext.testSetUp = callback;
-}
+@deprecated
+void filterTests(testFilter) {}
 
-/// Registers a function to be run after tests.
-///
-/// This function will be called after each test is run. [callback] may be
-/// asynchronous; if so, it must return a [Future].
-///
-/// If this is called within a test group, it applies only to tests in that
-/// group. [callback] will be run before any tear-down callbacks in parent groups or
-/// at the top level.
-void tearDown(Function callback) {
-  _requireNotRunning();
-  environment.currentContext.testTearDown = callback;
-}
+@deprecated
+void runTests() {}
 
-/// Advance to the next test case.
-void _nextTestCase() {
-  environment.currentTestCaseIndex++;
-  _runTest();
-}
+@deprecated
+void ensureInitialized() {}
 
-/// Handle an error that occurs outside of any test.
-void handleExternalError(e, String message, [stackTrace]) {
-  var msg = '$message\nCaught $e';
-
-  if (currentTestCase != null) {
-    _currentTestCase.error(msg, stackTrace);
-  } else {
-    environment.uncaughtErrorMessage = "$msg: $stackTrace";
-  }
-}
-
-/// Remove any tests that match [testFilter].
-///
-/// [testFilter] can be a predicate function, a [RegExp], or a [String]. If it's
-/// a function, it's called with each [TestCase]. If it's a [String], it's
-/// parsed as a [RegExp] and matched against each [TestCase.description].
-///
-/// This is different from enabling or disabling tests in that it removes the
-/// tests completely.
-void filterTests(testFilter) {
-  var filterFunction;
-  if (testFilter is String) {
-    var re = new RegExp(testFilter);
-    filterFunction = (t) => re.hasMatch(t.description);
-  } else if (testFilter is RegExp) {
-    filterFunction = (t) => testFilter.hasMatch(t.description);
-  } else if (testFilter is Function) {
-    filterFunction = testFilter;
-  }
-  environment.testCases.retainWhere(filterFunction);
-}
+@deprecated
+void setSoloTest(int id) {}
 
-/// Runs all queued tests, one at a time.
-void runTests() {
-  _requireNotRunning();
-  _ensureInitialized(false);
-  environment.currentTestCaseIndex = 0;
-  config.onStart();
-  _runTest();
-}
+@deprecated
+void enableTest(int id) {}
 
-/// Registers an exception that was caught for the current test.
-void registerException(error, [StackTrace stackTrace]) =>
-    _currentTestCase.registerException(error, stackTrace);
-
-/// Runs the next test.
-void _runTest() {
-  if (environment.currentTestCaseIndex >= testCases.length) {
-    assert(environment.currentTestCaseIndex == testCases.length);
-    _completeTests();
-    return;
-  }
-
-  var testCase = _currentTestCase;
-  var f = runZoned(testCase.run, onError: (error, stack) {
-    // TODO(kevmoo) Do a better job of flagging these are async errors.
-    // https://code.google.com/p/dart/issues/detail?id=16530
-    testCase.registerException(error, stack);
-  });
+@deprecated
+void disableTest(int id) {}
 
-  var timer;
-  var timeout = unittestConfiguration.timeout;
-  if (timeout != null) {
-    try {
-      timer = new Timer(timeout, () {
-        testCase.error("Test timed out after ${timeout.inSeconds} seconds.");
-        _nextTestCase();
-      });
-    } on UnsupportedError catch (e) {
-      if (e.message != "Timer greater than 0.") rethrow;
-      // Support running on d8 and jsshell which don't support timers.
-    }
-  }
-
-  f.whenComplete(() {
-    if (timer != null) timer.cancel();
-    var now = new DateTime.now().millisecondsSinceEpoch;
-    if (now - environment.lastBreath >= BREATH_INTERVAL) {
-      environment.lastBreath = now;
-      Timer.run(_nextTestCase);
-    } else {
-      scheduleMicrotask(_nextTestCase); // Schedule the next test.
-    }
-  });
-}
-
-/// Notify the configuration that the testing has finished.
-void _completeTests() {
-  if (!environment.initialized) return;
-
-  var passed = 0;
-  var failed = 0;
-  var errors = 0;
-  for (var testCase in testCases) {
-    switch (testCase.result) {
-      case PASS:
-        passed++;
-        break;
-      case FAIL:
-        failed++;
-        break;
-      case ERROR:
-        errors++;
-        break;
-    }
-  }
-
-  config.onSummary(
-      passed, failed, errors, testCases, environment.uncaughtErrorMessage);
-  config.onDone(passed > 0 &&
-      failed == 0 &&
-      errors == 0 &&
-      environment.uncaughtErrorMessage == null);
-  environment.initialized = false;
-  environment.currentTestCaseIndex = -1;
-}
-
-/// Initializes the test environment if it hasn't already been initialized.
-void ensureInitialized() {
-  _ensureInitialized(true);
-}
-
-/// Initializes the test environment.
-///
-/// If [configAutoStart] is `true`, schedule a microtask to run the tests. This
-/// microtask is expected to run after all the tests are defined.
-void _ensureInitialized(bool configAutoStart) {
-  if (environment.initialized) return;
-
-  environment.initialized = true;
-
-  environment.uncaughtErrorMessage = null;
-
-  unittestConfiguration.onInit();
-
-  // Immediately queue the suite up. It will run after a timeout (i.e. after
-  // main() has returned).
-  if (configAutoStart && config.autoStart) scheduleMicrotask(runTests);
-}
-
-/// Remove all tests other than the one identified by [id].
-void setSoloTest(int id) =>
-    environment.testCases.retainWhere((t) => t.id == id);
-
-/// Enable the test identified by [id].
-void enableTest(int id) => _setTestEnabledState(id, enable: true);
-
-/// Disable the test by [id].
-void disableTest(int id) => _setTestEnabledState(id, enable: false);
-
-/// Enable or disable the test identified by [id].
-void _setTestEnabledState(int id, {bool enable: true}) {
-  // Try fast path first.
-  if (testCases.length > id && testCases[id].id == id) {
-    environment.testCases[id].enabled = enable;
-  } else {
-    for (var i = 0; i < testCases.length; i++) {
-      if (testCases[i].id != id) continue;
-      environment.testCases[i].enabled = enable;
-      break;
-    }
-  }
-}
-
-/// Throws a [StateError] if tests are running.
-void _requireNotRunning() {
-  if (environment.currentTestCaseIndex == -1) return;
-  throw new StateError('Not allowed when tests are running.');
-}
-
-/// Creates a test environment running in its own zone scope.
-///
-/// This allows for multiple invocations of the unittest library in the same
-/// application instance. This is useful when, for example, creating a test
-/// runner application which needs to create a new pristine test environment on
-/// each invocation to run a given set of tests.
-withTestEnvironment(callback()) {
-  return runZoned(callback,
-      zoneValues: {#unittest.environment: new TestEnvironment()});
-}
+@deprecated
+withTestEnvironment(callback()) => callback();
diff --git a/lib/vm_config.dart b/lib/vm_config.dart
index 77e91e35..44d5158d 100644
--- a/lib/vm_config.dart
+++ b/lib/vm_config.dart
@@ -2,64 +2,25 @@
 // 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.
 
-/// A simple unit test library for running tests on the VM.
+@deprecated
 library unittest.vm_config;
 
-import 'dart:async';
-import 'dart:io';
-import 'unittest.dart';
+import 'src/simple_configuration.dart';
 
+/// This is a stub class used to preserve compatibility with unittest 0.11.*.
+///
+/// It will be removed before the next version is released.
+@deprecated
 class VMConfiguration extends SimpleConfiguration {
-  // Color constants used for generating messages.
   final String GREEN_COLOR = '\u001b[32m';
   final String RED_COLOR = '\u001b[31m';
   final String MAGENTA_COLOR = '\u001b[35m';
   final String NO_COLOR = '\u001b[0m';
 
-  // We make this public so the user can turn it off if they want.
-  bool useColor;
+  bool useColor = false;
 
-  VMConfiguration()
-      : super(),
-        useColor = stdioType(stdout) == StdioType.TERMINAL;
-
-  String formatResult(TestCase testCase) {
-    String result = super.formatResult(testCase);
-    if (useColor) {
-      if (testCase.result == PASS) {
-        return "${GREEN_COLOR}${result}${NO_COLOR}";
-      } else if (testCase.result == FAIL) {
-        return "${RED_COLOR}${result}${NO_COLOR}";
-      } else if (testCase.result == ERROR) {
-        return "${MAGENTA_COLOR}${result}${NO_COLOR}";
-      }
-    }
-    return result;
-  }
-
-  void onInit() {
-    super.onInit();
-    filterStacks = formatStacks = true;
-  }
-
-  void onDone(bool success) {
-    int status;
-    try {
-      super.onDone(success);
-      status = 0;
-    } catch (ex) {
-      // A non-zero exit code is used by the test infrastructure to detect
-      // failure.
-      status = 1;
-    }
-    Future.wait([stdout.close(), stderr.close()]).then((_) {
-      exit(status);
-    });
-  }
-}
-
-void useVMConfiguration() {
-  unittestConfiguration = _singleton;
+  VMConfiguration() : super();
 }
 
-final _singleton = new VMConfiguration();
+@deprecated
+void useVMConfiguration() {}
diff --git a/pubspec.yaml b/pubspec.yaml
index 049509e8..869b14cf 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -16,7 +16,3 @@ dependencies:
     git: https://github.com/dart-lang/matcher.git
 dev_dependencies:
   fake_async: '>=0.1.2 <0.2.0'
-  metatest:
-    git:
-      ref: b6348d7e7f3c5b00a48aa579694457d1abd36b69
-      url: https://github.com/dart-lang/metatest
diff --git a/test/async_exception_test.dart b/test/async_exception_test.dart
deleted file mode 100644
index d81e7de6..00000000
--- a/test/async_exception_test.dart
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.async_exception_test;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsFail('async errors cause tests to fail', () {
-    test('async', () {
-      expectAsync(() {});
-      new Future(() {
-        throw "an error!";
-      });
-    });
-
-    test('sync', () {
-      expectAsync(() {});
-      new Future(() {
-        throw "an error!";
-      });
-    });
-  });
-}
diff --git a/test/async_exception_with_future_test.dart b/test/async_exception_with_future_test.dart
deleted file mode 100644
index c5c445e1..00000000
--- a/test/async_exception_with_future_test.dart
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) 2014, 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.
-
-library unittest.async_exception_with_future_test;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('async exception with future test', () {
-    var tearDownHappened = false;
-
-    tearDown(() {
-      tearDownHappened = true;
-    });
-
-    test('test', () {
-      expect(tearDownHappened, isFalse);
-      // The "throw" statement below should terminate the test immediately.
-      // The framework should not wait for the future to complete.
-      // tearDown should still execute.
-      new Future.sync(() {
-        throw "error!";
-      });
-      return new Completer().future;
-    });
-
-    test('follow up', () {
-      expect(tearDownHappened, isTrue);
-    });
-  }, [
-    {'description': 'test', 'message': 'Caught error!', 'result': 'fail',},
-    {'description': 'follow up', 'result': 'pass',}
-  ]);
-}
diff --git a/test/async_setup_teardown_test.dart b/test/async_setup_teardown_test.dart
deleted file mode 100644
index 1068ee70..00000000
--- a/test/async_setup_teardown_test.dart
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.async_setup_teardown;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsPass('good setup/good teardown', () {
-    setUp(() {
-      return new Future.value(0);
-    });
-    tearDown(() {
-      return new Future.value(0);
-    });
-    test('foo1', () {});
-  });
-
-  expectTestResults('good setup/bad teardown', () {
-    setUp(() {
-      return new Future.value(0);
-    });
-    tearDown(() {
-      return new Future.error("Failed to complete tearDown");
-    });
-    test('foo2', () {});
-  }, [
-    {
-      'result': 'error',
-      'message': 'Teardown failed: Caught Failed to complete tearDown'
-    }
-  ]);
-
-  expectTestResults('bad setup/good teardown', () {
-    setUp(() {
-      return new Future.error("Failed to complete setUp");
-    });
-    tearDown(() {
-      return new Future.value(0);
-    });
-    test('foo3', () {});
-  }, [
-    {
-      'result': 'error',
-      'message': 'Setup failed: Caught Failed to complete setUp'
-    }
-  ]);
-
-  expectTestResults('bad setup/bad teardown', () {
-    setUp(() {
-      return new Future.error("Failed to complete setUp");
-    });
-    tearDown(() {
-      return new Future.error("Failed to complete tearDown");
-    });
-    test('foo4', () {});
-  }, [
-    {
-      'result': 'error',
-      'message': 'Setup failed: Caught Failed to complete setUp'
-    }
-  ]);
-}
diff --git a/test/breath_test.dart b/test/breath_test.dart
deleted file mode 100644
index 244d2d2c..00000000
--- a/test/breath_test.dart
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (c) 2012, 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.
-
-library unittest.breath_test;
-
-import 'dart:async';
-
-import 'package:unittest/unittest.dart';
-
-void main() {
-  // Test the sync test 'breath' feature of unittest.
-
-  // We use the testStartStopwatch to determine if the 'starve'
-  // test was executed within a small enough time interval from
-  // the first test that we are guaranteed the second test is
-  // running in a microtask. If the second test is running as a
-  // microtask we are guaranteed the timer scheduled in the
-  // first test has not been run yet.
-  var testStartStopwatch = new Stopwatch()..start();
-
-  group('breath', () {
-    var sentinel = 0;
-
-    test('initial', () {
-      Timer.run(() {
-        sentinel = 1;
-      });
-    });
-
-    test('starve', () {
-      // If less than BREATH_INTERVAL time has passed since before
-      // we started the test group then the previous test's timer
-      // has not been run (at least this is what we are testing).
-      if (testStartStopwatch.elapsed.inMilliseconds <= BREATH_INTERVAL) {
-        expect(sentinel, 0);
-      }
-
-      // Next we wait for at least BREATH_INTERVAL to guaranteed the
-      // next (third) test is run using a timer which means it will
-      // run after the timer scheduled in the first test and hence
-      // the sentinel should have been set to 1.
-      var sw = new Stopwatch()..start();
-      while (sw.elapsed.inMilliseconds < BREATH_INTERVAL);
-    });
-
-    test('breathed', () {
-      expect(sentinel, 1);
-    });
-  });
-}
diff --git a/test/completion_test.dart b/test/completion_test.dart
deleted file mode 100644
index 6803e68b..00000000
--- a/test/completion_test.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.completion_test;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsPass('completion test', () {
-    var count = 0;
-    test('test', () {
-      var _callback;
-      _callback = expectAsyncUntil(() {
-        if (++count < 10) {
-          new Future.sync(_callback);
-        }
-      }, () => (count == 10));
-      new Future.sync(_callback);
-    });
-
-    test('verify count', () {
-      expect(count, 10);
-    });
-  });
-}
diff --git a/test/console_reporter_test.dart b/test/console_reporter_test.dart
index 81661b7d..6a114f31 100644
--- a/test/console_reporter_test.dart
+++ b/test/console_reporter_test.dart
@@ -23,9 +23,9 @@ void main() {
 
   test("runs several successful tests and reports when each completes", () {
     _expectReport("""
-        declarer.test('success 1', () {});
-        declarer.test('success 2', () {});
-        declarer.test('success 3', () {});""",
+        test('success 1', () {});
+        test('success 2', () {});
+        test('success 3', () {});""",
         """
         +0: success 1
         +1: success 1
@@ -38,28 +38,28 @@ void main() {
 
   test("runs several failing tests and reports when each fails", () {
     _expectReport("""
-        declarer.test('failure 1', () => throw new TestFailure('oh no'));
-        declarer.test('failure 2', () => throw new TestFailure('oh no'));
-        declarer.test('failure 3', () => throw new TestFailure('oh no'));""",
+        test('failure 1', () => throw new TestFailure('oh no'));
+        test('failure 2', () => throw new TestFailure('oh no'));
+        test('failure 3', () => throw new TestFailure('oh no'));""",
         """
         +0: failure 1
         +0 -1: failure 1
           oh no
-          test.dart 7:42  main.<fn>
+          test.dart 6:33  main.<fn>
           dart:isolate    _RawReceivePortImpl._handleMessage
 
 
         +0 -1: failure 2
         +0 -2: failure 2
           oh no
-          test.dart 8:42  main.<fn>
+          test.dart 7:33  main.<fn>
           dart:isolate    _RawReceivePortImpl._handleMessage
 
 
         +0 -2: failure 3
         +0 -3: failure 3
           oh no
-          test.dart 9:42  main.<fn>
+          test.dart 8:33  main.<fn>
           dart:isolate    _RawReceivePortImpl._handleMessage
 
 
@@ -68,15 +68,15 @@ void main() {
 
   test("runs failing tests along with successful tests", () {
     _expectReport("""
-        declarer.test('failure 1', () => throw new TestFailure('oh no'));
-        declarer.test('success 1', () {});
-        declarer.test('failure 2', () => throw new TestFailure('oh no'));
-        declarer.test('success 2', () {});""",
+        test('failure 1', () => throw new TestFailure('oh no'));
+        test('success 1', () {});
+        test('failure 2', () => throw new TestFailure('oh no'));
+        test('success 2', () {});""",
         """
         +0: failure 1
         +0 -1: failure 1
           oh no
-          test.dart 7:42  main.<fn>
+          test.dart 6:33  main.<fn>
           dart:isolate    _RawReceivePortImpl._handleMessage
 
 
@@ -85,7 +85,7 @@ void main() {
         +1 -1: failure 2
         +1 -2: failure 2
           oh no
-          test.dart 9:42  main.<fn>
+          test.dart 8:33  main.<fn>
           dart:isolate    _RawReceivePortImpl._handleMessage
 
 
@@ -99,40 +99,40 @@ void main() {
         // This completer ensures that the test isolate isn't killed until all
         // errors have been thrown.
         var completer = new Completer();
-        declarer.test('failures', () {
+        test('failures', () {
           new Future.microtask(() => throw 'first error');
           new Future.microtask(() => throw 'second error');
           new Future.microtask(() => throw 'third error');
           new Future.microtask(completer.complete);
         });
-        declarer.test('wait', () => completer.future);""",
+        test('wait', () => completer.future);""",
         """
         +0: failures
         +0 -1: failures
           first error
-          test.dart 11:38  main.<fn>.<fn>
+          test.dart 10:38  main.<fn>.<fn>
           dart:isolate     _RawReceivePortImpl._handleMessage
           ===== asynchronous gap ===========================
           dart:async       Future.Future.microtask
-          test.dart 11:15  main.<fn>
+          test.dart 10:15  main.<fn>
           dart:isolate     _RawReceivePortImpl._handleMessage
 
 
           second error
-          test.dart 12:38  main.<fn>.<fn>
+          test.dart 11:38  main.<fn>.<fn>
           dart:isolate     _RawReceivePortImpl._handleMessage
           ===== asynchronous gap ===========================
           dart:async       Future.Future.microtask
-          test.dart 12:15  main.<fn>
+          test.dart 11:15  main.<fn>
           dart:isolate     _RawReceivePortImpl._handleMessage
 
 
           third error
-          test.dart 13:38  main.<fn>.<fn>
+          test.dart 12:38  main.<fn>.<fn>
           dart:isolate     _RawReceivePortImpl._handleMessage
           ===== asynchronous gap ===========================
           dart:async       Future.Future.microtask
-          test.dart 13:15  main.<fn>
+          test.dart 12:15  main.<fn>
           dart:isolate     _RawReceivePortImpl._handleMessage
 
 
@@ -151,7 +151,6 @@ import 'dart:async';
 import 'package:unittest/unittest.dart';
 
 void main() {
-  var declarer = Zone.current[#unittest.declarer];
 $tests
 }
 """;
diff --git a/test/correct_callback_test.dart b/test/correct_callback_test.dart
deleted file mode 100644
index f69766ed..00000000
--- a/test/correct_callback_test.dart
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.correct_callback_test;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsPass('correct callback test', () {
-    var count = 0;
-    test('test', () => new Future.sync(expectAsync(() {
-      ++count;
-    })));
-
-    test('verify count', () {
-      expect(count, 1);
-    });
-  });
-}
diff --git a/test/exception_test.dart b/test/exception_test.dart
deleted file mode 100644
index 137f0a36..00000000
--- a/test/exception_test.dart
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.exception_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('good setup/good teardown', () {
-    test('test', () {
-      throw new Exception('Fail.');
-    });
-  }, [{'result': 'error', 'message': 'Test failed: Caught Exception: Fail.'}]);
-}
diff --git a/test/excess_callback_test.dart b/test/excess_callback_test.dart
deleted file mode 100644
index 2679d17e..00000000
--- a/test/excess_callback_test.dart
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.excess_callback_test;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  var count = 0;
-
-  expectTestResults('excess callback test', () {
-    test('test', () {
-      var _callback0 = expectAsync(() => ++count);
-      var _callback1 = expectAsync(() => ++count);
-      var _callback2 = expectAsync(() {
-        _callback1();
-        _callback1();
-        _callback0();
-      });
-      new Future.sync(_callback2);
-    });
-
-    test('verify count', () {
-      expect(count, 1);
-    });
-  }, [
-    {
-      'description': 'test',
-      'message': 'Callback called more times than expected (1).',
-      'result': 'fail'
-    },
-    {'description': 'verify count', 'result': 'pass',}
-  ]);
-}
diff --git a/test/expect_async_args_test.dart b/test/expect_async_args_test.dart
deleted file mode 100644
index 8e814dc9..00000000
--- a/test/expect_async_args_test.dart
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.expect_async_args_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('expect async args', () {
-    var count = 0;
-    List<int> _getArgs([a = 0, b = 0, c = 0, d = 0, e = 0, f = 0]) {
-      count++;
-      return [a, b, c, d, e, f];
-    }
-
-    test('expect async args', () {
-      expect(expectAsync(_getArgs)(), [0, 0, 0, 0, 0, 0]);
-      expect(expectAsync(_getArgs)(5), [5, 0, 0, 0, 0, 0]);
-      expect(expectAsync(_getArgs)(1, 2, 3, 4, 5, 6), [1, 2, 3, 4, 5, 6]);
-    });
-
-    test('invoked with too many args', () {
-      expectAsync(_getArgs)(1, 2, 3, 4, 5, 6, 7);
-    });
-
-    test('created with too many args', () {
-      expectAsync((a1, a2, a3, a4, a5, a6, a7) {
-        count++;
-      })();
-    });
-
-    test('verify count', () {
-      expect(count, 3);
-    });
-  }, [
-    {'description': 'expect async args', 'result': 'pass',},
-    {'description': 'invoked with too many args', 'result': 'error',},
-    {'description': 'created with too many args', 'result': 'error',},
-    {'description': 'verify count', 'result': 'pass',}
-  ]);
-}
diff --git a/test/expect_async_test.dart b/test/expect_async_test.dart
index 844e44b0..a512f988 100644
--- a/test/expect_async_test.dart
+++ b/test/expect_async_test.dart
@@ -1,100 +1,328 @@
-// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// Copyright (c) 2015, 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.
 
-library unittest.expect_async_test;
+import 'package:unittest/src/state.dart';
+import 'package:unittest/unittest.dart';
 
-import 'dart:async';
+import 'utils.dart';
 
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
+void main() {
+  group("supports a function with this many arguments:", () {
+    test("0", () {
+      var callbackRun = false;
+      return runTest(() {
+        expectAsync(() {
+          callbackRun = true;
+        })();
+      }).then((liveTest) {
+        expectTestPassed(liveTest);
+        expect(callbackRun, isTrue);
+      });
+    });
 
-void main() => initTests(_test);
+    test("1", () {
+      var callbackRun = false;
+      return runTest(() {
+        expectAsync((arg) {
+          expect(arg, equals(1));
+          callbackRun = true;
+        })(1);
+      }).then((liveTest) {
+        expectTestPassed(liveTest);
+        expect(callbackRun, isTrue);
+      });
+    });
 
-void _test(message) {
-  initMetatest(message);
+    test("2", () {
+      var callbackRun = false;
+      return runTest(() {
+        expectAsync((arg1, arg2) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          callbackRun = true;
+        })(1, 2);
+      }).then((liveTest) {
+        expectTestPassed(liveTest);
+        expect(callbackRun, isTrue);
+      });
+    });
 
-  var count = 0;
+    test("3", () {
+      var callbackRun = false;
+      return runTest(() {
+        expectAsync((arg1, arg2, arg3) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          callbackRun = true;
+        })(1, 2, 3);
+      }).then((liveTest) {
+        expectTestPassed(liveTest);
+        expect(callbackRun, isTrue);
+      });
+    });
 
-  expectTestsPass('expect async test', () {
-    test('expectAsync zero params', () {
-      new Future.sync(expectAsync(() {
-        ++count;
-      }));
+    test("4", () {
+      var callbackRun = false;
+      return runTest(() {
+        expectAsync((arg1, arg2, arg3, arg4) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          expect(arg4, equals(4));
+          callbackRun = true;
+        })(1, 2, 3, 4);
+      }).then((liveTest) {
+        expectTestPassed(liveTest);
+        expect(callbackRun, isTrue);
+      });
     });
 
-    test('expectAsync 1 param', () {
-      var func = expectAsync((arg) {
-        expect(arg, 0);
-        ++count;
+    test("5", () {
+      var callbackRun = false;
+      return runTest(() {
+        expectAsync((arg1, arg2, arg3, arg4, arg5) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          expect(arg4, equals(4));
+          expect(arg5, equals(5));
+          callbackRun = true;
+        })(1, 2, 3, 4, 5);
+      }).then((liveTest) {
+        expectTestPassed(liveTest);
+        expect(callbackRun, isTrue);
       });
-      new Future.sync(() => func(0));
     });
 
-    test('expectAsync 2 param', () {
-      var func = expectAsync((arg0, arg1) {
-        expect(arg0, 0);
-        expect(arg1, 1);
-        ++count;
+    test("6", () {
+      var callbackRun = false;
+      return runTest(() {
+        expectAsync((arg1, arg2, arg3, arg4, arg5, arg6) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          expect(arg4, equals(4));
+          expect(arg5, equals(5));
+          expect(arg6, equals(6));
+          callbackRun = true;
+        })(1, 2, 3, 4, 5, 6);
+      }).then((liveTest) {
+        expectTestPassed(liveTest);
+        expect(callbackRun, isTrue);
       });
-      new Future.sync(() => func(0, 1));
     });
+  });
 
-    test('single arg to Future.catchError', () {
-      var func = expectAsync((error) {
-        expect(error, isStateError);
-        ++count;
+  group("with optional arguments", () {
+    test("allows them to be passed", () {
+      var callbackRun = false;
+      return runTest(() {
+        expectAsync(([arg = 1]) {
+          expect(arg, equals(2));
+          callbackRun = true;
+        })(2);
+      }).then((liveTest) {
+        expectTestPassed(liveTest);
+        expect(callbackRun, isTrue);
       });
+    });
 
-      new Future(() {
-        throw new StateError('test');
-      }).catchError(func);
+    test("allows them not to be passed", () {
+      var callbackRun = false;
+      return runTest(() {
+        expectAsync(([arg = 1]) {
+          expect(arg, equals(1));
+          callbackRun = true;
+        })();
+      }).then((liveTest) {
+        expectTestPassed(liveTest);
+        expect(callbackRun, isTrue);
+      });
     });
+  });
+
+  test("doesn't support a function with 7 arguments", () {
+    expect(() => expectAsync((_1, _2, _3, _4, _5, _6, _7) {}),
+        throwsArgumentError);
+  });
+
+  group("by default", () {
+    test("won't allow the test to complete until it's called", () {
+      return expectTestBlocks(
+          () => expectAsync(() {}),
+          (callback) => callback());
+    });
+
+    test("may only be called once", () {
+      return runTest(() {
+        var callback = expectAsync(() {});
+        callback();
+        callback();
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            "Callback called more times than expected (1).");
+      });
+    });
+  });
 
-    test('2 args to Future.catchError', () {
-      var func = expectAsync((error, stack) {
-        expect(error, isStateError);
-        expect(stack is StackTrace, isTrue);
-        ++count;
+  group("with count", () {
+    test("won't allow the test to complete until it's called at least that "
+        "many times", () {
+      var liveTest;
+      var future;
+      liveTest = createTest(() {
+        var callback = expectAsync(() {}, count: 3);
+        future = pumpEventQueue().then((_) {
+          expect(liveTest.state.status, equals(Status.running));
+          callback();
+          return pumpEventQueue();
+        }).then((_) {
+          expect(liveTest.state.status, equals(Status.running));
+          callback();
+          return pumpEventQueue();
+        }).then((_) {
+          expect(liveTest.state.status, equals(Status.running));
+          callback();
+        });
       });
 
-      new Future(() {
-        throw new StateError('test');
-      }).catchError(func);
+      return liveTest.run().then((_) {
+        expectTestPassed(liveTest);
+        // Ensure that the outer test doesn't complete until the inner future
+        // completes.
+        return future;
+      });
+    });
+
+    test("will throw an error if it's called more than that many times", () {
+      return runTest(() {
+        var callback = expectAsync(() {}, count: 3);
+        callback();
+        callback();
+        callback();
+        callback();
+      }).then((liveTest) {
+        expectTestFailed(
+            liveTest, "Callback called more times than expected (3).");
+      });
     });
 
-    test('zero of two optional positional args', () {
-      var func = expectAsync(([arg0 = true, arg1 = true]) {
-        expect(arg0, isTrue);
-        expect(arg1, isTrue);
-        ++count;
+    group("0,", () {
+      test("won't block the test's completion", () {
+        expectAsync(() {}, count: 0);
+      });
+
+      test("will throw an error if it's ever called", () {
+        return runTest(() {
+          expectAsync(() {}, count: 0)();
+        }).then((liveTest) {
+          expectTestFailed(
+              liveTest, "Callback called more times than expected (0).");
+        });
       });
+    });
+  });
+
+  group("with max", () {
+    test("will allow the callback to be called that many times", () {
+      var callback = expectAsync(() {}, max: 3);
+      callback();
+      callback();
+      callback();
+    });
 
-      new Future.sync(() => func());
+    test("will allow the callback to be called fewer than that many times", () {
+      var callback = expectAsync(() {}, max: 3);
+      callback();
     });
 
-    test('one of two optional positional args', () {
-      var func = expectAsync(([arg0 = true, arg1 = true]) {
-        expect(arg0, isFalse);
-        expect(arg1, isTrue);
-        ++count;
+    test("will throw an error if it's called more than that many times", () {
+      return runTest(() {
+        var callback = expectAsync(() {}, max: 3);
+        callback();
+        callback();
+        callback();
+        callback();
+      }).then((liveTest) {
+        expectTestFailed(
+            liveTest, "Callback called more times than expected (3).");
       });
+    });
 
-      new Future.sync(() => func(false));
+    test("-1, will allow the callback to be called any number of times", () {
+      var callback = expectAsync(() {}, max: -1);
+      for (var i = 0; i < 20; i++) {
+        callback();
+      }
     });
+  });
 
-    test('two of two optional positional args', () {
-      var func = expectAsync(([arg0 = true, arg1 = true]) {
-        expect(arg0, isFalse);
-        expect(arg1, isNull);
-        ++count;
+  test("will throw an error if max is less than count", () {
+    expect(() => expectAsync(() {}, max: 1, count: 2),
+        throwsArgumentError);
+  });
+
+  group("expectAsyncUntil()", () {
+    test("won't allow the test to complete until isDone returns true", () {
+      var liveTest;
+      var future;
+      liveTest = createTest(() {
+        var done = false;
+        var callback = expectAsyncUntil(() {}, () => done);
+
+        future = pumpEventQueue().then((_) {
+          expect(liveTest.state.status, equals(Status.running));
+          callback();
+          return pumpEventQueue();
+        }).then((_) {
+          expect(liveTest.state.status, equals(Status.running));
+          done = true;
+          callback();
+        });
       });
 
-      new Future.sync(() => func(false, null));
+      return liveTest.run().then((_) {
+        expectTestPassed(liveTest);
+        // Ensure that the outer test doesn't complete until the inner future
+        // completes.
+        return future;
+      });
     });
 
-    test('verify count', () {
-      expect(count, 8);
+    test("doesn't call isDone until after the callback is called", () {
+      var callbackRun = false;
+      expectAsyncUntil(() => callbackRun = true, () {
+        expect(callbackRun, isTrue);
+        return true;
+      })();
+    });
+  });
+
+  group("with errors", () {
+    test("reports them to the current test", () {
+      return runTest(() {
+        expectAsync(() => throw new TestFailure('oh no'))();
+      }).then((liveTest) {
+        expectTestFailed(liveTest, 'oh no');
+      });
+    });
+
+    test("swallows them and returns null", () {
+      var returnValue;
+      var caughtError = false;
+      return runTest(() {
+        try {
+          returnValue = expectAsync(() => throw new TestFailure('oh no'))();
+        } on TestFailure catch (_) {
+          caughtError = true;
+        }
+      }).then((liveTest) {
+        expectTestFailed(liveTest, 'oh no');
+        expect(returnValue, isNull);
+        expect(caughtError, isFalse);
+      });
     });
   });
 }
diff --git a/test/future_matchers_test.dart b/test/future_matchers_test.dart
deleted file mode 100644
index c09542c3..00000000
--- a/test/future_matchers_test.dart
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright (c) 2012, 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.
-
-library unittest.future_matchers_test;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('group name test', () {
-    test('completes - unexpected error', () {
-      var completer = new Completer();
-      completer.completeError('X');
-      expect(completer.future, completes);
-    });
-
-    test('completes - successfully', () {
-      var completer = new Completer();
-      completer.complete('1');
-      expect(completer.future, completes);
-    });
-
-    test('throws - unexpected to see normal completion', () {
-      var completer = new Completer();
-      completer.complete('1');
-      expect(completer.future, throws);
-    });
-
-    test('throws - expected to see exception', () {
-      var completer = new Completer();
-      completer.completeError('X');
-      expect(completer.future, throws);
-    });
-
-    test('throws - expected to see exception thrown later on', () {
-      var completer = new Completer();
-      var chained = completer.future.then((_) {
-        throw 'X';
-      });
-      expect(chained, throws);
-      completer.complete('1');
-    });
-
-    test('throwsA - unexpected normal completion', () {
-      var completer = new Completer();
-      completer.complete('1');
-      expect(completer.future, throwsA(equals('X')));
-    });
-
-    test('throwsA - correct error', () {
-      var completer = new Completer();
-      completer.completeError('X');
-      expect(completer.future, throwsA(equals('X')));
-    });
-
-    test('throwsA - wrong error', () {
-      var completer = new Completer();
-      completer.completeError('X');
-      expect(completer.future, throwsA(equals('Y')));
-    });
-  }, [
-    {
-      'result': 'fail',
-      'message': 'Expected future to complete successfully, but it failed with '
-          'X',
-    },
-    {'result': 'pass'},
-    {
-      'result': 'fail',
-      'message': 'Expected future to fail, but succeeded with \'1\'.'
-    },
-    {'result': 'pass'},
-    {'result': 'pass'},
-    {
-      'result': 'fail',
-      'message': 'Expected future to fail, but succeeded with \'1\'.'
-    },
-    {'result': 'pass'},
-    {
-      'result': 'fail',
-      'message': '''Expected: 'Y'
-  Actual: 'X'
-   Which: is different.
-Expected: Y
-  Actual: X
-          ^
- Differ at offset 0
-'''
-    }
-  ]);
-}
diff --git a/test/group_name_test.dart b/test/group_name_test.dart
deleted file mode 100644
index 3184ffc3..00000000
--- a/test/group_name_test.dart
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.group_name_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('group name test', () {
-    group('a', () {
-      test('a', () {});
-      group('b', () {
-        test('b', () {});
-      });
-    });
-  }, [{'description': 'a a'}, {'description': 'a b b'}]);
-}
diff --git a/test/invalid_ops_test.dart b/test/invalid_ops_test.dart
deleted file mode 100644
index d9e05e74..00000000
--- a/test/invalid_ops_test.dart
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.invalid_ops_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsPass('testcases immutable', () {
-    test('test', () {
-      expect(() => test('test', () {}), throwsStateError);
-      expect(() => solo_test('test', () {}), throwsStateError);
-      expect(() => group('test', () {}), throwsStateError);
-      expect(() => solo_group('test', () {}), throwsStateError);
-      expect(() => setUp(() {}), throwsStateError);
-      expect(() => tearDown(() {}), throwsStateError);
-      expect(() => runTests(), throwsStateError);
-    });
-  });
-}
diff --git a/test/io.dart b/test/io.dart
index fc545621..09bd31df 100644
--- a/test/io.dart
+++ b/test/io.dart
@@ -17,10 +17,18 @@ String _computePackageDir() =>
 
 /// Runs the unittest executable with the package root set properly.
 ProcessResult runUnittest(List<String> args, {String workingDirectory}) {
-  var allArgs = Platform.executableArguments.toList()
-     ..add(p.join(packageDir, 'bin/unittest.dart'))
-     ..add("--package-root=${p.join(packageDir, 'packages')}")
-     ..addAll(args);
+  var allArgs = [
+    p.join(packageDir, 'bin/unittest.dart'),
+    "--package-root=${p.join(packageDir, 'packages')}"
+  ]..addAll(args);
+
+  // TODO(nweiz): Use ScheduledProcess once it's compatible.
+  return runDart(allArgs, workingDirectory: workingDirectory);
+}
+
+/// Runs Dart.
+ProcessResult runDart(List<String> args, {String workingDirectory}) {
+  var allArgs = Platform.executableArguments.toList()..addAll(args);
 
   // TODO(nweiz): Use ScheduledProcess once it's compatible.
   return Process.runSync(Platform.executable, allArgs,
diff --git a/test/late_exception_test.dart b/test/late_exception_test.dart
deleted file mode 100644
index 94ad30b3..00000000
--- a/test/late_exception_test.dart
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.late_exception_test;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('late exception test', () {
-    var f;
-    test('testOne', () {
-      f = expectAsync(() {});
-      new Future.sync(f);
-    });
-    test('testTwo', () {
-      new Future.sync(expectAsync(() {
-        f();
-      }));
-    });
-  }, [
-    {
-      'description': 'testOne',
-      'message': 'Callback called (2) after test case testOne had already been '
-          'marked as pass.',
-      'result': 'error',
-    },
-    {'description': 'testTwo', 'result': 'pass',}
-  ]);
-}
diff --git a/test/loader_test.dart b/test/loader_test.dart
index 47197fbf..9ef3f6e3 100644
--- a/test/loader_test.dart
+++ b/test/loader_test.dart
@@ -2,8 +2,6 @@
 // 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.
 
-library unittest.loader;
-
 import 'dart:io';
 
 import 'package:path/path.dart' as p;
@@ -23,10 +21,9 @@ import 'dart:async';
 import 'package:unittest/unittest.dart';
 
 void main() {
-  var declarer = Zone.current[#unittest.declarer];
-  declarer.test("success", () {});
-  declarer.test("failure", () => throw new TestFailure('oh no'));
-  declarer.test("error", () => throw 'oh no');
+  test("success", () {});
+  test("failure", () => throw new TestFailure('oh no'));
+  test("error", () => throw 'oh no');
 }
 """;
 
diff --git a/test/matcher/completion_test.dart b/test/matcher/completion_test.dart
new file mode 100644
index 00000000..360a5cf2
--- /dev/null
+++ b/test/matcher/completion_test.dart
@@ -0,0 +1,100 @@
+// Copyright (c) 2015, 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.
+
+import 'dart:async';
+
+import 'package:unittest/unittest.dart';
+
+import '../utils.dart';
+
+void main() {
+  group("[completes]", () {
+    test("blocks the test until the Future completes", () {
+      return expectTestBlocks(() {
+        var completer = new Completer();
+        expect(completer.future, completes);
+        return completer;
+      }, (completer) => completer.complete());
+    });
+
+    test("with an error", () {
+      return runTest(() {
+        expect(new Future.error('X'), completes);
+      }).then((liveTest) {
+        expectTestFailed(liveTest, startsWith(
+            "Expected future to complete successfully, but it failed with X"));
+      });
+    });
+
+    test("with a failure", () {
+      return runTest(() {
+        expect(new Future.error(new TestFailure('oh no')), completes);
+      }).then((liveTest) {
+        expectTestFailed(liveTest, "oh no");
+      });
+    });
+
+    test("with a non-function", () {
+      return runTest(() {
+        expect(10, completes);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            "Expected: completes successfully\n"
+            "  Actual: <10>\n");
+      });
+    });
+
+    test("with a successful future", () {
+      expect(new Future.value('1'), completes);
+    });
+  });
+
+  group("[completion]", () {
+    test("blocks the test until the Future completes", () {
+      return expectTestBlocks(() {
+        var completer = new Completer();
+        expect(completer.future, completion(isNull));
+        return completer;
+      }, (completer) => completer.complete());
+    });
+
+    test("with an error", () {
+      return runTest(() {
+        expect(new Future.error('X'), completion(isNull));
+      }).then((liveTest) {
+        expectTestFailed(liveTest, startsWith(
+            "Expected future to complete successfully, but it failed with X"));
+      });
+    });
+
+    test("with a failure", () {
+      return runTest(() {
+        expect(new Future.error(new TestFailure('oh no')), completion(isNull));
+      }).then((liveTest) {
+        expectTestFailed(liveTest, "oh no");
+      });
+    });
+
+    test("with a non-function", () {
+      return runTest(() {
+        expect(10, completion(equals(10)));
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            "Expected: completes to a value that <10>\n"
+            "  Actual: <10>\n");
+      });
+    });
+
+    test("with an incorrect value", () {
+      return runTest(() {
+        expect(new Future.value('a'), completion(equals('b')));
+      }).then((liveTest) {
+        expectTestFailed(liveTest, startsWith(
+            "Expected: 'b'\n"
+            "  Actual: 'a'\n"
+            "   Which: is different."));;
+      });
+    });
+  });
+}
diff --git a/test/matcher/prints_test.dart b/test/matcher/prints_test.dart
new file mode 100644
index 00000000..23eea0df
--- /dev/null
+++ b/test/matcher/prints_test.dart
@@ -0,0 +1,149 @@
+// Copyright (c) 2014, 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.
+
+import 'dart:async';
+
+import 'package:unittest/unittest.dart';
+
+import '../utils.dart';
+
+void main() {
+  group("synchronous", () {
+    test("passes with an expected print", () {
+      expect(() => print("Hello, world!"), prints("Hello, world!\n"));
+    });
+
+    test("combines multiple prints", () {
+      expect(() {
+        print("Hello");
+        print("World!");
+      }, prints("Hello\nWorld!\n"));
+    });
+
+    test("works with a Matcher", () {
+      expect(() => print("Hello, world!"), prints(contains("Hello")));
+    });
+
+    test("describes a failure nicely", () {
+      return runTest(() {
+        expect(() => print("Hello, world!"), prints("Goodbye, world!\n"));
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            "Expected: prints 'Goodbye, world!\\n'\n"
+            "  ''\n"
+            "  Actual: <Closure: () => dynamic>\n"
+            "   Which: printed 'Hello, world!\\n'\n"
+            "  ''\n"
+            "   Which: is different.\n"
+            "Expected: Goodbye, w ...\n"
+            "  Actual: Hello, wor ...\n"
+            "          ^\n"
+            " Differ at offset 0\n");
+      });
+    });
+
+    test("describes a failure with a non-descriptive Matcher nicely", () {
+      return runTest(() {
+        expect(() => print("Hello, world!"), prints(contains("Goodbye")));
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            "Expected: prints contains 'Goodbye'\n"
+            "  Actual: <Closure: () => dynamic>\n"
+            "   Which: printed 'Hello, world!\\n'\n"
+            "  ''\n");
+      });
+    });
+
+    test("describes a failure with no text nicely", () {
+      return runTest(() {
+        expect(() {}, prints(contains("Goodbye")));
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            "Expected: prints contains 'Goodbye'\n"
+            "  Actual: <Closure: () => dynamic>\n"
+            "   Which: printed nothing.\n");
+      });
+    });
+
+    test("with a non-function", () {
+      return runTest(() {
+        expect(10, prints(contains("Goodbye")));
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            "Expected: prints contains 'Goodbye'\n"
+            "  Actual: <10>\n");
+      });
+    });
+  });
+
+  group('asynchronous', () {
+    test("passes with an expected print", () {
+      expect(() => new Future(() => print("Hello, world!")),
+          prints("Hello, world!\n"));
+    });
+
+    test("combines multiple prints", () {
+      expect(() => new Future(() {
+        print("Hello");
+        print("World!");
+      }), prints("Hello\nWorld!\n"));
+    });
+
+    test("works with a Matcher", () {
+      expect(() => new Future(() => print("Hello, world!")),
+          prints(contains("Hello")));
+    });
+
+    test("describes a failure nicely", () {
+      return runTest(() {
+        expect(() => new Future(() => print("Hello, world!")),
+            prints("Goodbye, world!\n"));
+      }).then((liveTest) {
+        expectTestFailed(liveTest, startsWith(
+            "Expected: prints 'Goodbye, world!\\n'\n"
+            "  ''\n"
+            "  Actual: <Closure: () => dynamic>\n"
+            "   Which: printed 'Hello, world!\\n'\n"
+            "  ''\n"
+            "   Which: is different.\n"
+            "Expected: Goodbye, w ...\n"
+            "  Actual: Hello, wor ...\n"
+            "          ^\n"
+            " Differ at offset 0"));
+      });
+    });
+
+    test("describes a failure with a non-descriptive Matcher nicely", () {
+      return runTest(() {
+        expect(() => new Future(() => print("Hello, world!")),
+            prints(contains("Goodbye")));
+      }).then((liveTest) {
+        expectTestFailed(liveTest, startsWith(
+            "Expected: prints contains 'Goodbye'\n"
+            "  Actual: <Closure: () => dynamic>\n"
+            "   Which: printed 'Hello, world!\\n'\n"
+            "  ''"));
+      });
+    });
+
+    test("describes a failure with no text nicely", () {
+      return runTest(() {
+        expect(() => new Future.value(), prints(contains("Goodbye")));
+      }).then((liveTest) {
+        expectTestFailed(liveTest, startsWith(
+            "Expected: prints contains 'Goodbye'\n"
+            "  Actual: <Closure: () => dynamic>\n"
+            "   Which: printed nothing."));
+      });
+    });
+
+    test("won't let the test end until the Future completes", () {
+      return expectTestBlocks(() {
+        var completer = new Completer();
+        expect(() => completer.future, prints(isEmpty));
+        return completer;
+      }, (completer) => completer.complete());
+    });
+  });
+}
diff --git a/test/matcher/throws_test.dart b/test/matcher/throws_test.dart
new file mode 100644
index 00000000..ef0ff49e
--- /dev/null
+++ b/test/matcher/throws_test.dart
@@ -0,0 +1,149 @@
+// Copyright (c) 2015, 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.
+
+import 'dart:async';
+
+import 'package:unittest/unittest.dart';
+
+import '../utils.dart';
+
+void main() {
+  group("synchronous", () {
+    group("[throws]", () {
+      test("with a function that throws an error", () {
+        expect(() => throw 'oh no', throws);
+      });
+
+      test("with a function that doesn't throw", () {
+        return runTest(() {
+          expect(() {}, throws);
+        }).then((liveTest) {
+          expectTestFailed(liveTest,
+              "Expected: throws\n"
+              "  Actual: <Closure: () => dynamic>\n"
+              "   Which: did not throw\n");
+        });
+      });
+
+      test("with a non-function", () {
+        return runTest(() {
+          expect(10, throws);
+        }).then((liveTest) {
+          expectTestFailed(liveTest,
+              "Expected: throws\n"
+              "  Actual: <10>\n"
+              "   Which: is not a Function or Future\n");
+        });
+      });
+    });
+
+    group("[throwsA]", () {
+      test("with a function that throws an identical error", () {
+        expect(() => throw 'oh no', throwsA('oh no'));
+      });
+
+      test("with a function that throws a matching error", () {
+        expect(() => throw new FormatException("bad"),
+            throwsA(isFormatException));
+      });
+
+      test("with a function that doesn't throw", () {
+        return runTest(() {
+          expect(() {}, throwsA('oh no'));
+        }).then((liveTest) {
+          expectTestFailed(liveTest,
+              "Expected: throws 'oh no'\n"
+              "  Actual: <Closure: () => dynamic>\n"
+              "   Which: did not throw\n");
+        });
+      });
+
+      test("with a non-function", () {
+        return runTest(() {
+          expect(10, throwsA('oh no'));
+        }).then((liveTest) {
+          expectTestFailed(liveTest,
+              "Expected: throws 'oh no'\n"
+              "  Actual: <10>\n"
+              "   Which: is not a Function or Future\n");
+        });
+      });
+
+      test("with a function that throws the wrong error", () {
+        return runTest(() {
+          expect(() => throw 'aw dang', throwsA('oh no'));
+        }).then((liveTest) {
+          expectTestFailed(liveTest,
+              "Expected: throws 'oh no'\n"
+              "  Actual: <Closure: () => dynamic>\n"
+              "   Which: threw 'aw dang'\n");
+        });
+      });
+    });
+  });
+
+  group("asynchronous", () {
+    group("[throws]", () {
+      test("with a Future that throws an error", () {
+        expect(new Future.error('oh no'), throws);
+      });
+
+      test("with a Future that doesn't throw", () {
+        return runTest(() {
+          expect(new Future.value(), throws);
+        }).then((liveTest) {
+          expectTestFailed(liveTest,
+              "Expected future to fail, but succeeded with 'null'.");
+        });
+      });
+
+      test("won't let the test end until the Future completes", () {
+        return expectTestBlocks(() {
+          var completer = new Completer();
+          expect(completer.future, throws);
+          return completer;
+        }, (completer) => completer.completeError('oh no'));
+      });
+    });
+
+    group("[throwsA]", () {
+      test("with a Future that throws an identical error", () {
+        expect(new Future.error('oh no'), throwsA('oh no'));
+      });
+
+      test("with a Future that throws a matching error", () {
+        expect(new Future.error(new FormatException("bad")),
+            throwsA(isFormatException));
+      });
+
+      test("with a Future that doesn't throw", () {
+        return runTest(() {
+          expect(new Future.value(), throwsA('oh no'));
+        }).then((liveTest) {
+          expectTestFailed(liveTest,
+              "Expected future to fail, but succeeded with 'null'.");
+        });
+      });
+
+      test("with a Future that throws the wrong error", () {
+        return runTest(() {
+          expect(new Future.error('aw dang'), throwsA('oh no'));
+        }).then((liveTest) {
+          expectTestFailed(liveTest, startsWith(
+              "Expected: throws 'oh no'\n"
+              "  Actual: <Closure: () => dynamic>\n"
+              "   Which: threw 'aw dang'\n"));
+        });
+      });
+
+      test("won't let the test end until the Future completes", () {
+        return expectTestBlocks(() {
+          var completer = new Completer();
+          expect(completer.future, throwsA('oh no'));
+          return completer;
+        }, (completer) => completer.completeError('oh no'));
+      });
+    });
+  });
+}
diff --git a/test/matcher/throws_type_test.dart b/test/matcher/throws_type_test.dart
new file mode 100644
index 00000000..0e9a2cef
--- /dev/null
+++ b/test/matcher/throws_type_test.dart
@@ -0,0 +1,178 @@
+// Copyright (c) 2015, 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.
+
+import 'package:unittest/unittest.dart';
+
+import '../utils.dart';
+
+void main() {
+  group('[throwsArgumentError]', () {
+    test("passes when a ArgumentError is thrown", () {
+      expect(() => throw new ArgumentError(''), throwsArgumentError);
+    });
+
+    test("fails when a non-ArgumentError is thrown", () {
+      return runTest(() {
+        expect(() => throw new Exception(), throwsArgumentError);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            startsWith("Expected: throws ArgumentError"));
+      });
+    });
+  });
+
+  group('[throwsConcurrentModificationError]', () {
+    test("passes when a ConcurrentModificationError is thrown", () {
+      expect(() => throw new ConcurrentModificationError(''),
+          throwsConcurrentModificationError);
+    });
+
+    test("fails when a non-ConcurrentModificationError is thrown", () {
+      return runTest(() {
+        expect(() => throw new Exception(), throwsConcurrentModificationError);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            startsWith("Expected: throws ConcurrentModificationError"));
+      });
+    });
+  });
+
+  group('[throwsCyclicInitializationError]', () {
+    test("passes when a CyclicInitializationError is thrown", () {
+      expect(() => throw new CyclicInitializationError(''),
+          throwsCyclicInitializationError);
+    });
+
+    test("fails when a non-CyclicInitializationError is thrown", () {
+      return runTest(() {
+        expect(() => throw new Exception(), throwsCyclicInitializationError);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            startsWith("Expected: throws CyclicInitializationError"));
+      });
+    });
+  });
+
+  group('[throwsException]', () {
+    test("passes when a Exception is thrown", () {
+      expect(() => throw new Exception(''), throwsException);
+    });
+
+    test("fails when a non-Exception is thrown", () {
+      return runTest(() {
+        expect(() => throw 'oh no', throwsException);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            startsWith("Expected: throws Exception"));
+      });
+    });
+  });
+
+  group('[throwsFormatException]', () {
+    test("passes when a FormatException is thrown", () {
+      expect(() => throw new FormatException(''), throwsFormatException);
+    });
+
+    test("fails when a non-FormatException is thrown", () {
+      return runTest(() {
+        expect(() => throw new Exception(), throwsFormatException);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            startsWith("Expected: throws FormatException"));
+      });
+    });
+  });
+
+  group('[throwsNoSuchMethodError]', () {
+    test("passes when a NoSuchMethodError is thrown", () {
+      expect(() {
+        throw new NoSuchMethodError(null, #name, null, null);
+      }, throwsNoSuchMethodError);
+    });
+
+    test("fails when a non-NoSuchMethodError is thrown", () {
+      return runTest(() {
+        expect(() => throw new Exception(), throwsNoSuchMethodError);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            startsWith("Expected: throws NoSuchMethodError"));
+      });
+    });
+  });
+
+  group('[throwsNullThrownError]', () {
+    test("passes when a NullThrownError is thrown", () {
+      expect(() => throw null, throwsNullThrownError);
+    });
+
+    test("fails when a non-NullThrownError is thrown", () {
+      return runTest(() {
+        expect(() => throw new Exception(), throwsNullThrownError);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            startsWith("Expected: throws NullThrownError"));
+      });
+    });
+  });
+
+  group('[throwsRangeError]', () {
+    test("passes when a RangeError is thrown", () {
+      expect(() => throw new RangeError(''), throwsRangeError);
+    });
+
+    test("fails when a non-RangeError is thrown", () {
+      return runTest(() {
+        expect(() => throw new Exception(), throwsRangeError);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            startsWith("Expected: throws RangeError"));
+      });
+    });
+  });
+
+  group('[throwsStateError]', () {
+    test("passes when a StateError is thrown", () {
+      expect(() => throw new StateError(''), throwsStateError);
+    });
+
+    test("fails when a non-StateError is thrown", () {
+      return runTest(() {
+        expect(() => throw new Exception(), throwsStateError);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            startsWith("Expected: throws StateError"));
+      });
+    });
+  });
+
+  group('[throwsUnimplementedError]', () {
+    test("passes when a UnimplementedError is thrown", () {
+      expect(() => throw new UnimplementedError(''), throwsUnimplementedError);
+    });
+
+    test("fails when a non-UnimplementedError is thrown", () {
+      return runTest(() {
+        expect(() => throw new Exception(), throwsUnimplementedError);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            startsWith("Expected: throws UnimplementedError"));
+      });
+    });
+  });
+
+  group('[throwsUnsupportedError]', () {
+    test("passes when a UnsupportedError is thrown", () {
+      expect(() => throw new UnsupportedError(''), throwsUnsupportedError);
+    });
+
+    test("fails when a non-UnsupportedError is thrown", () {
+      return runTest(() {
+        expect(() => throw new Exception(), throwsUnsupportedError);
+      }).then((liveTest) {
+        expectTestFailed(liveTest,
+            startsWith("Expected: throws UnsupportedError"));
+      });
+    });
+  });
+}
diff --git a/test/matchers_minified_test.dart b/test/matchers_minified_test.dart
deleted file mode 100644
index 5c38d4cc..00000000
--- a/test/matchers_minified_test.dart
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright (c) 2012, 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.
-
-// This file is for matcher tests that rely on the names of various Dart types.
-// These tests normally fail when run in minified dart2js, since the names will
-// be mangled. This version of the file is modified to expect minified names.
-
-library unittest.minified_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'test_utils.dart';
-
-// A regexp fragment matching a minified name.
-const _MINIFIED_NAME = r"[A-Za-z0-9]{1,3}";
-
-void main() {
-  group('Core matchers', () {
-    test('throwsFormatException', () {
-      shouldPass(() {
-        throw new FormatException('');
-      }, throwsFormatException);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsFormatException, matches(r"Expected: throws FormatException +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw " + _MINIFIED_NAME + r":<Exception>"));
-    });
-
-    test('throwsArgumentError', () {
-      shouldPass(() {
-        throw new ArgumentError('');
-      }, throwsArgumentError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsArgumentError, matches(r"Expected: throws ArgumentError +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw " + _MINIFIED_NAME + r":<Exception>"));
-    });
-
-    test('throwsRangeError', () {
-      shouldPass(() {
-        throw new RangeError(0);
-      }, throwsRangeError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsRangeError, matches(r"Expected: throws RangeError +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw " + _MINIFIED_NAME + r":<Exception>"));
-    });
-
-    test('throwsNoSuchMethodError', () {
-      shouldPass(() {
-        throw new NoSuchMethodError(null, const Symbol(''), null, null);
-      }, throwsNoSuchMethodError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsNoSuchMethodError, matches(
-          r"Expected: throws NoSuchMethodError +"
-              r"Actual: <Closure(: \(\) => dynamic)?> +"
-              r"Which: threw " + _MINIFIED_NAME + r":<Exception>"));
-    });
-
-    test('throwsUnimplementedError', () {
-      shouldPass(() {
-        throw new UnimplementedError('');
-      }, throwsUnimplementedError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsUnimplementedError, matches(
-          r"Expected: throws UnimplementedError +"
-              r"Actual: <Closure(: \(\) => dynamic)?> +"
-              r"Which: threw " + _MINIFIED_NAME + r":<Exception>"));
-    });
-
-    test('throwsUnsupportedError', () {
-      shouldPass(() {
-        throw new UnsupportedError('');
-      }, throwsUnsupportedError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsUnsupportedError, matches(r"Expected: throws UnsupportedError +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw " + _MINIFIED_NAME + r":<Exception>"));
-    });
-
-    test('throwsStateError', () {
-      shouldPass(() {
-        throw new StateError('');
-      }, throwsStateError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsStateError, matches(r"Expected: throws StateError +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw " + _MINIFIED_NAME + r":<Exception>"));
-    });
-  });
-
-  group('Iterable Matchers', () {
-    test('isEmpty', () {
-      var d = new SimpleIterable(0);
-      var e = new SimpleIterable(1);
-      shouldPass(d, isEmpty);
-      shouldFail(e, isEmpty,
-          matches(r"Expected: empty +Actual: " + _MINIFIED_NAME + r":\[1\]"));
-    });
-
-    test('isNotEmpty', () {
-      var d = new SimpleIterable(0);
-      var e = new SimpleIterable(1);
-      shouldPass(e, isNotEmpty);
-      shouldFail(d, isNotEmpty, matches(
-          r"Expected: non-empty +Actual: " + _MINIFIED_NAME + r":\[\]"));
-    });
-
-    test('contains', () {
-      var d = new SimpleIterable(3);
-      shouldPass(d, contains(2));
-      shouldFail(d, contains(5), matches(r"Expected: contains <5> +"
-          r"Actual: " + _MINIFIED_NAME + r":\[3, 2, 1\]"));
-    });
-  });
-
-  group('Feature Matchers', () {
-    test("Feature Matcher", () {
-      var w = new Widget();
-      w.price = 10;
-      shouldPass(w, new HasPrice(10));
-      shouldPass(w, new HasPrice(greaterThan(0)));
-      shouldFail(w, new HasPrice(greaterThan(10)), matches(
-          r"Expected: Widget with a price that is a value greater than "
-              r"<10> +"
-              r"Actual: <Instance of '" + _MINIFIED_NAME + r"'> +"
-              r"Which: has price with value <10> which is not "
-              r"a value greater than <10>"));
-    });
-  });
-}
diff --git a/test/matchers_unminified_test.dart b/test/matchers_unminified_test.dart
deleted file mode 100644
index f301dd14..00000000
--- a/test/matchers_unminified_test.dart
+++ /dev/null
@@ -1,136 +0,0 @@
-// Copyright (c) 2012, 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.
-
-// This file is for matcher tests that rely on the names of various Dart types.
-// These tests will fail when run in minified dart2js, since the names will be
-// mangled. A version of this file that works in minified dart2js is in
-// matchers_minified_test.dart.
-
-library unittest.unminified_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'test_utils.dart';
-
-void main() {
-  group('Core matchers', () {
-    test('throwsFormatException', () {
-      shouldPass(() {
-        throw new FormatException('');
-      }, throwsFormatException);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsFormatException, matches(r"Expected: throws FormatException +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw \?:<Exception>"));
-    });
-
-    test('throwsArgumentError', () {
-      shouldPass(() {
-        throw new ArgumentError('');
-      }, throwsArgumentError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsArgumentError, matches(r"Expected: throws ArgumentError +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw \?:<Exception>"));
-    });
-
-    test('throwsRangeError', () {
-      shouldPass(() {
-        throw new RangeError(0);
-      }, throwsRangeError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsRangeError, matches(r"Expected: throws RangeError +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw \?:<Exception>"));
-    });
-
-    test('throwsNoSuchMethodError', () {
-      shouldPass(() {
-        throw new NoSuchMethodError(null, const Symbol(''), null, null);
-      }, throwsNoSuchMethodError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsNoSuchMethodError, matches(
-          r"Expected: throws NoSuchMethodError +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw \?:<Exception>"));
-    });
-
-    test('throwsUnimplementedError', () {
-      shouldPass(() {
-        throw new UnimplementedError('');
-      }, throwsUnimplementedError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsUnimplementedError, matches(
-          r"Expected: throws UnimplementedError +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw \?:<Exception>"));
-    });
-
-    test('throwsUnsupportedError', () {
-      shouldPass(() {
-        throw new UnsupportedError('');
-      }, throwsUnsupportedError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsUnsupportedError, matches(r"Expected: throws UnsupportedError +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw \?:<Exception>"));
-    });
-
-    test('throwsStateError', () {
-      shouldPass(() {
-        throw new StateError('');
-      }, throwsStateError);
-      shouldFail(() {
-        throw new Exception();
-      }, throwsStateError, matches(r"Expected: throws StateError +"
-          r"Actual: <Closure(: \(\) => dynamic)?> +"
-          r"Which: threw \?:<Exception>"));
-    });
-  });
-
-  group('Iterable Matchers', () {
-    test('isEmpty', () {
-      var d = new SimpleIterable(0);
-      var e = new SimpleIterable(1);
-      shouldPass(d, isEmpty);
-      shouldFail(e, isEmpty, "Expected: empty "
-          "Actual: SimpleIterable:[1]");
-    });
-
-    test('isNotEmpty', () {
-      var d = new SimpleIterable(0);
-      var e = new SimpleIterable(1);
-      shouldPass(e, isNotEmpty);
-      shouldFail(d, isNotEmpty, "Expected: non-empty "
-          "Actual: SimpleIterable:[]");
-    });
-
-    test('contains', () {
-      var d = new SimpleIterable(3);
-      shouldPass(d, contains(2));
-      shouldFail(d, contains(5), "Expected: contains <5> "
-          "Actual: SimpleIterable:[3, 2, 1]");
-    });
-  });
-
-  group('Feature Matchers', () {
-    test("Feature Matcher", () {
-      var w = new Widget();
-      w.price = 10;
-      shouldPass(w, new HasPrice(10));
-      shouldPass(w, new HasPrice(greaterThan(0)));
-      shouldFail(w, new HasPrice(greaterThan(10)),
-          "Expected: Widget with a price that is a value greater than <10> "
-          "Actual: <Instance of 'Widget'> "
-          "Which: has price with value <10> which is not "
-          "a value greater than <10>");
-    });
-  });
-}
diff --git a/test/middle_exception_test.dart b/test/middle_exception_test.dart
deleted file mode 100644
index 1abd02e2..00000000
--- a/test/middle_exception_test.dart
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.middle_exception_test;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('late exception test', () {
-    test('testOne', () {
-      expect(true, isTrue);
-    });
-    test('testTwo', () {
-      expect(true, isFalse);
-    });
-    test('testThree', () {
-      var done = expectAsync(() {});
-      new Future.sync(() {
-        expect(true, isTrue);
-        done();
-      });
-    });
-  }, [{'result': 'pass'}, {'result': 'fail',}, {'result': 'pass'}]);
-}
diff --git a/test/missing_tick_test.dart b/test/missing_tick_test.dart
deleted file mode 100644
index 81b03315..00000000
--- a/test/missing_tick_test.dart
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.missing_tick_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message, timeout: const Duration(seconds: 1));
-
-  expectTestResults('missing tick', () {
-    test('test that should time out', () {
-      expectAsync(() {});
-    });
-  }, [
-    {
-      'description': 'test that should time out',
-      'message': 'Test timed out after 1 seconds.',
-      'result': 'error',
-    }
-  ]);
-}
diff --git a/test/nested_groups_setup_teardown_test.dart b/test/nested_groups_setup_teardown_test.dart
deleted file mode 100644
index ca09ec62..00000000
--- a/test/nested_groups_setup_teardown_test.dart
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.nested_groups_setup_teardown_test;
-
-import 'dart:async';
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsPass('nested groups setup/teardown', () {
-    StringBuffer s = new StringBuffer();
-    group('level 1', () {
-      setUp(makeDelayedSetup(1, s));
-      group('level 2', () {
-        setUp(makeImmediateSetup(2, s));
-        tearDown(makeDelayedTeardown(2, s));
-        group('level 3', () {
-          group('level 4', () {
-            setUp(makeDelayedSetup(4, s));
-            tearDown(makeImmediateTeardown(4, s));
-            group('level 5', () {
-              setUp(makeImmediateSetup(5, s));
-              group('level 6', () {
-                tearDown(makeDelayedTeardown(6, s));
-                test('inner', () {});
-              });
-            });
-          });
-        });
-      });
-    });
-    test('after nest', () {
-      expect(s.toString(), "l1 U l2 U l4 U l5 U l6 D l4 D l2 D ");
-    });
-  });
-}
-
-Function makeDelayedSetup(int index, StringBuffer s) => () {
-  return new Future.delayed(new Duration(milliseconds: 1), () {
-    s.write('l$index U ');
-  });
-};
-
-Function makeDelayedTeardown(int index, StringBuffer s) => () {
-  return new Future.delayed(new Duration(milliseconds: 1), () {
-    s.write('l$index D ');
-  });
-};
-
-Function makeImmediateSetup(int index, StringBuffer s) => () {
-  s.write('l$index U ');
-};
-
-Function makeImmediateTeardown(int index, StringBuffer s) => () {
-  s.write('l$index D ');
-};
diff --git a/test/prints_matcher_test.dart b/test/prints_matcher_test.dart
deleted file mode 100644
index cdce9df0..00000000
--- a/test/prints_matcher_test.dart
+++ /dev/null
@@ -1,144 +0,0 @@
-// Copyright (c) 2014, 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.
-
-library unittest.prints_matchers_test;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-/// The VM and dart2js have different toStrings for closures.
-final closureToString = (() {}).toString();
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('synchronous', () {
-    test("passes with an expected print", () {
-      expect(() => print("Hello, world!"), prints("Hello, world!\n"));
-    });
-
-    test("combines multiple prints", () {
-      expect(() {
-        print("Hello");
-        print("World!");
-      }, prints("Hello\nWorld!\n"));
-    });
-
-    test("works with a Matcher", () {
-      expect(() => print("Hello, world!"), prints(contains("Hello")));
-    });
-
-    test("describes a failure nicely", () {
-      expect(() => print("Hello, world!"), prints("Goodbye, world!\n"));
-    });
-
-    test("describes a failure with a non-descriptive Matcher nicely", () {
-      expect(() => print("Hello, world!"), prints(contains("Goodbye")));
-    });
-
-    test("describes a failure with no text nicely", () {
-      expect(() {}, prints(contains("Goodbye")));
-    });
-  }, [
-    {'result': 'pass'},
-    {'result': 'pass'},
-    {'result': 'pass'},
-    {
-      'result': 'fail',
-      'message': r'''Expected: prints 'Goodbye, world!\n'
-  ''
-  Actual: <Closure: () => dynamic>
-   Which: printed 'Hello, world!\n'
-  ''
-   Which: is different.
-Expected: Goodbye, w ...
-  Actual: Hello, wor ...
-          ^
- Differ at offset 0
-'''
-    },
-    {
-      'result': 'fail',
-      'message': r'''Expected: prints contains 'Goodbye'
-  Actual: <Closure: () => dynamic>
-   Which: printed 'Hello, world!\n'
-  ''
-'''
-    },
-    {
-      'result': 'fail',
-      'message': r'''Expected: prints contains 'Goodbye'
-  Actual: <Closure: () => dynamic>
-   Which: printed nothing.
-'''
-    }
-  ]);
-
-  expectTestResults('asynchronous', () {
-    test("passes with an expected print", () {
-      expect(() => new Future(() => print("Hello, world!")),
-          prints("Hello, world!\n"));
-    });
-
-    test("combines multiple prints", () {
-      expect(() => new Future(() {
-        print("Hello");
-        print("World!");
-      }), prints("Hello\nWorld!\n"));
-    });
-
-    test("works with a Matcher", () {
-      expect(() => new Future(() => print("Hello, world!")),
-          prints(contains("Hello")));
-    });
-
-    test("describes a failure nicely", () {
-      expect(() => new Future(() => print("Hello, world!")),
-          prints("Goodbye, world!\n"));
-    });
-
-    test("describes a failure with a non-descriptive Matcher nicely", () {
-      expect(() => new Future(() => print("Hello, world!")),
-          prints(contains("Goodbye")));
-    });
-
-    test("describes a failure with no text nicely", () {
-      expect(() => new Future.value(), prints(contains("Goodbye")));
-    });
-  }, [
-    {'result': 'pass'},
-    {'result': 'pass'},
-    {'result': 'pass'},
-    {
-      'result': 'fail',
-      'message': startsWith(r'''Expected future to complete successfully, but it failed with Expected: 'Goodbye, world!\n'
-  ''
-  Actual: 'Hello, world!\n'
-  ''
-   Which: is different.
-Expected: Goodbye, w ...
-  Actual: Hello, wor ...
-          ^
- Differ at offset 0
-''')
-    },
-    {
-      'result': 'fail',
-      'message': startsWith(r'''Expected future to complete successfully, but it failed with Expected: contains 'Goodbye'
-  Actual: 'Hello, world!\n'
-  ''
-''')
-    },
-    {
-      'result': 'fail',
-      'message': startsWith(r'''Expected future to complete successfully, but it failed with Expected: contains 'Goodbye'
-  Actual: ''
-''')
-    }
-  ]);
-}
diff --git a/test/protect_async_test.dart b/test/protect_async_test.dart
deleted file mode 100644
index 19211f2b..00000000
--- a/test/protect_async_test.dart
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.protect_async_test;
-
-import 'dart:async';
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('protectAsync', () {
-    test('protectAsync0', () {
-      var protected = () {
-        throw new StateError('error during protectAsync0');
-      };
-      new Future(protected);
-    });
-
-    test('protectAsync1', () {
-      var protected = (arg) {
-        throw new StateError('error during protectAsync1: $arg');
-      };
-      new Future(() => protected('one arg'));
-    });
-
-    test('protectAsync2', () {
-      var protected = (arg1, arg2) {
-        throw new StateError('error during protectAsync2: $arg1, $arg2');
-      };
-      new Future(() => protected('arg1', 'arg2'));
-    });
-
-    test('throw away 1', () {
-      return new Future(() {});
-    });
-  }, [
-    {
-      'result': 'error',
-      'message': 'Caught Bad state: error during protectAsync0'
-    },
-    {
-      'result': 'error',
-      'message': 'Caught Bad state: error during protectAsync1: one arg'
-    },
-    {
-      'result': 'error',
-      'message': 'Caught Bad state: error during protectAsync2: arg1, arg2'
-    },
-    {'result': 'pass', 'message': ''}
-  ]);
-}
diff --git a/test/returning_future_test.dart b/test/returning_future_test.dart
deleted file mode 100644
index d59d8c1b..00000000
--- a/test/returning_future_test.dart
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.returning_future_test;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('returning futures', () {
-    test("successful", () {
-      return new Future.sync(() {
-        expect(true, true);
-      });
-    });
-    // We repeat the fail and error tests, because during development
-    // I had a situation where either worked fine on their own, and
-    // error/fail worked, but fail/error would time out.
-    test("error1", () {
-      var callback = expectAsync(() {});
-      var excesscallback = expectAsync(() {});
-      return new Future.sync(() {
-        excesscallback();
-        excesscallback();
-        excesscallback();
-        callback();
-      });
-    });
-    test("fail1", () {
-      return new Future.sync(() {
-        expect(true, false);
-      });
-    });
-    test("error2", () {
-      var callback = expectAsync(() {});
-      var excesscallback = expectAsync(() {});
-      return new Future.sync(() {
-        excesscallback();
-        excesscallback();
-        callback();
-      });
-    });
-    test("fail2", () {
-      return new Future.sync(() {
-        fail('failure');
-      });
-    });
-    test('foo5', () {});
-  }, [
-    {'result': 'pass'},
-    {
-      'result': 'fail',
-      'message': 'Callback called more times than expected (1).'
-    },
-    {'result': 'fail', 'message': 'Expected: <false>\n  Actual: <true>\n'},
-    {
-      'result': 'fail',
-      'message': 'Callback called more times than expected (1).'
-    },
-    {'result': 'fail', 'message': 'failure'},
-    {'result': 'pass'}
-  ]);
-}
diff --git a/test/returning_future_using_runasync_test.dart b/test/returning_future_using_runasync_test.dart
deleted file mode 100644
index 3ae13c4d..00000000
--- a/test/returning_future_using_runasync_test.dart
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.returning_future_using_runasync_test;
-
-import 'dart:async';
-
-import 'package:metatest/metatest.dart';
-import 'package:unittest/unittest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('test returning future using scheduleMicrotask', () {
-    test("successful", () {
-      return new Future.sync(() {
-        scheduleMicrotask(() {
-          expect(true, true);
-        });
-      });
-    });
-    test("fail1", () {
-      var callback = expectAsync(() {});
-      return new Future.sync(() {
-        scheduleMicrotask(() {
-          expect(true, false);
-          callback();
-        });
-      });
-    });
-    test('error1', () {
-      var callback = expectAsync(() {});
-      var excesscallback = expectAsync(() {});
-      return new Future.sync(() {
-        scheduleMicrotask(() {
-          excesscallback();
-          excesscallback();
-          callback();
-        });
-      });
-    });
-    test("fail2", () {
-      var callback = expectAsync(() {});
-      return new Future.sync(() {
-        scheduleMicrotask(() {
-          fail('failure');
-          callback();
-        });
-      });
-    });
-    test('error2', () {
-      var callback = expectAsync(() {});
-      var excesscallback = expectAsync(() {});
-      return new Future.sync(() {
-        scheduleMicrotask(() {
-          excesscallback();
-          excesscallback();
-          excesscallback();
-          callback();
-        });
-      });
-    });
-    test('foo6', () {});
-  }, [
-    {'description': 'successful', 'result': 'pass',},
-    {
-      'description': 'fail1',
-      'message': 'Expected: <false>\n' '  Actual: <true>\n' '',
-      'result': 'fail',
-    },
-    {
-      'description': 'error1',
-      'message': 'Callback called more times than expected (1).',
-      'result': 'fail',
-    },
-    {'description': 'fail2', 'message': 'failure', 'result': 'fail',},
-    {
-      'description': 'error2',
-      'message': 'Callback called more times than expected (1).',
-      'result': 'fail',
-    },
-    {'description': 'foo6', 'result': 'pass',}
-  ]);
-}
diff --git a/test/runner_test.dart b/test/runner_test.dart
index 2b22667f..5dc54392 100644
--- a/test/runner_test.dart
+++ b/test/runner_test.dart
@@ -15,9 +15,10 @@ String _sandbox;
 final _success = """
 import 'dart:async';
 
+import 'package:unittest/unittest.dart';
+
 void main() {
-  var declarer = Zone.current[#unittest.declarer];
-  declarer.test("success", () {});
+  test("success", () {});
 }
 """;
 
@@ -27,8 +28,7 @@ import 'dart:async';
 import 'package:unittest/unittest.dart';
 
 void main() {
-  var declarer = Zone.current[#unittest.declarer];
-  declarer.test("failure", () => throw new TestFailure("oh no"));
+  test("failure", () => throw new TestFailure("oh no"));
 }
 """;
 
@@ -167,6 +167,15 @@ Usage: pub run unittest:unittest [files or directories...]
       var result = _runUnittest([]);
       expect(result.exitCode, equals(0));
     });
+
+    test("directly", () {
+      new File(p.join(_sandbox, "test.dart")).writeAsStringSync(_success);
+      var result = _runDart([
+        "--package-root=${p.join(packageDir, 'packages')}",
+        "test.dart"
+      ]);
+      expect(result.stdout, contains("All tests passed!"));
+    });
   });
 
   group("runs failing tests", () {
@@ -196,8 +205,20 @@ Usage: pub run unittest:unittest [files or directories...]
       var result = _runUnittest([]);
       expect(result.exitCode, equals(1));
     });
+
+    test("directly", () {
+      new File(p.join(_sandbox, "test.dart")).writeAsStringSync(_failure);
+      var result = _runDart([
+        "--package-root=${p.join(packageDir, 'packages')}",
+        "test.dart"
+      ]);
+      expect(result.stdout, contains("Some tests failed."));
+    });
   });
 }
 
 ProcessResult _runUnittest(List<String> args) =>
     runUnittest(args, workingDirectory: _sandbox);
+
+ProcessResult _runDart(List<String> args) =>
+    runDart(args, workingDirectory: _sandbox);
diff --git a/test/runtests_without_tests_test.dart b/test/runtests_without_tests_test.dart
deleted file mode 100644
index 4257ddaa..00000000
--- a/test/runtests_without_tests_test.dart
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.runtests_without_tests;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('runTests() without tests', () {
-    group('no tests', () {});
-  }, []);
-}
diff --git a/test/setup_and_teardown_test.dart b/test/setup_and_teardown_test.dart
deleted file mode 100644
index 37121370..00000000
--- a/test/setup_and_teardown_test.dart
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.setup_and_teardown_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsPass('setup and teardown test', () {
-    var hasSetup = false;
-    var hasTeardown = false;
-
-    group('a', () {
-      setUp(() {
-        hasSetup = true;
-      });
-      tearDown(() {
-        hasTeardown = true;
-      });
-      test('test', () {});
-    });
-
-    test('verify', () {
-      expect(hasSetup, isTrue);
-      expect(hasTeardown, isTrue);
-    });
-  });
-}
diff --git a/test/setup_test.dart b/test/setup_test.dart
deleted file mode 100644
index ba27b443..00000000
--- a/test/setup_test.dart
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.setup_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsPass('setup test', () {
-    group('a', () {
-      var hasSetup = false;
-      setUp(() {
-        hasSetup = true;
-      });
-      test('test', () {
-        expect(hasSetup, isTrue);
-      });
-    });
-  });
-}
diff --git a/test/single_correct_test.dart b/test/single_correct_test.dart
deleted file mode 100644
index a6b41558..00000000
--- a/test/single_correct_test.dart
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.single_correct_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsPass('single correct test', () {
-    test('test', () => expect(2 + 3, equals(5)));
-  });
-}
diff --git a/test/single_failing_test.dart b/test/single_failing_test.dart
deleted file mode 100644
index e9c1819b..00000000
--- a/test/single_failing_test.dart
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.single_failing_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsFail('single failing test', () {
-    test('test', () => expect(2 + 2, equals(5)));
-  });
-}
diff --git a/test/skipped_soloed_nested_test.dart b/test/skipped_soloed_nested_test.dart
deleted file mode 100644
index 1e590e7c..00000000
--- a/test/skipped_soloed_nested_test.dart
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.skipped_soloed_nested_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestResults('skipped/soloed nested groups with setup/teardown', () {
-    StringBuffer s = null;
-    setUp(() {
-      if (s == null) s = new StringBuffer();
-    });
-    test('top level', () {
-      s.write('A');
-    });
-    skip_test('skipped top level', () {
-      s.write('B');
-    });
-    skip_group('skipped top level group', () {
-      setUp(() {
-        s.write('C');
-      });
-      solo_test('skipped solo nested test', () {
-        s.write('D');
-      });
-    });
-    group('non-solo group', () {
-      setUp(() {
-        s.write('E');
-      });
-      test('in non-solo group', () {
-        s.write('F');
-      });
-      solo_test('solo_test in non-solo group', () {
-        s.write('G');
-      });
-    });
-    solo_group('solo group', () {
-      setUp(() {
-        s.write('H');
-      });
-      test('solo group non-solo test', () {
-        s.write('I');
-      });
-      solo_test('solo group solo test', () {
-        s.write('J');
-      });
-      group('nested non-solo group in solo group', () {
-        test('nested non-solo group non-solo test', () {
-          s.write('K');
-        });
-        solo_test('nested non-solo group solo test', () {
-          s.write('L');
-        });
-      });
-    });
-    solo_test('final', () {
-      expect(s.toString(), "EGHIHJHKHL");
-    });
-  }, [
-    {
-      'description': 'non-solo group solo_test in non-solo group',
-      'result': 'pass',
-    },
-    {'description': 'solo group solo group non-solo test', 'result': 'pass',},
-    {'description': 'solo group solo group solo test', 'result': 'pass',},
-    {
-      'description': 'solo group nested non-solo group in solo group '
-          'nested non-solo group non-solo test',
-      'result': 'pass',
-    },
-    {
-      'description': 'solo group nested non-solo group in solo group '
-          'nested non-solo group solo test',
-      'result': 'pass',
-    },
-    {'description': 'final', 'result': 'pass',}
-  ]);
-}
diff --git a/test/teardown_test.dart b/test/teardown_test.dart
deleted file mode 100644
index b1f6ee09..00000000
--- a/test/teardown_test.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.teardown_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsPass('teardown test', () {
-    var tornDown = false;
-
-    group('a', () {
-      tearDown(() {
-        tornDown = true;
-      });
-      test('test', () {});
-    });
-
-    test('expect teardown', () {
-      expect(tornDown, isTrue);
-    });
-  });
-}
diff --git a/test/test_utils.dart b/test/test_utils.dart
deleted file mode 100644
index f86631bb..00000000
--- a/test/test_utils.dart
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (c) 2012, 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.
-
-library unittest.test_utils;
-
-import 'dart:collection';
-
-import 'package:unittest/unittest.dart';
-
-void shouldFail(value, Matcher matcher, expected) {
-  var failed = false;
-  try {
-    expect(value, matcher);
-  } on TestFailure catch (err) {
-    failed = true;
-
-    var _errorString = err.message;
-
-    if (expected is String) {
-      expect(_errorString, equalsIgnoringWhitespace(expected));
-    } else {
-      expect(_errorString.replaceAll('\n', ''), expected);
-    }
-  }
-
-  expect(failed, isTrue, reason: 'Expected to fail.');
-}
-
-void shouldPass(value, Matcher matcher) {
-  expect(value, matcher);
-}
-
-class Widget {
-  int price;
-}
-
-class HasPrice extends CustomMatcher {
-  HasPrice(matcher) : super("Widget with a price that is", "price", matcher);
-  featureValueOf(actual) => actual.price;
-}
-
-class SimpleIterable extends IterableBase<int> {
-  final int count;
-
-  SimpleIterable(this.count);
-
-  bool contains(int val) => count < val ? false : true;
-
-  bool any(bool f(element)) {
-    for (var i = 0; i <= count; i++) {
-      if (f(i)) return true;
-    }
-    return false;
-  }
-
-  String toString() => "<[$count]>";
-
-  Iterator get iterator {
-    return new _SimpleIterator(count);
-  }
-}
-
-class _SimpleIterator implements Iterator<int> {
-  int _count;
-  int _current;
-
-  _SimpleIterator(this._count);
-
-  bool moveNext() {
-    if (_count > 0) {
-      _current = _count;
-      _count--;
-      return true;
-    }
-    _current = null;
-    return false;
-  }
-
-  int get current => _current;
-}
diff --git a/test/testcases_immutable_test.dart b/test/testcases_immutable_test.dart
deleted file mode 100644
index c744fa1d..00000000
--- a/test/testcases_immutable_test.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) 2013, 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.
-
-library unittest.testcases_immutable;
-
-import 'package:unittest/unittest.dart';
-
-import 'package:metatest/metatest.dart';
-
-void main() => initTests(_test);
-
-void _test(message) {
-  initMetatest(message);
-
-  expectTestsPass('testcases immutable', () {
-    test('test', () {
-      expect(() => testCases.clear(), throwsUnsupportedError);
-      expect(() => testCases.removeLast(), throwsUnsupportedError);
-    });
-  });
-}
diff --git a/test/throws_matchers_test.dart b/test/throws_matchers_test.dart
deleted file mode 100644
index 3ed8d01a..00000000
--- a/test/throws_matchers_test.dart
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) 2012, 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.
-
-library unittest.core_matchers_test;
-
-import 'package:unittest/unittest.dart';
-
-import 'test_utils.dart';
-
-doesNotThrow() {}
-doesThrow() {
-  throw 'X';
-}
-
-void main() {
-  test('throws', () {
-    shouldFail(doesNotThrow, throws, matches(r"Expected: throws"
-        r"  Actual: <Closure(: \(\) => dynamic "
-        r"from Function 'doesNotThrow': static\.)?>"
-        r"   Which: did not throw"));
-    shouldPass(doesThrow, throws);
-    shouldFail(true, throws, "Expected: throws"
-        "  Actual: <true>"
-        "   Which: is not a Function or Future");
-  });
-
-  test('throwsA', () {
-    shouldPass(doesThrow, throwsA(equals('X')));
-    shouldFail(doesThrow, throwsA(equals('Y')), matches(r"Expected: throws 'Y'"
-        r"  Actual: <Closure(: \(\) => dynamic "
-        r"from Function 'doesThrow': static\.)?>"
-        r"   Which: threw 'X'"));
-  });
-
-  test('throwsA', () {
-    shouldPass(doesThrow, throwsA(equals('X')));
-    shouldFail(doesThrow, throwsA(equals('Y')), matches("Expected: throws 'Y'.*"
-        "Actual: <Closure.*"
-        "Which: threw 'X'"));
-  });
-
-  group('exception/error matchers', () {
-    test('throwsCyclicInitializationError', () {
-      expect(() => _Bicycle.foo, throwsCyclicInitializationError);
-    });
-
-    test('throwsConcurrentModificationError', () {
-      expect(() {
-        var a = {'foo': 'bar'};
-        for (var k in a.keys) {
-          a.remove(k);
-        }
-      }, throwsConcurrentModificationError);
-    });
-
-    test('throwsNullThrownError', () {
-      expect(() => throw null, throwsNullThrownError);
-    });
-  });
-}
-
-class _Bicycle {
-  static final foo = bar();
-
-  static bar() {
-    return foo + 1;
-  }
-}
diff --git a/test/utils.dart b/test/utils.dart
index 64a3da71..d8e3e9de 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -7,10 +7,12 @@ library unittest.test.utils;
 import 'dart:async';
 import 'dart:collection';
 
+import 'package:unittest/src/invoker.dart';
 import 'package:unittest/src/live_test.dart';
 import 'package:unittest/src/load_exception.dart';
 import 'package:unittest/src/remote_exception.dart';
 import 'package:unittest/src/state.dart';
+import 'package:unittest/src/suite.dart';
 import 'package:unittest/unittest.dart';
 
 // The last state change detected via [expectStates].
@@ -63,19 +65,102 @@ void expectSingleError(LiveTest liveTest) {
 }
 
 /// Returns a matcher that matches a [TestFailure] with the given [message].
-Matcher isTestFailure(String message) => predicate(
-    (error) => error is TestFailure && error.message == message,
-    'is a TestFailure with message "$message"');
+///
+/// [message] can be a string or a [Matcher].
+Matcher isTestFailure(message) => new _IsTestFailure(wrapMatcher(message));
+
+class _IsTestFailure extends Matcher {
+  final Matcher _message;
+
+  _IsTestFailure(this._message);
+
+  bool matches(item, Map matchState) =>
+      item is TestFailure && _message.matches(item.message, matchState);
+
+  Description describe(Description description) =>
+      description.add('a TestFailure with message ').addDescriptionOf(_message);
+
+  Description describeMismatch(item, Description mismatchDescription,
+                               Map matchState, bool verbose) {
+    if (item is! TestFailure) {
+      return mismatchDescription.addDescriptionOf(item)
+          .add('is not a TestFailure');
+    } else {
+      return mismatchDescription
+          .add('message ')
+          .addDescriptionOf(item.message)
+          .add(' is not ')
+          .addDescriptionOf(_message);
+    }
+  }
+}
 
 /// Returns a matcher that matches a [RemoteException] with the given [message].
-Matcher isRemoteException(String message) => predicate(
-    (error) => error is RemoteException && error.message == message,
-    'is a RemoteException with message "$message"');
+///
+/// [message] can be a string or a [Matcher].
+Matcher isRemoteException(message) =>
+    new _IsRemoteException(wrapMatcher(message));
+
+class _IsRemoteException extends Matcher {
+  final Matcher _message;
+
+  _IsRemoteException(this._message);
+
+  bool matches(item, Map matchState) =>
+      item is RemoteException && _message.matches(item.message, matchState);
+
+  Description describe(Description description) =>
+      description.add('a RemoteException with message ')
+          .addDescriptionOf(_message);
+
+  Description describeMismatch(item, Description mismatchDescription,
+                               Map matchState, bool verbose) {
+    if (item is! RemoteException) {
+      return mismatchDescription.addDescriptionOf(item)
+          .add('is not a RemoteException');
+    } else {
+      return mismatchDescription
+          .add('message ')
+          .addDescriptionOf(item)
+          .add(' is not ')
+          .addDescriptionOf(_message);
+    }
+  }
+}
+
+/// Returns a matcher that matches a [LoadException] with the given
+/// [innerError].
+///
+/// [innerError] can be a string or a [Matcher].
+Matcher isLoadException(innerError) =>
+    new _IsLoadException(wrapMatcher(innerError));
+
+class _IsLoadException extends Matcher {
+  final Matcher _innerError;
+
+  _IsLoadException(this._innerError);
 
-/// Returns a matcher that matches a [LoadException] with the given [message].
-Matcher isLoadException(String message) => predicate(
-    (error) => error is LoadException && error.innerError == message,
-    'is a LoadException with message "$message"');
+  bool matches(item, Map matchState) =>
+      item is LoadException && _innerError.matches(item.innerError, matchState);
+
+  Description describe(Description description) =>
+      description.add('a LoadException with message ')
+          .addDescriptionOf(_innerError);
+
+  Description describeMismatch(item, Description mismatchDescription,
+                               Map matchState, bool verbose) {
+    if (item is! LoadException) {
+      return mismatchDescription.addDescriptionOf(item)
+          .add('is not a LoadException');
+    } else {
+      return mismatchDescription
+          .add('inner error ')
+          .addDescriptionOf(item)
+          .add(' is not ')
+          .addDescriptionOf(_innerError);
+    }
+  }
+}
 
 /// Returns a [Future] that completes after pumping the event queue [times]
 /// times.
@@ -90,3 +175,67 @@ Future pumpEventQueue([int times=20]) {
   // method.
   return new Future(() => pumpEventQueue(times - 1));
 }
+
+/// Returns a local [LiveTest] that runs [body].
+LiveTest createTest(body()) {
+  var test = new LocalTest("test", body);
+  var suite = new Suite("suite", [test]);
+  return test.load(suite);
+}
+
+/// Runs [body] as a test.
+///
+/// Once it completes, returns the [LiveTest] used to run it.
+Future<LiveTest> runTest(body()) {
+  var liveTest = createTest(body);
+  return liveTest.run().then((_) => liveTest);
+}
+
+/// Asserts that [liveTest] has completed and passed.
+///
+/// If the test had any errors, they're surfaced nicely into the outer test.
+void expectTestPassed(LiveTest liveTest) {
+  // Since the test is expected to pass, we forward any current or future errors
+  // to the outer test, because they're definitely unexpected.
+  for (var error in liveTest.errors) {
+    registerException(error.error, error.stackTrace);
+  }
+  liveTest.onError.listen((error) {
+    registerException(error.error, error.stackTrace);
+  });
+
+  expect(liveTest.state.status, equals(Status.complete));
+  expect(liveTest.state.result, equals(Result.success));
+}
+
+/// Asserts that [liveTest] failed with a single [TestFailure] whose message
+/// matches [message].
+void expectTestFailed(LiveTest liveTest, message) {
+  expect(liveTest.state.status, equals(Status.complete));
+  expect(liveTest.state.result, equals(Result.failure));
+  expect(liveTest.errors, hasLength(1));
+  expect(liveTest.errors.first.error, isTestFailure(message));
+}
+
+/// Assert that the [test] callback causes a test to block until [stopBlocking]
+/// is called at some later time.
+///
+/// [stopBlocking] is passed the return value of [test].
+Future expectTestBlocks(test(), stopBlocking(value)) {
+  var liveTest;
+  var future;
+  liveTest = createTest(() {
+    var value = test();
+    future = pumpEventQueue().then((_) {
+      expect(liveTest.state.status, equals(Status.running));
+      stopBlocking(value);
+    });
+  });
+
+  return liveTest.run().then((_) {
+    expectTestPassed(liveTest);
+    // Ensure that the outer test doesn't complete until the inner future
+    // completes.
+    return future;
+  });
+}
diff --git a/test/vm_listener_test.dart b/test/vm_listener_test.dart
index 47ee330a..f401a0c7 100644
--- a/test/vm_listener_test.dart
+++ b/test/vm_listener_test.dart
@@ -5,7 +5,6 @@
 import 'dart:async';
 import 'dart:isolate';
 
-import 'package:unittest/src/declarer.dart';
 import 'package:unittest/src/invoker.dart';
 import 'package:unittest/src/isolate_test.dart';
 import 'package:unittest/src/live_test.dart';
@@ -17,9 +16,6 @@ import 'package:unittest/unittest.dart';
 
 import 'utils.dart';
 
-/// The current declarer.
-Declarer get _declarer => Zone.current[#unittest.declarer];
-
 /// An isolate that's been spun up for the current test.
 ///
 /// This is tracked so that it can be killed once the test is done.
@@ -312,23 +308,23 @@ void _wrongArity(SendPort sendPort) =>
 /// An isolate entrypoint that defines three tests that succeed.
 void _successfulTests(SendPort sendPort) {
   VmListener.start(sendPort, () => () {
-    _declarer.test("successful 1", () {});
-    _declarer.test("successful 2", () {});
-    _declarer.test("successful 3", () {});
+    test("successful 1", () {});
+    test("successful 2", () {});
+    test("successful 3", () {});
   });
 }
 
 /// An isolate entrypoint that defines a test that fails.
 void _failingTest(SendPort sendPort) {
   VmListener.start(sendPort, () => () {
-    _declarer.test("failure", () => throw new TestFailure('oh no'));
+    test("failure", () => throw new TestFailure('oh no'));
   });
 }
 
 /// An isolate entrypoint that defines a test that fails after succeeding.
 void _failAfterSucceedTest(SendPort sendPort) {
   VmListener.start(sendPort, () => () {
-    _declarer.test("fail after succeed", () {
+    test("fail after succeed", () {
       pumpEventQueue().then((_) {
         throw new TestFailure('oh no');
       });
@@ -339,7 +335,7 @@ void _failAfterSucceedTest(SendPort sendPort) {
 /// An isolate entrypoint that defines a test that fails multiple times.
 void _multiFailTest(SendPort sendPort) {
   VmListener.start(sendPort, () => () {
-    _declarer.test("multiple failures", () {
+    test("multiple failures", () {
       Invoker.current.addOutstandingCallback();
       new Future(() => throw new TestFailure("one"));
       new Future(() => throw new TestFailure("two"));
@@ -352,14 +348,14 @@ void _multiFailTest(SendPort sendPort) {
 /// An isolate entrypoint that defines a test that errors.
 void _errorTest(SendPort sendPort) {
   VmListener.start(sendPort, () => () {
-    _declarer.test("error", () => throw 'oh no');
+    test("error", () => throw 'oh no');
   });
 }
 
 /// An isolate entrypoint that defines a test that errors after succeeding.
 void _errorAfterSucceedTest(SendPort sendPort) {
   VmListener.start(sendPort, () => () {
-    _declarer.test("error after succeed", () {
+    test("error after succeed", () {
       pumpEventQueue().then((_) => throw 'oh no');
     });
   });
@@ -368,7 +364,7 @@ void _errorAfterSucceedTest(SendPort sendPort) {
 /// An isolate entrypoint that defines a test that errors multiple times.
 void _multiErrorTest(SendPort sendPort) {
   VmListener.start(sendPort, () => () {
-    _declarer.test("multiple errors", () {
+    test("multiple errors", () {
       Invoker.current.addOutstandingCallback();
       new Future(() => throw "one");
       new Future(() => throw "two");
diff --git a/test/with_test_environment_test.dart b/test/with_test_environment_test.dart
deleted file mode 100644
index 456384fa..00000000
--- a/test/with_test_environment_test.dart
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright (c) 2014, 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.
-
-library unittest.with_test_environment_test;
-
-import 'dart:async';
-import 'package:unittest/unittest.dart';
-
-class TestConfiguration extends SimpleConfiguration {
-  final Completer _completer = new Completer();
-  List<TestCase> _results;
-
-  TestConfiguration();
-
-  void onSummary(int passed, int failed, int errors, List<TestCase> results,
-      String uncaughtError) {
-    super.onSummary(passed, failed, errors, results, uncaughtError);
-    _results = results;
-  }
-
-  Future get done => _completer.future;
-
-  onDone(success) {
-    new Future.sync(() => super.onDone(success))
-        .then(_completer.complete)
-        .catchError(_completer.completeError);
-  }
-
-  bool checkIfTestRan(String testName) {
-    return _results.any((test) => test.description == testName);
-  }
-}
-
-Future runUnittests(Function callback) {
-  TestConfiguration config = unittestConfiguration = new TestConfiguration();
-  callback();
-
-  return config.done;
-}
-
-void runTests() {
-  test('test', () => expect(2 + 3, equals(5)));
-}
-
-void runTests1() {
-  test('test1', () => expect(4 + 3, equals(7)));
-}
-
-// Test that we can run two different sets of tests in the same run using the
-// withTestEnvironment method.
-void main() {
-  // Check that setting config a second time does not change the configuration
-  runUnittests(runTests);
-  var config = unittestConfiguration;
-  runUnittests(runTests1);
-  expect(config, unittestConfiguration);
-
-  // Test that we can run both when encapsulating in their own private test
-  // environment. Also test that the tests actually running are the ones
-  // scheduled in the runTests/runTests1 methods.
-  withTestEnvironment(() {
-    runUnittests(runTests).whenComplete(() {
-      TestConfiguration config = unittestConfiguration;
-      expect(config.checkIfTestRan('test'), true);
-      expect(config.checkIfTestRan('test1'), false);
-    });
-  });
-  withTestEnvironment(() {
-    runUnittests(runTests1).whenComplete(() {
-      TestConfiguration config = unittestConfiguration;
-      expect(config.checkIfTestRan('test'), false);
-      expect(config.checkIfTestRan('test1'), true);
-    });
-  });
-
-  // Third test that we can run with two nested test environments.
-  withTestEnvironment(() {
-    runUnittests(runTests);
-    withTestEnvironment(() {
-      runUnittests(runTests1);
-    });
-  });
-}
-- 
GitLab