Add support for setUpAll and tearDownAll.

Closes #18

R=kevmoo@google.com

Review URL: https://codereview.chromium.org//1400743002 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa737e7..299d220 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 ## 0.12.5
 
+* Add `setUpAll()` and `tearDownAll()` methods that run callbacks before and
+  after all tests in a group or suite. **Note that these methods are for special
+  cases and should be avoided**—they make it very easy to accidentally introduce
+  dependencies between tests. Use `setUp()` and `tearDown()` instead if
+  possible.
+
 * Allow `setUp()` and `tearDown()` to be called multiple times within the same
   group.
 
diff --git a/lib/src/backend/declarer.dart b/lib/src/backend/declarer.dart
index f8dae47..46a52d9 100644
--- a/lib/src/backend/declarer.dart
+++ b/lib/src/backend/declarer.dart
@@ -9,9 +9,10 @@
 import '../frontend/timeout.dart';
 import '../utils.dart';
 import 'group.dart';
+import 'group_entry.dart';
 import 'invoker.dart';
 import 'metadata.dart';
-import 'group_entry.dart';
+import 'test.dart';
 
 /// A class that manages the state of tests as they're declared.
 ///
@@ -34,12 +35,18 @@
   /// and of the test suite.
   final Metadata _metadata;
 
-  /// The set-up functions for this group.
+  /// The set-up functions to run for each test in this group.
   final _setUps = new List<AsyncFunction>();
 
-  /// The tear-down functions for this group.
+  /// The tear-down functions to run for each test in this group.
   final _tearDowns = new List<AsyncFunction>();
 
+  /// The set-up functions to run once for this group.
+  final _setUpAlls = new List<AsyncFunction>();
+
+  /// The tear-down functions to run once for this group.
+  final _tearDownAlls = new List<AsyncFunction>();
+
   /// The children of this group, either tests or sub-groups.
   final _entries = new List<GroupEntry>();
 
@@ -67,9 +74,7 @@
   /// Defines a test case with the given name and body.
   void test(String name, body(), {String testOn, Timeout timeout, skip,
       Map<String, dynamic> onPlatform}) {
-    if (_built) {
-      throw new StateError("Can't call test() once tests have begun running.");
-    }
+    _checkNotBuilt("test");
 
     var metadata = _metadata.merge(new Metadata.parse(
         testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform));
@@ -90,9 +95,7 @@
   /// Creates a group of tests.
   void group(String name, void body(), {String testOn, Timeout timeout, skip,
       Map<String, dynamic> onPlatform}) {
-    if (_built) {
-      throw new StateError("Can't call group() once tests have begun running.");
-    }
+    _checkNotBuilt("group");
 
     var metadata = _metadata.merge(new Metadata.parse(
         testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform));
@@ -113,31 +116,45 @@
 
   /// Registers a function to be run before each test in this group.
   void setUp(callback()) {
-    if (_built) {
-      throw new StateError("Can't call setUp() once tests have begun running.");
-    }
-
+    _checkNotBuilt("setUp");
     _setUps.add(callback);
   }
 
   /// Registers a function to be run after each test in this group.
   void tearDown(callback()) {
-    if (_built) {
-      throw new StateError(
-          "Can't call tearDown() once tests have begun running.");
-    }
-
+    _checkNotBuilt("tearDown");
     _tearDowns.add(callback);
   }
 
+  /// Registers a function to be run once before all tests.
+  void setUpAll(callback()) {
+    _checkNotBuilt("setUpAll");
+    _setUpAlls.add(callback);
+  }
+
+  /// Registers a function to be run once after all tests.
+  void tearDownAll(callback()) {
+    _checkNotBuilt("tearDownAll");
+    _tearDownAlls.add(callback);
+  }
+
   /// Finalizes and returns the group being declared.
   Group build() {
-    if (_built) {
-      throw new StateError("Can't call Declarer.build() more than once.");
-    }
+    _checkNotBuilt("build");
 
     _built = true;
-    return new Group(_name, _entries.toList(), metadata: _metadata);
+    return new Group(_name, _entries.toList(),
+        metadata: _metadata,
+        setUpAll: _setUpAll,
+        tearDownAll: _tearDownAll);
+  }
+
+  /// Throws a [StateError] if [build] has been called.
+  ///
+  /// [name] should be the name of the method being called.
+  void _checkNotBuilt(String name) {
+    if (!_built) return;
+    throw new StateError("Can't call $name() once tests have begun running.");
   }
 
   /// Run the set-up functions for this and any parent groups.
@@ -173,6 +190,26 @@
     });
   }
 
+  /// Returns a [Test] that runs the callbacks in [_setUpAll].
+  Test get _setUpAll {
+    if (_setUpAlls.isEmpty) return null;
+
+    return new LocalTest(_prefix("(setUpAll)"), _metadata, () {
+      return Future.forEach(_setUpAlls, (setUp) => setUp());
+    });
+  }
+
+  /// Returns a [Test] that runs the callbacks in [_tearDownAll].
+  Test get _tearDownAll {
+    if (_tearDownAlls.isEmpty) return null;
+
+    return new LocalTest(_prefix("(tearDownAll)"), _metadata, () {
+      return Invoker.current.unclosable(() {
+        return Future.forEach(_tearDownAlls.reversed, _errorsDontStopTest);
+      });
+    });
+  }
+
   /// Runs [body] with special error-handling behavior.
   ///
   /// Errors emitted [body] will still cause the current test to fail, but they
diff --git a/lib/src/backend/group.dart b/lib/src/backend/group.dart
index bcc7e77..dc7e6bd 100644
--- a/lib/src/backend/group.dart
+++ b/lib/src/backend/group.dart
@@ -25,7 +25,18 @@
   Group.root(Iterable<GroupEntry> entries, {Metadata metadata})
       : this(null, entries, metadata: metadata);
 
-  Group(this.name, Iterable<GroupEntry> entries, {Metadata metadata})
+  /// A test to run before all tests in the group.
+  ///
+  /// This is `null` if no `setUpAll` callbacks were declared.
+  final Test setUpAll;
+
+  /// A test to run after all tests in the group.
+  ///
+  /// This is `null` if no `tearDown` callbacks were declared.
+  final Test tearDownAll;
+
+  Group(this.name, Iterable<GroupEntry> entries, {Metadata metadata,
+          Test this.setUpAll, Test this.tearDownAll})
       : entries = new List<GroupEntry>.unmodifiable(entries),
         metadata = metadata == null ? new Metadata() : metadata;
 
@@ -34,13 +45,15 @@
     var newMetadata = metadata.forPlatform(platform, os: os);
     var filtered = _map((entry) => entry.forPlatform(platform, os: os));
     if (filtered.isEmpty) return null;
-    return new Group(name, filtered, metadata: newMetadata);
+    return new Group(name, filtered,
+        metadata: newMetadata, setUpAll: setUpAll, tearDownAll: tearDownAll);
   }
 
   Group filter(bool callback(Test test)) {
     var filtered = _map((entry) => entry.filter(callback));
     if (filtered.isEmpty) return null;
-    return new Group(name, filtered, metadata: metadata);
+    return new Group(name, filtered,
+        metadata: metadata, setUpAll: setUpAll, tearDownAll: tearDownAll);
   }
 
   /// Returns the entries of this group mapped using [callback].
diff --git a/lib/src/runner/browser/browser_manager.dart b/lib/src/runner/browser/browser_manager.dart
index 925dddc..fc570d1 100644
--- a/lib/src/runner/browser/browser_manager.dart
+++ b/lib/src/runner/browser/browser_manager.dart
@@ -13,6 +13,7 @@
 
 import '../../backend/group.dart';
 import '../../backend/metadata.dart';
+import '../../backend/test.dart';
 import '../../backend/test_platform.dart';
 import '../../util/cancelable_future.dart';
 import '../../util/multi_channel.dart';
@@ -259,11 +260,25 @@
         return _deserializeGroup(suiteChannel, mapper, entry);
       }
 
-      var testMetadata = new Metadata.deserialize(entry['metadata']);
-      var testChannel = suiteChannel.virtualChannel(entry['channel']);
-      return new IframeTest(entry['name'], testMetadata, testChannel,
-          mapper: mapper);
-    }), metadata: metadata);
+      return _deserializeTest(suiteChannel, mapper, entry);
+    }),
+        metadata: metadata,
+        setUpAll: _deserializeTest(suiteChannel, mapper, group['setUpAll']),
+        tearDownAll:
+            _deserializeTest(suiteChannel, mapper, group['tearDownAll']));
+  }
+
+  /// Deserializes [test] into a concrete [Test] class.
+  ///
+  /// Returns `null` if [test] is `null`.
+  Test _deserializeTest(MultiChannel suiteChannel, StackTraceMapper mapper,
+      Map test) {
+    if (test == null) return null;
+
+    var metadata = new Metadata.deserialize(test['metadata']);
+    var testChannel = suiteChannel.virtualChannel(test['channel']);
+    return new IframeTest(test['name'], metadata, testChannel,
+        mapper: mapper);
   }
 
   /// An implementation of [Environment.displayPause].
diff --git a/lib/src/runner/browser/iframe_listener.dart b/lib/src/runner/browser/iframe_listener.dart
index 815a37e..834f3e1 100644
--- a/lib/src/runner/browser/iframe_listener.dart
+++ b/lib/src/runner/browser/iframe_listener.dart
@@ -131,32 +131,42 @@
     });
   }
 
-  /// Serializes [entries] into a JSON-safe map.
+  /// Serializes [group] into a JSON-safe map.
   Map _serializeGroup(MultiChannel channel, Group group) {
     return {
       "type": "group",
       "name": group.name,
       "metadata": group.metadata.serialize(),
+      "setUpAll": _serializeTest(channel, group.setUpAll),
+      "tearDownAll": _serializeTest(channel, group.tearDownAll),
       "entries": group.entries.map((entry) {
-        if (entry is Group) return _serializeGroup(channel, entry);
-
-        var test = entry as Test;
-        var testChannel = channel.virtualChannel();
-        testChannel.stream.listen((message) {
-          assert(message['command'] == 'run');
-          _runTest(test, channel.virtualChannel(message['channel']));
-        });
-
-        return {
-          "type": "test",
-          "name": test.name,
-          "metadata": test.metadata.serialize(),
-          "channel": testChannel.id
-        };
+        return entry is Group
+            ? _serializeGroup(channel, entry)
+            : _serializeTest(channel, entry);
       }).toList()
     };
   }
 
+  /// Serializes [test] into a JSON-safe map.
+  ///
+  /// Returns `null` if [test] is `null`.
+  Map _serializeTest(MultiChannel channel, Test test) {
+    if (test == null) return null;
+
+    var testChannel = channel.virtualChannel();
+    testChannel.stream.listen((message) {
+      assert(message['command'] == 'run');
+      _runTest(test, channel.virtualChannel(message['channel']));
+    });
+
+    return {
+      "type": "test",
+      "name": test.name,
+      "metadata": test.metadata.serialize(),
+      "channel": testChannel.id
+    };
+  }
+
   /// Runs [test] and sends the results across [channel].
   void _runTest(Test test, MultiChannel channel) {
     var liveTest = test.load(_suite);
diff --git a/lib/src/runner/engine.dart b/lib/src/runner/engine.dart
index 6bd51bf..ed88544 100644
--- a/lib/src/runner/engine.dart
+++ b/lib/src/runner/engine.dart
@@ -215,18 +215,35 @@
       return;
     }
 
-    for (var entry in group.entries) {
-      if (_closed) return;
+    var setUpAllSucceeded = true;
+    if (group.setUpAll != null) {
+      var liveTest = group.setUpAll.load(suite);
+      await _runLiveTest(liveTest, countSuccess: false);
+      setUpAllSucceeded = liveTest.state.result == Result.success;
+    }
 
-      if (entry is Group) {
-        await _runGroup(suite, entry);
-      } else if (entry.metadata.skip) {
-        await _runLiveTest(_skippedTest(suite, entry));
-      } else {
-        var test = entry as Test;
-        await _runLiveTest(test.load(suite));
+    if (!_closed && setUpAllSucceeded) {
+      for (var entry in group.entries) {
+        if (_closed) return;
+
+        if (entry is Group) {
+          await _runGroup(suite, entry);
+        } else if (entry.metadata.skip) {
+          await _runLiveTest(_skippedTest(suite, entry));
+        } else {
+          var test = entry as Test;
+          await _runLiveTest(test.load(suite));
+        }
       }
     }
+
+    // Even if we're closed or setUpAll failed, we want to run all the teardowns
+    // to ensure that any state is properly cleaned up.
+    if (group.tearDownAll != null) {
+      var liveTest = group.tearDownAll.load(suite);
+      await _runLiveTest(liveTest, countSuccess: false);
+      if (_closed) await liveTest.close();
+    }
   }
 
   /// Returns a dummy [LiveTest] for a test or group marked as "skip".
@@ -244,7 +261,10 @@
   }
 
   /// Runs [liveTest].
-  Future _runLiveTest(LiveTest liveTest) async {
+  ///
+  /// If [countSuccess] is `true` (the default), the test is put into [passed]
+  /// if it succeeds. Otherwise, it's removed from [liveTests] entirely.
+  Future _runLiveTest(LiveTest liveTest, {bool countSuccess: true}) async {
     _liveTests.add(liveTest);
     _active.add(liveTest);
 
@@ -270,8 +290,10 @@
         _failed.add(liveTest);
       } else if (liveTest.test.metadata.skip) {
         _skipped.add(liveTest);
-      } else {
+      } else if (countSuccess) {
         _passed.add(liveTest);
+      } else {
+        _liveTests.remove(liveTest);
       }
     });
 
@@ -342,15 +364,19 @@
   /// the engine indicates that no more output should be emitted.
   Future close() async {
     _closed = true;
-    if (_closedBeforeDone == null) _closedBeforeDone = true;
+    if (_closedBeforeDone != null) _closedBeforeDone = true;
     _suiteController.close();
 
     // Close the running tests first so that we're sure to wait for them to
     // finish before we close their suites and cause them to become unloaded.
     var allLiveTests = liveTests.toSet()..addAll(_activeLoadTests);
-    await Future.wait(allLiveTests.map((liveTest) => liveTest.close()));
+    var futures = allLiveTests.map((liveTest) => liveTest.close()).toList();
 
-    var allSuites = allLiveTests.map((liveTest) => liveTest.suite).toSet();
-    await Future.wait(allSuites.map((suite) => suite.close()));
+    // Closing the load pool will close the test suites as soon as their tests
+    // are done. For browser suites this is effectively immediate since their
+    // tests shut down as soon as they're closed, but for VM suites we may need
+    // to wait for tearDowns or tearDownAlls to run.
+    futures.add(_loadPool.close());
+    await Future.wait(futures, eagerError: true);
   }
 }
diff --git a/lib/src/runner/loader.dart b/lib/src/runner/loader.dart
index 1585078..62fb46b 100644
--- a/lib/src/runner/loader.dart
+++ b/lib/src/runner/loader.dart
@@ -15,6 +15,7 @@
 
 import '../backend/group.dart';
 import '../backend/metadata.dart';
+import '../backend/test.dart';
 import '../backend/test_platform.dart';
 import '../util/dart.dart' as dart;
 import '../util/io.dart';
@@ -244,9 +245,21 @@
     var metadata = new Metadata.deserialize(group['metadata']);
     return new Group(group['name'], group['entries'].map((entry) {
       if (entry['type'] == 'group') return _deserializeGroup(entry);
-      var testMetadata = new Metadata.deserialize(entry['metadata']);
-      return new IsolateTest(entry['name'], testMetadata, entry['sendPort']);
-    }), metadata: metadata);
+      return _deserializeTest(entry);
+    }),
+        metadata: metadata,
+        setUpAll: _deserializeTest(group['setUpAll']),
+        tearDownAll: _deserializeTest(group['tearDownAll']));
+  }
+
+  /// Deserializes [test] into a concrete [Test] class.
+  ///
+  /// Returns `null` if [test] is `null`.
+  Test _deserializeTest(Map test) {
+    if (test == null) return null;
+
+    var metadata = new Metadata.deserialize(test['metadata']);
+    return new IsolateTest(test['name'], metadata, test['sendPort']);
   }
 
   /// Closes the loader and releases all resources allocated by it.
diff --git a/lib/src/runner/vm/isolate_listener.dart b/lib/src/runner/vm/isolate_listener.dart
index 493ade4..23f7b73 100644
--- a/lib/src/runner/vm/isolate_listener.dart
+++ b/lib/src/runner/vm/isolate_listener.dart
@@ -117,26 +117,34 @@
       "type": "group",
       "name": group.name,
       "metadata": group.metadata.serialize(),
+      "setUpAll": _serializeTest(group.setUpAll),
+      "tearDownAll": _serializeTest(group.tearDownAll),
       "entries": group.entries.map((entry) {
-        if (entry is Group) return _serializeGroup(entry);
-
-        var test = entry as Test;
-        var receivePort = new ReceivePort();
-        receivePort.listen((message) {
-          assert(message['command'] == 'run');
-          _runTest(test, message['reply']);
-        });
-
-        return {
-          "type": "test",
-          "name": test.name,
-          "metadata": test.metadata.serialize(),
-          "sendPort": receivePort.sendPort
-        };
+        return entry is Group ? _serializeGroup(entry) : _serializeTest(entry);
       }).toList()
     };
   }
 
+  /// Serializes [test] into a JSON-safe map.
+  ///
+  /// Returns `null` if [test] is `null`.
+  Map _serializeTest(Test test) {
+    if (test == null) return null;
+
+    var receivePort = new ReceivePort();
+    receivePort.listen((message) {
+      assert(message['command'] == 'run');
+      _runTest(test, message['reply']);
+    });
+
+    return {
+      "type": "test",
+      "name": test.name,
+      "metadata": test.metadata.serialize(),
+      "sendPort": receivePort.sendPort
+    };
+  }
+
   /// Runs [test] and sends the results across [sendPort].
   void _runTest(Test test, SendPort sendPort) {
     var liveTest = test.load(_suite);
diff --git a/lib/test.dart b/lib/test.dart
index e662d1e..48235ff 100644
--- a/lib/test.dart
+++ b/lib/test.dart
@@ -187,6 +187,34 @@
 /// reverse of the order they were declared.
 void tearDown(callback()) => _declarer.tearDown(callback);
 
+/// Registers a function to be run once before all tests.
+///
+/// [callback] may be asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, [callback] will run before all tests
+/// in that group. It will be run after any [setUpAll] callbacks in parent
+/// groups or at the top level. It won't be run if none of the tests in the
+/// group are run.
+///
+/// **Note**: This function makes it very easy to accidentally introduce hidden
+/// dependencies between tests that should be isolated. In general, you should
+/// prefer [setUp], and only use [setUpAll] if the callback is prohibitively
+/// slow.
+void setUpAll(callback()) => _declarer.setUpAll(callback);
+
+/// Registers a function to be run once after all tests.
+///
+/// If this is called within a test group, [callback] will run after all tests
+/// in that group. It will be run before any [tearDownAll] callbacks in parent
+/// groups or at the top level. It won't be run if none of the tests in the
+/// group are run.
+///
+/// **Note**: This function makes it very easy to accidentally introduce hidden
+/// dependencies between tests that should be isolated. In general, you should
+/// prefer [tearDown], and only use [tearDOwnAll] if the callback is
+/// prohibitively slow.
+void tearDownAll(callback()) => _declarer.tearDownAll(callback);
+
 /// Registers an exception that was caught for the current test.
 void registerException(error, [StackTrace stackTrace]) {
   // This will usually forward directly to [Invoker.current.handleError], but
diff --git a/pubspec.yaml b/pubspec.yaml
index 89683f2..61854b5 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test
-version: 0.12.4+9
+version: 0.12.5
 author: Dart Team <misc@dartlang.org>
 description: A library for writing dart unit tests.
 homepage: https://github.com/dart-lang/test
@@ -16,7 +16,7 @@
   http_multi_server: '^1.0.0'
   http_parser: '>=0.0.2 <2.0.0'
   path: '^1.2.0'
-  pool: '^1.1.0'
+  pool: '^1.2.0'
   pub_semver: '^1.0.0'
   shelf: '>=0.6.0 <0.7.0'
   shelf_static: '^0.2.0'
diff --git a/test/frontend/set_up_all_test.dart b/test/frontend/set_up_all_test.dart
new file mode 100644
index 0000000..7cfc274
--- /dev/null
+++ b/test/frontend/set_up_all_test.dart
@@ -0,0 +1,324 @@
+// 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:async/async.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void main() {
+  test("runs once before all tests", () {
+    return expectTestsPass(() {
+      var setUpAllRun = false;
+      setUpAll(() {
+        expect(setUpAllRun, isFalse);
+        setUpAllRun = true;
+      });
+
+      test("test 1", () {
+        expect(setUpAllRun, isTrue);
+      });
+
+      test("test 2", () {
+        expect(setUpAllRun, isTrue);
+      });
+    });
+  });
+
+  test("runs once per group, outside-in", () {
+    return expectTestsPass(() {
+      var setUpAll1Run = false;
+      var setUpAll2Run = false;
+      var setUpAll3Run = false;
+      setUpAll(() {
+        expect(setUpAll1Run, isFalse);
+        expect(setUpAll2Run, isFalse);
+        expect(setUpAll3Run, isFalse);
+        setUpAll1Run = true;
+      });
+
+      group("mid", () {
+        setUpAll(() {
+          expect(setUpAll1Run, isTrue);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+          setUpAll2Run = true;
+        });
+
+        group("inner", () {
+          setUpAll(() {
+            expect(setUpAll1Run, isTrue);
+            expect(setUpAll2Run, isTrue);
+            expect(setUpAll3Run, isFalse);
+            setUpAll3Run = true;
+          });
+
+          test("test", () {
+            expect(setUpAll1Run, isTrue);
+            expect(setUpAll2Run, isTrue);
+            expect(setUpAll3Run, isTrue);
+          });
+        });
+      });
+    });
+  });
+
+  test("runs before setUps", () {
+    return expectTestsPass(() {
+      var setUpAllRun = false;
+      setUp(() {
+        expect(setUpAllRun, isTrue);
+      });
+
+      setUpAll(() {
+        expect(setUpAllRun, isFalse);
+        setUpAllRun = true;
+      });
+
+      setUp(() {
+        expect(setUpAllRun, isTrue);
+      });
+
+      test("test", () {
+        expect(setUpAllRun, isTrue);
+      });
+    });
+  });
+
+  test("multiples run in order", () {
+    return expectTestsPass(() {
+      var setUpAll1Run = false;
+      var setUpAll2Run = false;
+      var setUpAll3Run = false;
+      setUpAll(() {
+        expect(setUpAll1Run, isFalse);
+        expect(setUpAll2Run, isFalse);
+        expect(setUpAll3Run, isFalse);
+        setUpAll1Run = true;
+      });
+
+      setUpAll(() {
+        expect(setUpAll1Run, isTrue);
+        expect(setUpAll2Run, isFalse);
+        expect(setUpAll3Run, isFalse);
+        setUpAll2Run = true;
+      });
+
+      setUpAll(() {
+        expect(setUpAll1Run, isTrue);
+        expect(setUpAll2Run, isTrue);
+        expect(setUpAll3Run, isFalse);
+        setUpAll3Run = true;
+      });
+
+      test("test", () {
+        expect(setUpAll1Run, isTrue);
+        expect(setUpAll2Run, isTrue);
+        expect(setUpAll3Run, isTrue);
+      });
+    });
+  });
+
+  group("asynchronously", () {
+    test("blocks additional setUpAlls on in-band async", () {
+      return expectTestsPass(() {
+        var setUpAll1Run = false;
+        var setUpAll2Run = false;
+        var setUpAll3Run = false;
+        setUpAll(() async {
+          expect(setUpAll1Run, isFalse);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+          await pumpEventQueue();
+          setUpAll1Run = true;
+        });
+
+        setUpAll(() async {
+          expect(setUpAll1Run, isTrue);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+          await pumpEventQueue();
+          setUpAll2Run = true;
+        });
+
+        setUpAll(() async {
+          expect(setUpAll1Run, isTrue);
+          expect(setUpAll2Run, isTrue);
+          expect(setUpAll3Run, isFalse);
+          await pumpEventQueue();
+          setUpAll3Run = true;
+        });
+
+        test("test", () {
+          expect(setUpAll1Run, isTrue);
+          expect(setUpAll2Run, isTrue);
+          expect(setUpAll3Run, isTrue);
+        });
+      });
+    });
+
+    test("doesn't block additional setUpAlls on out-of-band async", () {
+      return expectTestsPass(() {
+        var setUpAll1Run = false;
+        var setUpAll2Run = false;
+        var setUpAll3Run = false;
+        setUpAll(() {
+          expect(setUpAll1Run, isFalse);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+
+          expect(pumpEventQueue().then((_) {
+            setUpAll1Run = true;
+          }), completes);
+        });
+
+        setUpAll(() {
+          expect(setUpAll1Run, isFalse);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+
+          expect(pumpEventQueue().then((_) {
+            setUpAll2Run = true;
+          }), completes);
+        });
+
+        setUpAll(() {
+          expect(setUpAll1Run, isFalse);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+
+          expect(pumpEventQueue().then((_) {
+            setUpAll3Run = true;
+          }), completes);
+        });
+
+        test("test", () {
+          expect(setUpAll1Run, isTrue);
+          expect(setUpAll2Run, isTrue);
+          expect(setUpAll3Run, isTrue);
+        });
+      });
+    });
+  });
+
+  test("isn't run for a skipped group", () async {
+    // Declare this in the outer test so if it runs, the outer test will fail.
+    var shouldNotRun = expectAsync(() {}, count: 0);
+
+    var engine = declareEngine(() {
+      group("skipped", () {
+        setUpAll(shouldNotRun);
+
+        test("test", () {});
+      }, skip: true);
+    });
+
+    await engine.run();
+    expect(engine.liveTests, hasLength(1));
+    expect(engine.skipped, hasLength(1));
+    expect(engine.liveTests, equals(engine.skipped));
+  });
+
+  test("is emitted through Engine.onTestStarted", () async {
+    var engine = declareEngine(() {
+      setUpAll(() {});
+
+      test("test", () {});
+    });
+
+    var queue = new StreamQueue(engine.onTestStarted);
+    var setUpAllFuture = queue.next;
+    var liveTestFuture = queue.next;
+
+    await engine.run();
+
+    var setUpAllLiveTest = await setUpAllFuture;
+    expect(setUpAllLiveTest.test.name, equals("(setUpAll)"));
+    expectTestPassed(setUpAllLiveTest);
+
+    // The fake test for setUpAll should be removed from the engine's live
+    // test list so that reporters don't display it as a passed test.
+    expect(engine.liveTests, isNot(contains(setUpAllLiveTest)));
+    expect(engine.passed, isNot(contains(setUpAllLiveTest)));
+    expect(engine.failed, isNot(contains(setUpAllLiveTest)));
+    expect(engine.skipped, isNot(contains(setUpAllLiveTest)));
+    expect(engine.active, isNot(contains(setUpAllLiveTest)));
+
+    var liveTest = await liveTestFuture;
+    expectTestPassed(await liveTestFuture);
+    expect(engine.liveTests, contains(liveTest));
+    expect(engine.passed, contains(liveTest));
+  });
+
+  group("with an error", () {
+    test("reports the error and remains in Engine.liveTests", () async {
+      var engine = declareEngine(() {
+        setUpAll(() => throw new TestFailure("fail"));
+
+        test("test", () {});
+      });
+
+      var queue = new StreamQueue(engine.onTestStarted);
+      var setUpAllFuture = queue.next;
+
+      expect(await engine.run(), isFalse);
+
+      var setUpAllLiveTest = await setUpAllFuture;
+      expect(setUpAllLiveTest.test.name, equals("(setUpAll)"));
+      expectTestFailed(setUpAllLiveTest, "fail");
+
+      // The fake test for setUpAll should be removed from the engine's live
+      // test list so that reporters don't display it as a passed test.
+      expect(engine.liveTests, contains(setUpAllLiveTest));
+      expect(engine.failed, contains(setUpAllLiveTest));
+      expect(engine.passed, isNot(contains(setUpAllLiveTest)));
+      expect(engine.skipped, isNot(contains(setUpAllLiveTest)));
+      expect(engine.active, isNot(contains(setUpAllLiveTest)));
+    });
+
+    test("doesn't run tests in the group", () async {
+      // Declare this in the outer test so if it runs, the outer test will fail.
+      var shouldNotRun = expectAsync(() {}, count: 0);
+
+      var engine = declareEngine(() {
+        setUpAll(() => throw "error");
+
+        test("test", shouldNotRun);
+      });
+
+      expect(await engine.run(), isFalse);
+    });
+
+    test("doesn't run inner groups", () async {
+      // Declare this in the outer test so if it runs, the outer test will fail.
+      var shouldNotRun = expectAsync(() {}, count: 0);
+
+      var engine = declareEngine(() {
+        setUpAll(() => throw "error");
+
+        group("group", () {
+          test("test", shouldNotRun);
+        });
+      });
+
+      expect(await engine.run(), isFalse);
+    });
+
+    test("doesn't run further setUpAlls", () async {
+      // Declare this in the outer test so if it runs, the outer test will fail.
+      var shouldNotRun = expectAsync(() {}, count: 0);
+
+      var engine = declareEngine(() {
+        setUpAll(() => throw "error");
+        setUpAll(shouldNotRun);
+
+        test("test", shouldNotRun);
+      });
+
+      expect(await engine.run(), isFalse);
+    });
+  });
+}
diff --git a/test/frontend/tear_down_all_test.dart b/test/frontend/tear_down_all_test.dart
new file mode 100644
index 0000000..a5d9e0b
--- /dev/null
+++ b/test/frontend/tear_down_all_test.dart
@@ -0,0 +1,372 @@
+// 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:async/async.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void main() {
+  test("runs once after all tests", () {
+    return expectTestsPass(() {
+      var test1Run = false;
+      var test2Run = false;
+      var tearDownAllRun = false;
+      tearDownAll(() {
+        expect(test1Run, isTrue);
+        expect(test2Run, isTrue);
+        expect(tearDownAllRun, isFalse);
+        tearDownAllRun = true;
+      });
+
+      test("test 1", () {
+        expect(tearDownAllRun, isFalse);
+        test1Run = true;
+      });
+
+      test("test 2", () {
+        expect(tearDownAllRun, isFalse);
+        test2Run = true;
+      });
+    });
+  });
+
+  test("runs once per group, inside-out", () {
+    return expectTestsPass(() {
+      var tearDownAll1Run = false;
+      var tearDownAll2Run = false;
+      var tearDownAll3Run = false;
+      var testRun = false;
+      tearDownAll(() {
+        expect(tearDownAll1Run, isFalse);
+        expect(tearDownAll2Run, isTrue);
+        expect(tearDownAll3Run, isTrue);
+        expect(testRun, isTrue);
+        tearDownAll1Run = true;
+      });
+
+      group("mid", () {
+        tearDownAll(() {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isTrue);
+          expect(testRun, isTrue);
+          tearDownAll2Run = true;
+        });
+
+        group("inner", () {
+          tearDownAll(() {
+            expect(tearDownAll1Run, isFalse);
+            expect(tearDownAll2Run, isFalse);
+            expect(tearDownAll3Run, isFalse);
+            expect(testRun, isTrue);
+            tearDownAll3Run = true;
+          });
+
+          test("test", () {
+            expect(tearDownAll1Run, isFalse);
+            expect(tearDownAll2Run, isFalse);
+            expect(tearDownAll3Run, isFalse);
+            testRun = true;
+          });
+        });
+      });
+    });
+  });
+
+  test("runs after tearDowns", () {
+    return expectTestsPass(() {
+      var tearDown1Run = false;
+      var tearDown2Run = false;
+      var tearDownAllRun = false;
+      tearDown(() {
+        expect(tearDownAllRun, isFalse);
+        tearDown1Run = true;
+      });
+
+      tearDownAll(() {
+        expect(tearDown1Run, isTrue);
+        expect(tearDown2Run, isTrue);
+        expect(tearDownAllRun, isFalse);
+        tearDownAllRun = true;
+      });
+
+      tearDown(() {
+        expect(tearDownAllRun, isFalse);
+        tearDown2Run = true;
+      });
+
+      test("test", () {
+        expect(tearDownAllRun, isFalse);
+      });
+    });
+  });
+
+  test("multiples run in reverse order", () {
+    return expectTestsPass(() {
+      var tearDownAll1Run = false;
+      var tearDownAll2Run = false;
+      var tearDownAll3Run = false;
+      tearDownAll(() {
+        expect(tearDownAll1Run, isFalse);
+        expect(tearDownAll2Run, isTrue);
+        expect(tearDownAll3Run, isTrue);
+        tearDownAll1Run = true;
+      });
+
+      tearDownAll(() {
+        expect(tearDownAll1Run, isFalse);
+        expect(tearDownAll2Run, isFalse);
+        expect(tearDownAll3Run, isTrue);
+        tearDownAll2Run = true;
+      });
+
+      tearDownAll(() {
+        expect(tearDownAll1Run, isFalse);
+        expect(tearDownAll2Run, isFalse);
+        expect(tearDownAll3Run, isFalse);
+        tearDownAll3Run = true;
+      });
+
+      test("test", () {
+        expect(tearDownAll1Run, isFalse);
+        expect(tearDownAll2Run, isFalse);
+        expect(tearDownAll3Run, isFalse);
+      });
+    });
+  });
+
+  group("asynchronously", () {
+    test("blocks additional tearDownAlls on in-band async", () {
+      return expectTestsPass(() {
+        var tearDownAll1Run = false;
+        var tearDownAll2Run = false;
+        var tearDownAll3Run = false;
+        tearDownAll(() async {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isTrue);
+          expect(tearDownAll3Run, isTrue);
+          await pumpEventQueue();
+          tearDownAll1Run = true;
+        });
+
+        tearDownAll(() async {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isTrue);
+          await pumpEventQueue();
+          tearDownAll2Run = true;
+        });
+
+        tearDownAll(() async {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+          await pumpEventQueue();
+          tearDownAll3Run = true;
+        });
+
+        test("test", () {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+        });
+      });
+    });
+
+    test("doesn't block additional tearDownAlls on out-of-band async", () {
+      return expectTestsPass(() {
+        var tearDownAll1Run = false;
+        var tearDownAll2Run = false;
+        var tearDownAll3Run = false;
+        tearDownAll(() {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+
+          expect(new Future(() {
+            tearDownAll1Run = true;
+          }), completes);
+        });
+
+        tearDownAll(() {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+
+          expect(new Future(() {
+            tearDownAll2Run = true;
+          }), completes);
+        });
+
+        tearDownAll(() {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+
+          expect(new Future(() {
+            tearDownAll3Run = true;
+          }), completes);
+        });
+
+        test("test", () {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+        });
+      });
+    });
+
+    test("blocks further tests on in-band async", () {
+      return expectTestsPass(() {
+        var tearDownAllRun = false;
+        group("group", () {
+          tearDownAll(() async {
+            expect(tearDownAllRun, isFalse);
+            await pumpEventQueue();
+            tearDownAllRun = true;
+          });
+
+          test("test", () {});
+        });
+
+        test("after", () {
+          expect(tearDownAllRun, isTrue);
+        });
+      });
+    });
+
+    test("blocks further tests on out-of-band async", () {
+      return expectTestsPass(() {
+        var tearDownAllRun = false;
+        group("group", () {
+          tearDownAll(() async {
+            expect(tearDownAllRun, isFalse);
+            expect(pumpEventQueue().then((_) {
+              tearDownAllRun = true;
+            }), completes);
+          });
+
+          test("test", () {});
+        });
+
+        test("after", () {
+          expect(tearDownAllRun, isTrue);
+        });
+      });
+    });
+  });
+
+  test("isn't run for a skipped group", () async {
+    // Declare this in the outer test so if it runs, the outer test will fail.
+    var shouldNotRun = expectAsync(() {}, count: 0);
+
+    var engine = declareEngine(() {
+      group("skipped", () {
+        tearDownAll(shouldNotRun);
+
+        test("test", () {});
+      }, skip: true);
+    });
+
+    await engine.run();
+    expect(engine.liveTests, hasLength(1));
+    expect(engine.skipped, hasLength(1));
+    expect(engine.liveTests, equals(engine.skipped));
+  });
+
+  test("is emitted through Engine.onTestStarted", () async {
+    var engine = declareEngine(() {
+      tearDownAll(() {});
+
+      test("test", () {});
+    });
+
+    var queue = new StreamQueue(engine.onTestStarted);
+    var liveTestFuture = queue.next;
+    var tearDownAllFuture = queue.next;
+
+    await engine.run();
+
+    var tearDownAllLiveTest = await tearDownAllFuture;
+    expect(tearDownAllLiveTest.test.name, equals("(tearDownAll)"));
+    expectTestPassed(tearDownAllLiveTest);
+
+    // The fake test for tearDownAll should be removed from the engine's live
+    // test list so that reporters don't display it as a passed test.
+    expect(engine.liveTests, isNot(contains(tearDownAllLiveTest)));
+    expect(engine.passed, isNot(contains(tearDownAllLiveTest)));
+    expect(engine.failed, isNot(contains(tearDownAllLiveTest)));
+    expect(engine.skipped, isNot(contains(tearDownAllLiveTest)));
+    expect(engine.active, isNot(contains(tearDownAllLiveTest)));
+
+    var liveTest = await liveTestFuture;
+    expectTestPassed(await liveTestFuture);
+    expect(engine.liveTests, contains(liveTest));
+    expect(engine.passed, contains(liveTest));
+  });
+
+  group("with an error", () {
+    test("reports the error and remains in Engine.liveTests", () async {
+      var engine = declareEngine(() {
+        tearDownAll(() => throw new TestFailure("fail"));
+
+        test("test", () {});
+      });
+
+      var queue = new StreamQueue(engine.onTestStarted);
+      expect(queue.next, completes);
+      var tearDownAllFuture = queue.next;
+
+      expect(await engine.run(), isFalse);
+
+      var tearDownAllLiveTest = await tearDownAllFuture;
+      expect(tearDownAllLiveTest.test.name, equals("(tearDownAll)"));
+      expectTestFailed(tearDownAllLiveTest, "fail");
+
+      // The fake test for tearDownAll should be removed from the engine's live
+      // test list so that reporters don't display it as a passed test.
+      expect(engine.liveTests, contains(tearDownAllLiveTest));
+      expect(engine.failed, contains(tearDownAllLiveTest));
+      expect(engine.passed, isNot(contains(tearDownAllLiveTest)));
+      expect(engine.skipped, isNot(contains(tearDownAllLiveTest)));
+      expect(engine.active, isNot(contains(tearDownAllLiveTest)));
+    });
+
+    test("runs further tearDownAlls", () async {
+      // Declare this in the outer test so if it doesn't runs, the outer test
+      // will fail.
+      var shouldRun = expectAsync(() {});
+
+      var engine = declareEngine(() {
+        tearDownAll(() => throw "error");
+        tearDownAll(shouldRun);
+
+        test("test", () {});
+      });
+
+      expect(await engine.run(), isFalse);
+    });
+
+    test("runs outer tearDownAlls", () async {
+      // Declare this in the outer test so if it doesn't runs, the outer test
+      // will fail.
+      var shouldRun = expectAsync(() {});
+
+      var engine = declareEngine(() {
+        tearDownAll(shouldRun);
+
+        group("group", () {
+          tearDownAll(() => throw "error");
+
+          test("test", () {});
+        });
+      });
+
+      expect(await engine.run(), isFalse);
+    });
+  });
+}
diff --git a/test/runner/browser/runner_test.dart b/test/runner/browser/runner_test.dart
index 14ad58d..dcd18c6 100644
--- a/test/runner/browser/runner_test.dart
+++ b/test/runner/browser/runner_test.dart
@@ -211,6 +211,40 @@
       test.shouldExit(0);
     });
 
+    test("with setUpAll", () {
+      d.file("test.dart", r"""
+          import 'package:test/test.dart';
+
+          void main() {
+            setUpAll(() => print("in setUpAll"));
+
+            test("test", () {});
+          }
+          """).create();
+
+      var test = runTest(["-p", "content-shell", "test.dart"]);
+      test.stdout.expect(consumeThrough(contains('+0: (setUpAll)')));
+      test.stdout.expect('in setUpAll');
+      test.shouldExit(0);
+    });
+
+    test("with tearDownAll", () {
+      d.file("test.dart", r"""
+          import 'package:test/test.dart';
+
+          void main() {
+            tearDownAll(() => print("in tearDownAll"));
+
+            test("test", () {});
+          }
+          """).create();
+
+      var test = runTest(["-p", "content-shell", "test.dart"]);
+      test.stdout.expect(consumeThrough(contains('+1: (tearDownAll)')));
+      test.stdout.expect('in tearDownAll');
+      test.shouldExit(0);
+    });
+
     // Regression test; this broke in 0.12.0-beta.9.
     test("on a file in a subdirectory", () {
       d.dir("dir", [d.file("test.dart", _success)]).create();
diff --git a/test/runner/engine_test.dart b/test/runner/engine_test.dart
index bb6a7db..37d1213 100644
--- a/test/runner/engine_test.dart
+++ b/test/runner/engine_test.dart
@@ -57,11 +57,11 @@
   test("emits each test before it starts running and after the previous test "
       "finished", () {
     var testsRun = 0;
-    var engine = withTests(declare(() {
+    var engine = declareEngine(() {
       for (var i = 0; i < 3; i++) {
         test("test ${i + 1}", expectAsync(() => testsRun++, max: 1));
       }
-    }));
+    });
 
     engine.onTestStarted.listen(expectAsync((liveTest) {
       // [testsRun] should be one less than the test currently running.
@@ -77,33 +77,33 @@
   });
 
   test(".run() returns true if every test passes", () {
-    var engine = withTests(declare(() {
+    var engine = declareEngine(() {
       for (var i = 0; i < 2; i++) {
         test("test ${i + 1}", () {});
       }
-    }));
+    });
 
     expect(engine.run(), completion(isTrue));
   });
 
   test(".run() returns false if any test fails", () {
-    var engine = withTests(declare(() {
+    var engine = declareEngine(() {
       for (var i = 0; i < 2; i++) {
         test("test ${i + 1}", () {});
       }
       test("failure", () => throw new TestFailure("oh no"));
-    }));
+    });
 
     expect(engine.run(), completion(isFalse));
   });
 
   test(".run() returns false if any test errors", () {
-    var engine = withTests(declare(() {
+    var engine = declareEngine(() {
       for (var i = 0; i < 2; i++) {
         test("test ${i + 1}", () {});
       }
       test("failure", () => throw "oh no");
-    }));
+    });
 
     expect(engine.run(), completion(isFalse));
   });
@@ -117,9 +117,9 @@
   group("for a skipped test", () {
     test("doesn't run the test's body", () async {
       var bodyRun = false;
-      var engine = withTests(declare(() {
+      var engine = declareEngine(() {
         test("test", () => bodyRun = true, skip: true);
-      }));
+      });
 
       await engine.run();
       expect(bodyRun, isFalse);
@@ -130,7 +130,9 @@
         test("test", () {}, skip: true);
       });
 
-      var engine = withTests(tests);
+      var engine = new Engine.withSuites([
+        new RunnerSuite(const VMEnvironment(), new Group.root(tests))
+      ]);
 
       engine.onTestStarted.listen(expectAsync((liveTest) {
         expect(liveTest, same(engine.liveTests.single));
@@ -151,10 +153,3 @@
     });
   });
 }
-
-/// Returns an engine that will run [tests].
-Engine withTests(List<Test> tests) {
-  return new Engine.withSuites([
-    new RunnerSuite(const VMEnvironment(), new Group.root(tests))
-  ]);
-}
diff --git a/test/runner/set_up_all_test.dart b/test/runner/set_up_all_test.dart
new file mode 100644
index 0000000..2301eb4
--- /dev/null
+++ b/test/runner/set_up_all_test.dart
@@ -0,0 +1,120 @@
+// 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.
+
+@TestOn("vm")
+
+import 'package:scheduled_test/descriptor.dart' as d;
+import 'package:scheduled_test/scheduled_stream.dart';
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../io.dart';
+
+void main() {
+  useSandbox();
+
+  test("an error causes the run to fail", () {
+    d.file("test.dart", r"""
+        import 'package:test/test.dart';
+
+        void main() {
+          setUpAll(() => throw "oh no");
+
+          test("test", () {});
+        }
+        """).create();
+
+    var test = runTest(["test.dart"]);
+    test.stdout.expect(consumeThrough(contains("-1: (setUpAll)")));
+    test.stdout.expect(consumeThrough(contains("-1: Some tests failed.")));
+    test.shouldExit(1);
+  });
+
+  test("doesn't run if no tests in the group are selected", () {
+    d.file("test.dart", r"""
+        import 'package:test/test.dart';
+
+        void main() {
+          group("with setUpAll", () {
+            setUpAll(() => throw "oh no");
+
+            test("test", () {});
+          });
+
+          group("without setUpAll", () {
+            test("test", () {});
+          });
+        }
+        """).create();
+
+    var test = runTest(["test.dart", "--name", "without"]);
+    test.stdout.expect(never(contains("(setUpAll)")));
+    test.shouldExit(0);
+  });
+
+  test("doesn't run if no tests in the group are selected", () {
+    d.file("test.dart", r"""
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group", () {
+            setUpAll(() => throw "oh no");
+
+            test("with", () {});
+          });
+
+          group("group", () {
+            test("without", () {});
+          });
+        }
+        """).create();
+
+    var test = runTest(["test.dart", "--name", "without"]);
+    test.stdout.expect(never(contains("(setUpAll)")));
+    test.shouldExit(0);
+  });
+
+  test("doesn't run if no tests in the group match the platform", () {
+    d.file("test.dart", r"""
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group", () {
+            setUpAll(() => throw "oh no");
+
+            test("with", () {}, testOn: "browser");
+          });
+
+          group("group", () {
+            test("without", () {});
+          });
+        }
+        """).create();
+
+    var test = runTest(["test.dart"]);
+    test.stdout.expect(never(contains("(setUpAll)")));
+    test.shouldExit(0);
+  });
+
+  test("doesn't run if the group doesn't match the platform", () {
+    d.file("test.dart", r"""
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group", () {
+            setUpAll(() => throw "oh no");
+
+            test("with", () {});
+          }, testOn: "browser");
+
+          group("group", () {
+            test("without", () {});
+          });
+        }
+        """).create();
+
+    var test = runTest(["test.dart"]);
+    test.stdout.expect(never(contains("(setUpAll)")));
+    test.shouldExit(0);
+  });
+}
diff --git a/test/runner/signal_test.dart b/test/runner/signal_test.dart
index 5397d07..63e8e7b 100644
--- a/test/runner/signal_test.dart
+++ b/test/runner/signal_test.dart
@@ -82,7 +82,11 @@
 import 'package:test/test.dart';
 
 void main() {
-  tearDown(() => new File("output").writeAsStringSync("ran teardown"));
+  tearDownAll(() {
+    new File("output_all").writeAsStringSync("ran tearDownAll");
+  });
+
+  tearDown(() => new File("output").writeAsStringSync("ran tearDown"));
 
   test("test", () {
     print("running test");
@@ -95,7 +99,34 @@
       test.stdout.expect(consumeThrough("running test"));
       signalAndQuit(test);
 
-      d.file("output", "ran teardown").validate();
+      d.file("output", "ran tearDown").validate();
+      d.file("output_all", "ran tearDownAll").validate();
+      expectTempDirEmpty();
+    });
+
+    test("waits for an active tearDownAll to finish running", () {
+      d.file("test.dart", """
+import 'dart:async';
+import 'dart:io';
+
+import 'package:test/test.dart';
+
+void main() {
+  tearDownAll(() async {
+    print("running tearDownAll");
+    await new Future.delayed(new Duration(seconds: 1));
+    new File("output").writeAsStringSync("ran tearDownAll");
+  });
+
+  test("test", () {});
+}
+""").create();
+
+      var test = _runTest(["test.dart"]);
+      test.stdout.expect(consumeThrough("running tearDownAll"));
+      signalAndQuit(test);
+
+      d.file("output", "ran tearDownAll").validate();
       expectTempDirEmpty();
     });
 
diff --git a/test/runner/tear_down_all_test.dart b/test/runner/tear_down_all_test.dart
new file mode 100644
index 0000000..9b192c3
--- /dev/null
+++ b/test/runner/tear_down_all_test.dart
@@ -0,0 +1,120 @@
+// 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.
+
+@TestOn("vm")
+
+import 'package:scheduled_test/descriptor.dart' as d;
+import 'package:scheduled_test/scheduled_stream.dart';
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../io.dart';
+
+void main() {
+  useSandbox();
+
+  test("an error causes the run to fail", () {
+    d.file("test.dart", r"""
+        import 'package:test/test.dart';
+
+        void main() {
+          tearDownAll(() => throw "oh no");
+
+          test("test", () {});
+        }
+        """).create();
+
+    var test = runTest(["test.dart"]);
+    test.stdout.expect(consumeThrough(contains("-1: (tearDownAll)")));
+    test.stdout.expect(consumeThrough(contains("-1: Some tests failed.")));
+    test.shouldExit(1);
+  });
+
+  test("doesn't run if no tests in the group are selected", () {
+    d.file("test.dart", r"""
+        import 'package:test/test.dart';
+
+        void main() {
+          group("with tearDownAll", () {
+            tearDownAll(() => throw "oh no");
+
+            test("test", () {});
+          });
+
+          group("without tearDownAll", () {
+            test("test", () {});
+          });
+        }
+        """).create();
+
+    var test = runTest(["test.dart", "--name", "without"]);
+    test.stdout.expect(never(contains("(tearDownAll)")));
+    test.shouldExit(0);
+  });
+
+  test("doesn't run if no tests in the group are selected", () {
+    d.file("test.dart", r"""
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group", () {
+            tearDownAll(() => throw "oh no");
+
+            test("with", () {});
+          });
+
+          group("group", () {
+            test("without", () {});
+          });
+        }
+        """).create();
+
+    var test = runTest(["test.dart", "--name", "without"]);
+    test.stdout.expect(never(contains("(tearDownAll)")));
+    test.shouldExit(0);
+  });
+
+  test("doesn't run if no tests in the group match the platform", () {
+    d.file("test.dart", r"""
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group", () {
+            tearDownAll(() => throw "oh no");
+
+            test("with", () {}, testOn: "browser");
+          });
+
+          group("group", () {
+            test("without", () {});
+          });
+        }
+        """).create();
+
+    var test = runTest(["test.dart"]);
+    test.stdout.expect(never(contains("(tearDownAll)")));
+    test.shouldExit(0);
+  });
+
+  test("doesn't run if the group doesn't match the platform", () {
+    d.file("test.dart", r"""
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group", () {
+            tearDownAll(() => throw "oh no");
+
+            test("with", () {});
+          }, testOn: "browser");
+
+          group("group", () {
+            test("without", () {});
+          });
+        }
+        """).create();
+
+    var test = runTest(["test.dart"]);
+    test.stdout.expect(never(contains("(tearDownAll)")));
+    test.shouldExit(0);
+  });
+}
diff --git a/test/utils.dart b/test/utils.dart
index acbec12..9662716 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -16,7 +16,10 @@
 import 'package:test/src/backend/state.dart';
 import 'package:test/src/backend/suite.dart';
 import 'package:test/src/runner/application_exception.dart';
+import 'package:test/src/runner/engine.dart';
 import 'package:test/src/runner/load_exception.dart';
+import 'package:test/src/runner/runner_suite.dart';
+import 'package:test/src/runner/vm/environment.dart';
 import 'package:test/src/util/remote_exception.dart';
 import 'package:test/test.dart';
 
@@ -284,8 +287,29 @@
   return future;
 }
 
+/// Runs [body] with a declarer, runs all the declared tests, and asserts that
+/// they pass.
+Future expectTestsPass(void body()) async {
+  var engine = declareEngine(body);
+  var success = await engine.run();
+
+  for (var test in engine.liveTests) {
+    expectTestPassed(test);
+  }
+
+  expect(success, isTrue);
+}
+
 /// Runs [body] with a declarer and returns the declared entries.
 List<GroupEntry> declare(void body()) {
   var declarer = new Declarer()..declare(body);
   return declarer.build().entries;
 }
+
+/// Runs [body] with a declarer and returns an engine that runs those tests.
+Engine declareEngine(void body()) {
+  var declarer = new Declarer()..declare(body);
+  return new Engine.withSuites([
+    new RunnerSuite(const VMEnvironment(), declarer.build())
+  ]);
+}