Merge pull request #713 from dart-lang/add-tear-down-all

Make addTearDown() play nice with setUpAll()
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e38fd1f..4a87965 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## 0.12.27
 
+* When `addTearDown()` is called within a call to `setUpAll()`, it runs its
+  callback after *all* tests instead of running it after the `setUpAll()`
+  callback.
+
 * When running in an interactive terminal, the test runner now prints status
   lines as wide as the terminal and no wider.
 
diff --git a/lib/src/backend/declarer.dart b/lib/src/backend/declarer.dart
index 4e8851d..29490eb 100644
--- a/lib/src/backend/declarer.dart
+++ b/lib/src/backend/declarer.dart
@@ -155,11 +155,15 @@
         }
       }
 
-      await Invoker.current.waitForOutstandingCallbacks(() async {
-        await _runSetUps();
-        await body();
-      });
-    }, trace: _collectTraces ? new Trace.current(2) : null));
+      await runZoned(
+          () => Invoker.current.waitForOutstandingCallbacks(() async {
+                await _runSetUps();
+                await body();
+              }),
+          // Make the declarer visible to running tests so that they'll throw
+          // useful errors when calling `test()` and `group()` within a test.
+          zoneValues: {#test.declarer: this});
+    }, trace: _collectTraces ? new Trace.current(2) : null, guarded: false));
   }
 
   /// Creates a group of tests.
@@ -224,7 +228,14 @@
     _tearDownAlls.add(callback);
   }
 
+  /// Like [tearDownAll], but called from within a running [setUpAll] test to
+  /// dynamically add a [tearDownAll].
+  void addTearDownAll(callback()) => _tearDownAlls.add(callback);
+
   /// Finalizes and returns the group being declared.
+  ///
+  /// **Note**: The tests in this group must be run in a [Invoker.guard]
+  /// context; otherwise, test errors won't be captured.
   Group build() {
     _checkNotBuilt("build");
 
@@ -258,18 +269,30 @@
     if (_setUpAlls.isEmpty) return null;
 
     return new LocalTest(_prefix("(setUpAll)"), _metadata, () {
-      return Future.forEach(_setUpAlls, (setUp) => setUp());
-    }, trace: _setUpAllTrace);
+      return runZoned(() => Future.forEach(_setUpAlls, (setUp) => setUp()),
+          // Make the declarer visible to running scaffolds so they can add to
+          // the declarer's `tearDownAll()` list.
+          zoneValues: {#test.declarer: this});
+    }, trace: _setUpAllTrace, guarded: false, isScaffoldAll: true);
   }
 
   /// Returns a [Test] that runs the callbacks in [_tearDownAll].
   Test get _tearDownAll {
-    if (_tearDownAlls.isEmpty) return null;
+    // We have to create a tearDownAll if there's a setUpAll, since it might
+    // dynamically add tear-down code using [addTearDownAll].
+    if (_setUpAlls.isEmpty && _tearDownAlls.isEmpty) return null;
 
     return new LocalTest(_prefix("(tearDownAll)"), _metadata, () {
-      return Invoker.current.unclosable(() {
-        return Future.forEach(_tearDownAlls.reversed, errorsDontStopTest);
-      });
-    }, trace: _tearDownAllTrace);
+      return runZoned(() {
+        return Invoker.current.unclosable(() async {
+          while (_tearDownAlls.isNotEmpty) {
+            await errorsDontStopTest(_tearDownAlls.removeLast());
+          }
+        });
+      },
+          // Make the declarer visible to running scaffolds so they can add to
+          // the declarer's `tearDownAll()` list.
+          zoneValues: {#test.declarer: this});
+    }, trace: _tearDownAllTrace, guarded: false, isScaffoldAll: true);
   }
 }
diff --git a/lib/src/backend/invoker.dart b/lib/src/backend/invoker.dart
index 12295c5..2335d08 100644
--- a/lib/src/backend/invoker.dart
+++ b/lib/src/backend/invoker.dart
@@ -10,6 +10,7 @@
 import '../runner/load_suite.dart';
 import '../utils.dart';
 import 'closed_exception.dart';
+import 'declarer.dart';
 import 'group.dart';
 import 'live_test.dart';
 import 'live_test_controller.dart';
@@ -28,21 +29,38 @@
   final Metadata metadata;
   final Trace trace;
 
-  /// The test body.
-  final AsyncFunction _body;
+  /// Whether this is a test defined using `setUpAll()` or `tearDownAll()`.
+  final bool isScaffoldAll;
 
-  LocalTest(this.name, this.metadata, body(), {this.trace}) : _body = body;
+  /// The test body.
+  final Function() _body;
+
+  /// Whether the test is run in its own error zone.
+  final bool _guarded;
+
+  /// Creates a new [LocalTest].
+  ///
+  /// If [guarded] is `true`, the test is run in its own error zone, and any
+  /// errors that escape that zone cause the test to fail. If it's `false`, it's
+  /// the caller's responsiblity to invoke [LiveTest.run] in the context of a
+  /// call to [Invoker.guard].
+  LocalTest(this.name, this.metadata, this._body,
+      {this.trace, bool guarded: true, this.isScaffoldAll: false})
+      : _guarded = guarded;
+
+  LocalTest._(this.name, this.metadata, this._body, this.trace, this._guarded,
+      this.isScaffoldAll);
 
   /// Loads a single runnable instance of this test.
   LiveTest load(Suite suite, {Iterable<Group> groups}) {
-    var invoker = new Invoker._(suite, this, groups: groups);
+    var invoker = new Invoker._(suite, this, groups: groups, guarded: _guarded);
     return invoker.liveTest;
   }
 
   Test forPlatform(TestPlatform platform, {OperatingSystem os}) {
     if (!metadata.testOn.evaluate(platform, os: os)) return null;
-    return new LocalTest(name, metadata.forPlatform(platform, os: os), _body,
-        trace: trace);
+    return new LocalTest._(name, metadata.forPlatform(platform, os: os), _body,
+        trace, _guarded, isScaffoldAll);
   }
 }
 
@@ -58,6 +76,9 @@
   LiveTest get liveTest => _controller.liveTest;
   LiveTestController _controller;
 
+  /// Whether to run this test in its own error zone.
+  final bool _guarded;
+
   /// Whether the test can be closed in the current zone.
   bool get _closable => Zone.current[_closableKey];
 
@@ -118,6 +139,22 @@
     return Zone.current[#test.invoker];
   }
 
+  /// Runs [callback] in a zone where unhandled errors from [LiveTest]s are
+  /// caught and dispatched to the appropriate [Invoker].
+  static T guard<T>(T callback()) =>
+      runZoned(callback, zoneSpecification: new ZoneSpecification(
+          // Use [handleUncaughtError] rather than [onError] so we can
+          // capture [zone] and with it the outstanding callback counter for
+          // the zone in which [error] was thrown.
+          handleUncaughtError: (self, _, zone, error, stackTrace) {
+        var invoker = zone[#test.invoker];
+        if (invoker != null) {
+          self.parent.run(() => invoker._handleError(zone, error, stackTrace));
+        } else {
+          self.parent.handleUncaughtError(error, stackTrace);
+        }
+      }));
+
   /// The zone that the top level of [_test.body] is running in.
   ///
   /// Tracking this ensures that [_timeoutTimer] isn't created in a
@@ -135,7 +172,9 @@
   /// Messages to print if and when this test fails.
   final _printsOnFailure = <String>[];
 
-  Invoker._(Suite suite, LocalTest test, {Iterable<Group> groups}) {
+  Invoker._(Suite suite, LocalTest test,
+      {Iterable<Group> groups, bool guarded: true})
+      : _guarded = guarded {
     _controller = new LiveTestController(
         suite, test, _onRun, _onCloseCompleter.complete,
         groups: groups);
@@ -147,7 +186,12 @@
   /// run in the reverse of the order they're declared.
   void addTearDown(callback()) {
     if (closed) throw new ClosedException();
-    _tearDowns.add(callback);
+
+    if (_test.isScaffoldAll) {
+      Declarer.current.addTearDownAll(callback);
+    } else {
+      _tearDowns.add(callback);
+    }
   }
 
   /// Tells the invoker that there's a callback running that it should wait for
@@ -282,7 +326,15 @@
   void _handleError(Zone zone, error, [StackTrace stackTrace]) {
     // Ignore errors propagated from previous test runs
     if (_runCount != zone[#runCount]) return;
-    if (stackTrace == null) stackTrace = new Chain.current();
+
+    // Get the chain information from the zone in which the error was thrown.
+    zone.run(() {
+      if (stackTrace == null) {
+        stackTrace = new Chain.current();
+      } else {
+        stackTrace = new Chain.forTrace(stackTrace);
+      }
+    });
 
     // Store these here because they'll change when we set the state below.
     var shouldBeDone = liveTest.state.shouldBeDone;
@@ -334,60 +386,68 @@
 
     _runCount++;
     Chain.capture(() {
-      runZoned(() async {
-        _invokerZone = Zone.current;
-        _outstandingCallbackZones.add(Zone.current);
+      _guardIfGuarded(() {
+        runZoned(() async {
+          _invokerZone = Zone.current;
+          _outstandingCallbackZones.add(Zone.current);
 
-        // Run the test asynchronously so that the "running" state change has
-        // a chance to hit its event handler(s) before the test produces an
-        // error. If an error is emitted before the first state change is
-        // handled, we can end up with [onError] callbacks firing before the
-        // corresponding [onStateChkange], which violates the timing
-        // guarantees.
-        //
-        // Using [new Future] also avoids starving the DOM or other
-        // microtask-level events.
-        new Future(() async {
-          await _test._body();
-          await unclosable(_runTearDowns);
-          removeOutstandingCallback();
-        });
+          // Run the test asynchronously so that the "running" state change
+          // has a chance to hit its event handler(s) before the test produces
+          // an error. If an error is emitted before the first state change is
+          // handled, we can end up with [onError] callbacks firing before the
+          // corresponding [onStateChkange], which violates the timing
+          // guarantees.
+          //
+          // Using [new Future] also avoids starving the DOM or other
+          // microtask-level events.
+          new Future(() async {
+            await _test._body();
+            await unclosable(_runTearDowns);
+            removeOutstandingCallback();
+          });
 
-        await _outstandingCallbacks.noOutstandingCallbacks;
-        if (_timeoutTimer != null) _timeoutTimer.cancel();
+          await _outstandingCallbacks.noOutstandingCallbacks;
+          if (_timeoutTimer != null) _timeoutTimer.cancel();
 
-        if (liveTest.state.result != Result.success &&
-            _runCount < liveTest.test.metadata.retry + 1) {
+          if (liveTest.state.result != Result.success &&
+              _runCount < liveTest.test.metadata.retry + 1) {
+            _controller
+                .message(new Message.print("Retry: ${liveTest.test.name}"));
+            _onRun();
+            return;
+          }
+
           _controller
-              .message(new Message.print("Retry: ${liveTest.test.name}"));
-          _onRun();
-          return;
-        }
+              .setState(new State(Status.complete, liveTest.state.result));
 
-        _controller.setState(new State(Status.complete, liveTest.state.result));
-
-        _controller.completer.complete();
-      },
-          zoneValues: {
-            #test.invoker: this,
-            // Use the invoker as a key so that multiple invokers can have different
-            // outstanding callback counters at once.
-            _counterKey: outstandingCallbacksForBody,
-            _closableKey: true,
-            #runCount: _runCount
-          },
-          zoneSpecification: new ZoneSpecification(
-              print: (self, parent, zone, line) =>
-                  _controller.message(new Message.print(line)),
-              // Use [handleUncaughtError] rather than [onError] so we can
-              // capture [zone] and with it the outstanding callback counter for
-              // the zone in which [error] was thrown.
-              handleUncaughtError: (self, _, zone, error, stackTrace) => self
-                  .parent
-                  .run(() => _handleError(zone, error, stackTrace))));
-    }, when: liveTest.test.metadata.chainStackTraces);
+          _controller.completer.complete();
+        },
+            zoneValues: {
+              #test.invoker: this,
+              // Use the invoker as a key so that multiple invokers can have
+              // different outstanding callback counters at once.
+              _counterKey: outstandingCallbacksForBody,
+              _closableKey: true,
+              #runCount: _runCount,
+            },
+            zoneSpecification: new ZoneSpecification(
+                print: (_, __, ___, line) => _print(line)));
+      });
+    }, when: liveTest.test.metadata.chainStackTraces, errorZone: false);
   }
 
+  /// Runs [callback], in a [Invoker.guard] context if [_guarded] is `true`.
+  void _guardIfGuarded(void callback()) {
+    if (_guarded) {
+      Invoker.guard(callback);
+    } else {
+      callback();
+    }
+  }
+
+  /// Prints [text] as a message to [_controller].
+  void _print(String text) => _controller.message(new Message.print(text));
+
   /// Run [_tearDowns] in reverse order.
   Future _runTearDowns() async {
     while (_tearDowns.isNotEmpty) {
diff --git a/lib/src/runner/remote_listener.dart b/lib/src/runner/remote_listener.dart
index 883101f..bf08c44 100644
--- a/lib/src/runner/remote_listener.dart
+++ b/lib/src/runner/remote_listener.dart
@@ -9,6 +9,7 @@
 
 import '../backend/declarer.dart';
 import '../backend/group.dart';
+import '../backend/invoker.dart';
 import '../backend/live_test.dart';
 import '../backend/metadata.dart';
 import '../backend/operating_system.dart';
@@ -97,7 +98,14 @@
               : OperatingSystem.find(message['os']),
           path: message['path']);
 
-      new RemoteListener._(suite, printZone)._listen(channel);
+      runZoned(() {
+        Invoker.guard(
+            () => new RemoteListener._(suite, printZone)._listen(channel));
+      },
+          // Make the declarer visible to running tests so that they'll throw
+          // useful errors when calling `test()` and `group()` within a test,
+          // and so they can add to the declarer's `tearDownAll()` list.
+          zoneValues: {#test.declarer: declarer});
     }, onError: (error, stackTrace) {
       _sendError(channel, error, stackTrace, verboseChain);
     }, zoneSpecification: new ZoneSpecification(print: (_, __, ___, line) {
diff --git a/lib/test.dart b/lib/test.dart
index 2550bef..8d159ad 100644
--- a/lib/test.dart
+++ b/lib/test.dart
@@ -67,7 +67,8 @@
     ExpandedReporter.watch(engine,
         color: true, printPath: false, printPlatform: false);
 
-    var success = await engine.run();
+    var success = await runZoned(() => Invoker.guard(engine.run),
+        zoneValues: {#test.declarer: _globalDeclarer});
     // TODO(nweiz): Set the exit code on the VM when issue 6943 is fixed.
     if (success) return null;
     print('');
@@ -252,6 +253,9 @@
 ///
 /// The [callback] is run before any callbacks registered with [tearDown]. Like
 /// [tearDown], the most recently registered callback is run first.
+///
+/// If this is called from within a [setUpAll] or [tearDownAll] callback, it
+/// instead runs the function after *all* tests in the current test suite.
 void addTearDown(callback()) {
   if (Invoker.current == null) {
     throw new StateError("addTearDown() may only be called within a test.");
diff --git a/pubspec.yaml b/pubspec.yaml
index 5c20d16..a730642 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test
-version: 0.12.27-dev
+version: 0.12.27
 author: Dart Team <misc@dartlang.org>
 description: A library for writing dart unit tests.
 homepage: https://github.com/dart-lang/test
@@ -29,7 +29,7 @@
   source_map_stack_trace: '^1.1.4'
   source_maps: '^0.10.2'
   source_span: '^1.4.0'
-  stack_trace: '^1.6.0'
+  stack_trace: '^1.9.0'
   stream_channel: '^1.6.0'
   string_scanner: '>=0.1.1 <2.0.0'
   term_glyph: '^1.0.0'
diff --git a/test/frontend/add_tear_down_test.dart b/test/frontend/add_tear_down_test.dart
index 650f0fd..3a63ff8 100644
--- a/test/frontend/add_tear_down_test.dart
+++ b/test/frontend/add_tear_down_test.dart
@@ -7,319 +7,25 @@
 import 'package:async/async.dart';
 import 'package:test/test.dart';
 
+import 'package:test/src/backend/declarer.dart';
+
 import '../utils.dart';
 
 void main() {
-  test("runs after the test body", () {
-    return expectTestsPass(() {
-      var test1Run = false;
-      var tearDownRun = false;
-      test("test 1", () {
-        addTearDown(() {
-          expect(test1Run, isTrue);
-          expect(tearDownRun, isFalse);
-          tearDownRun = true;
-        });
-
-        expect(tearDownRun, isFalse);
-        test1Run = true;
-      });
-
-      test("test 2", () {
-        expect(tearDownRun, isTrue);
-      });
-    });
-  });
-
-  test("multiples run in reverse order", () {
-    return expectTestsPass(() {
-      var tearDown1Run = false;
-      var tearDown2Run = false;
-      var tearDown3Run = false;
-
-      test("test 1", () {
-        addTearDown(() {
-          expect(tearDown1Run, isFalse);
-          expect(tearDown2Run, isTrue);
-          expect(tearDown3Run, isTrue);
-          tearDown1Run = true;
-        });
-
-        addTearDown(() {
-          expect(tearDown1Run, isFalse);
-          expect(tearDown2Run, isFalse);
-          expect(tearDown3Run, isTrue);
-          tearDown2Run = true;
-        });
-
-        addTearDown(() {
-          expect(tearDown1Run, isFalse);
-          expect(tearDown2Run, isFalse);
-          expect(tearDown3Run, isFalse);
-          tearDown3Run = true;
-        });
-
-        expect(tearDown1Run, isFalse);
-        expect(tearDown2Run, isFalse);
-        expect(tearDown3Run, isFalse);
-      });
-
-      test("test 2", () {
-        expect(tearDown1Run, isTrue);
-        expect(tearDown2Run, isTrue);
-        expect(tearDown3Run, isTrue);
-      });
-    });
-  });
-
-  test("can be called in addTearDown", () {
-    return expectTestsPass(() {
-      var tearDown2Run = false;
-      var tearDown3Run = false;
-
-      test("test 1", () {
-        addTearDown(() {
-          expect(tearDown2Run, isTrue);
-          expect(tearDown3Run, isFalse);
-          tearDown3Run = true;
-        });
-
-        addTearDown(() {
-          addTearDown(() {
-            expect(tearDown2Run, isFalse);
-            expect(tearDown3Run, isFalse);
-            tearDown2Run = true;
-          });
-        });
-      });
-
-      test("test 2", () {
-        expect(tearDown2Run, isTrue);
-        expect(tearDown3Run, isTrue);
-      });
-    });
-  });
-
-  test("can be called in tearDown", () {
-    return expectTestsPass(() {
-      var tearDown2Run = false;
-      var tearDown3Run = false;
-
-      tearDown(() {
-        expect(tearDown2Run, isTrue);
-        expect(tearDown3Run, isFalse);
-        tearDown3Run = true;
-      });
-
-      tearDown(() {
-        tearDown2Run = false;
-        tearDown3Run = false;
-
-        addTearDown(() {
-          expect(tearDown2Run, isFalse);
-          expect(tearDown3Run, isFalse);
-          tearDown2Run = true;
-        });
-      });
-
-      test("test 1", () {});
-
-      test("test 2", () {
-        expect(tearDown2Run, isTrue);
-        expect(tearDown3Run, isTrue);
-      });
-    });
-  });
-
-  test("runs before a normal tearDown", () {
-    return expectTestsPass(() {
-      var groupTearDownRun = false;
-      var testTearDownRun = false;
-      group("group", () {
-        tearDown(() {
-          expect(testTearDownRun, isTrue);
-          expect(groupTearDownRun, isFalse);
-          groupTearDownRun = true;
-        });
-
-        test("test 1", () {
-          addTearDown(() {
-            expect(groupTearDownRun, isFalse);
-            expect(testTearDownRun, isFalse);
-            testTearDownRun = true;
-          });
-
-          expect(groupTearDownRun, isFalse);
-          expect(testTearDownRun, isFalse);
-        });
-      });
-
-      test("test 2", () {
-        expect(groupTearDownRun, isTrue);
-        expect(testTearDownRun, isTrue);
-      });
-    });
-  });
-
-  test("runs in the same error zone as the test", () {
-    return expectTestsPass(() {
-      test("test", () {
-        var future = new Future.error("oh no");
-        expect(future, throwsA("oh no"));
-
-        addTearDown(() {
-          // If the tear-down is in a different error zone than the test, the
-          // error will try to cross the zone boundary and get top-leveled.
-          expect(future, throwsA("oh no"));
-        });
-      });
-    });
-  });
-
-  group("asynchronously", () {
-    test("blocks additional test tearDowns on in-band async", () {
+  group("in a test", () {
+    test("runs after the test body", () {
       return expectTestsPass(() {
-        var tearDown1Run = false;
-        var tearDown2Run = false;
-        var tearDown3Run = false;
-        test("test", () {
-          addTearDown(() async {
-            expect(tearDown1Run, isFalse);
-            expect(tearDown2Run, isTrue);
-            expect(tearDown3Run, isTrue);
-            await pumpEventQueue();
-            tearDown1Run = true;
-          });
-
-          addTearDown(() async {
-            expect(tearDown1Run, isFalse);
-            expect(tearDown2Run, isFalse);
-            expect(tearDown3Run, isTrue);
-            await pumpEventQueue();
-            tearDown2Run = true;
-          });
-
-          addTearDown(() async {
-            expect(tearDown1Run, isFalse);
-            expect(tearDown2Run, isFalse);
-            expect(tearDown3Run, isFalse);
-            await pumpEventQueue();
-            tearDown3Run = true;
-          });
-
-          expect(tearDown1Run, isFalse);
-          expect(tearDown2Run, isFalse);
-          expect(tearDown3Run, isFalse);
-        });
-      });
-    });
-
-    test("doesn't block additional test tearDowns on out-of-band async", () {
-      return expectTestsPass(() {
-        var tearDown1Run = false;
-        var tearDown2Run = false;
-        var tearDown3Run = false;
-        test("test", () {
-          addTearDown(() {
-            expect(tearDown1Run, isFalse);
-            expect(tearDown2Run, isFalse);
-            expect(tearDown3Run, isFalse);
-
-            expect(new Future(() {
-              tearDown1Run = true;
-            }), completes);
-          });
-
-          addTearDown(() {
-            expect(tearDown1Run, isFalse);
-            expect(tearDown2Run, isFalse);
-            expect(tearDown3Run, isFalse);
-
-            expect(new Future(() {
-              tearDown2Run = true;
-            }), completes);
-          });
-
-          addTearDown(() {
-            expect(tearDown1Run, isFalse);
-            expect(tearDown2Run, isFalse);
-            expect(tearDown3Run, isFalse);
-
-            expect(new Future(() {
-              tearDown3Run = true;
-            }), completes);
-          });
-
-          expect(tearDown1Run, isFalse);
-          expect(tearDown2Run, isFalse);
-          expect(tearDown3Run, isFalse);
-        });
-      });
-    });
-
-    test("blocks additional group tearDowns on in-band async", () {
-      return expectTestsPass(() {
-        var groupTearDownRun = false;
-        var testTearDownRun = false;
-        tearDown(() async {
-          expect(groupTearDownRun, isFalse);
-          expect(testTearDownRun, isTrue);
-          await pumpEventQueue();
-          groupTearDownRun = true;
-        });
-
-        test("test", () {
-          addTearDown(() async {
-            expect(groupTearDownRun, isFalse);
-            expect(testTearDownRun, isFalse);
-            await pumpEventQueue();
-            testTearDownRun = true;
-          });
-
-          expect(groupTearDownRun, isFalse);
-          expect(testTearDownRun, isFalse);
-        });
-      });
-    });
-
-    test("doesn't block additional group tearDowns on out-of-band async", () {
-      return expectTestsPass(() {
-        var groupTearDownRun = false;
-        var testTearDownRun = false;
-        tearDown(() {
-          expect(groupTearDownRun, isFalse);
-          expect(testTearDownRun, isFalse);
-
-          expect(new Future(() {
-            groupTearDownRun = true;
-          }), completes);
-        });
-
-        test("test", () {
-          addTearDown(() {
-            expect(groupTearDownRun, isFalse);
-            expect(testTearDownRun, isFalse);
-
-            expect(new Future(() {
-              testTearDownRun = true;
-            }), completes);
-          });
-
-          expect(groupTearDownRun, isFalse);
-          expect(testTearDownRun, isFalse);
-        });
-      });
-    });
-
-    test("blocks further tests on in-band async", () {
-      return expectTestsPass(() {
+        var test1Run = false;
         var tearDownRun = false;
         test("test 1", () {
-          addTearDown(() async {
+          addTearDown(() {
+            expect(test1Run, isTrue);
             expect(tearDownRun, isFalse);
-            await pumpEventQueue();
             tearDownRun = true;
           });
+
+          expect(tearDownRun, isFalse);
+          test1Run = true;
         });
 
         test("test 2", () {
@@ -328,74 +34,768 @@
       });
     });
 
-    test("blocks further tests on out-of-band async", () {
+    test("multiples run in reverse order", () {
       return expectTestsPass(() {
-        var tearDownRun = false;
+        var tearDown1Run = false;
+        var tearDown2Run = false;
+        var tearDown3Run = false;
+
         test("test 1", () {
-          addTearDown(() async {
-            expect(tearDownRun, isFalse);
-            expect(
-                pumpEventQueue().then((_) {
-                  tearDownRun = true;
-                }),
-                completes);
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isTrue);
+            expect(tearDown3Run, isTrue);
+            tearDown1Run = true;
+          });
+
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isTrue);
+            tearDown2Run = true;
+          });
+
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+            tearDown3Run = true;
+          });
+
+          expect(tearDown1Run, isFalse);
+          expect(tearDown2Run, isFalse);
+          expect(tearDown3Run, isFalse);
+        });
+
+        test("test 2", () {
+          expect(tearDown1Run, isTrue);
+          expect(tearDown2Run, isTrue);
+          expect(tearDown3Run, isTrue);
+        });
+      });
+    });
+
+    test("can be called in addTearDown", () {
+      return expectTestsPass(() {
+        var tearDown2Run = false;
+        var tearDown3Run = false;
+
+        test("test 1", () {
+          addTearDown(() {
+            expect(tearDown2Run, isTrue);
+            expect(tearDown3Run, isFalse);
+            tearDown3Run = true;
+          });
+
+          addTearDown(() {
+            addTearDown(() {
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+              tearDown2Run = true;
+            });
           });
         });
 
-        test("after", () {
-          expect(tearDownRun, isTrue);
+        test("test 2", () {
+          expect(tearDown2Run, isTrue);
+          expect(tearDown3Run, isTrue);
         });
       });
     });
+
+    test("can be called in tearDown", () {
+      return expectTestsPass(() {
+        var tearDown2Run = false;
+        var tearDown3Run = false;
+
+        tearDown(() {
+          expect(tearDown2Run, isTrue);
+          expect(tearDown3Run, isFalse);
+          tearDown3Run = true;
+        });
+
+        tearDown(() {
+          tearDown2Run = false;
+          tearDown3Run = false;
+
+          addTearDown(() {
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+            tearDown2Run = true;
+          });
+        });
+
+        test("test 1", () {});
+
+        test("test 2", () {
+          expect(tearDown2Run, isTrue);
+          expect(tearDown3Run, isTrue);
+        });
+      });
+    });
+
+    test("runs before a normal tearDown", () {
+      return expectTestsPass(() {
+        var groupTearDownRun = false;
+        var testTearDownRun = false;
+        group("group", () {
+          tearDown(() {
+            expect(testTearDownRun, isTrue);
+            expect(groupTearDownRun, isFalse);
+            groupTearDownRun = true;
+          });
+
+          test("test 1", () {
+            addTearDown(() {
+              expect(groupTearDownRun, isFalse);
+              expect(testTearDownRun, isFalse);
+              testTearDownRun = true;
+            });
+
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+          });
+        });
+
+        test("test 2", () {
+          expect(groupTearDownRun, isTrue);
+          expect(testTearDownRun, isTrue);
+        });
+      });
+    });
+
+    test("runs in the same error zone as the test", () {
+      return expectTestsPass(() {
+        test("test", () {
+          var future = new Future.error("oh no");
+          expect(future, throwsA("oh no"));
+
+          addTearDown(() {
+            // If the tear-down is in a different error zone than the test, the
+            // error will try to cross the zone boundary and get top-leveled.
+            expect(future, throwsA("oh no"));
+          });
+        });
+      });
+    });
+
+    group("asynchronously", () {
+      test("blocks additional test tearDowns on in-band async", () {
+        return expectTestsPass(() {
+          var tearDown1Run = false;
+          var tearDown2Run = false;
+          var tearDown3Run = false;
+          test("test", () {
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isTrue);
+              expect(tearDown3Run, isTrue);
+              await pumpEventQueue();
+              tearDown1Run = true;
+            });
+
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isTrue);
+              await pumpEventQueue();
+              tearDown2Run = true;
+            });
+
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+              await pumpEventQueue();
+              tearDown3Run = true;
+            });
+
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+          });
+        });
+      });
+
+      test("doesn't block additional test tearDowns on out-of-band async", () {
+        return expectTestsPass(() {
+          var tearDown1Run = false;
+          var tearDown2Run = false;
+          var tearDown3Run = false;
+          test("test", () {
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(new Future(() {
+                tearDown1Run = true;
+              }), completes);
+            });
+
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(new Future(() {
+                tearDown2Run = true;
+              }), completes);
+            });
+
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(new Future(() {
+                tearDown3Run = true;
+              }), completes);
+            });
+
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+          });
+        });
+      });
+
+      test("blocks additional group tearDowns on in-band async", () {
+        return expectTestsPass(() {
+          var groupTearDownRun = false;
+          var testTearDownRun = false;
+          tearDown(() async {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isTrue);
+            await pumpEventQueue();
+            groupTearDownRun = true;
+          });
+
+          test("test", () {
+            addTearDown(() async {
+              expect(groupTearDownRun, isFalse);
+              expect(testTearDownRun, isFalse);
+              await pumpEventQueue();
+              testTearDownRun = true;
+            });
+
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+          });
+        });
+      });
+
+      test("doesn't block additional group tearDowns on out-of-band async", () {
+        return expectTestsPass(() {
+          var groupTearDownRun = false;
+          var testTearDownRun = false;
+          tearDown(() {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+
+            expect(new Future(() {
+              groupTearDownRun = true;
+            }), completes);
+          });
+
+          test("test", () {
+            addTearDown(() {
+              expect(groupTearDownRun, isFalse);
+              expect(testTearDownRun, isFalse);
+
+              expect(new Future(() {
+                testTearDownRun = true;
+              }), completes);
+            });
+
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+          });
+        });
+      });
+
+      test("blocks further tests on in-band async", () {
+        return expectTestsPass(() {
+          var tearDownRun = false;
+          test("test 1", () {
+            addTearDown(() async {
+              expect(tearDownRun, isFalse);
+              await pumpEventQueue();
+              tearDownRun = true;
+            });
+          });
+
+          test("test 2", () {
+            expect(tearDownRun, isTrue);
+          });
+        });
+      });
+
+      test("blocks further tests on out-of-band async", () {
+        return expectTestsPass(() {
+          var tearDownRun = false;
+          test("test 1", () {
+            addTearDown(() async {
+              expect(tearDownRun, isFalse);
+              expect(
+                  pumpEventQueue().then((_) {
+                    tearDownRun = true;
+                  }),
+                  completes);
+            });
+          });
+
+          test("after", () {
+            expect(tearDownRun, isTrue);
+          });
+        });
+      });
+    });
+
+    group("with an error", () {
+      test("reports the error", () async {
+        var engine = declareEngine(() {
+          test("test", () {
+            addTearDown(() => throw new TestFailure("fail"));
+          });
+        });
+
+        var queue = new StreamQueue(engine.onTestStarted);
+        var liveTestFuture = queue.next;
+
+        expect(await engine.run(), isFalse);
+
+        var liveTest = await liveTestFuture;
+        expect(liveTest.test.name, equals("test"));
+        expectTestFailed(liveTest, "fail");
+      });
+
+      test("runs further test tearDowns", () async {
+        // Declare this in the outer test so if it doesn't run, the outer test
+        // will fail.
+        var shouldRun = expectAsync0(() {});
+
+        var engine = declareEngine(() {
+          test("test", () {
+            addTearDown(() => throw "error");
+            addTearDown(shouldRun);
+          });
+        });
+
+        expect(await engine.run(), isFalse);
+      });
+
+      test("runs further group tearDowns", () async {
+        // Declare this in the outer test so if it doesn't run, the outer test
+        // will fail.
+        var shouldRun = expectAsync0(() {});
+
+        var engine = declareEngine(() {
+          tearDown(shouldRun);
+
+          test("test", () {
+            addTearDown(() => throw "error");
+          });
+        });
+
+        expect(await engine.run(), isFalse);
+      });
+    });
   });
 
-  group("with an error", () {
-    test("reports the error", () async {
-      var engine = declareEngine(() {
-        test("test", () {
-          addTearDown(() => throw new TestFailure("fail"));
+  group("in setUpAll()", () {
+    test("runs after all tests", () async {
+      var test1Run = false;
+      var test2Run = false;
+      var tearDownRun = false;
+      await expectTestsPass(() {
+        setUpAll(() {
+          addTearDown(() {
+            expect(test1Run, isTrue);
+            expect(test2Run, isTrue);
+            expect(tearDownRun, isFalse);
+            tearDownRun = true;
+          });
+        });
+
+        test("test 1", () {
+          test1Run = true;
+          expect(tearDownRun, isFalse);
+        });
+
+        test("test 2", () {
+          test2Run = true;
+          expect(tearDownRun, isFalse);
         });
       });
 
-      var queue = new StreamQueue(engine.onTestStarted);
-      var liveTestFuture = queue.next;
-
-      expect(await engine.run(), isFalse);
-
-      var liveTest = await liveTestFuture;
-      expect(liveTest.test.name, equals("test"));
-      expectTestFailed(liveTest, "fail");
+      expect(test1Run, isTrue);
+      expect(test2Run, isTrue);
+      expect(tearDownRun, isTrue);
     });
 
-    test("runs further test tearDowns", () async {
-      // Declare this in the outer test so if it doesn't run, the outer test
-      // will fail.
-      var shouldRun = expectAsync0(() {});
+    test("multiples run in reverse order", () async {
+      var tearDown1Run = false;
+      var tearDown2Run = false;
+      var tearDown3Run = false;
+      await expectTestsPass(() {
+        setUpAll(() {
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isTrue);
+            expect(tearDown3Run, isTrue);
+            tearDown1Run = true;
+          });
 
-      var engine = declareEngine(() {
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isTrue);
+            tearDown2Run = true;
+          });
+
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+            tearDown3Run = true;
+          });
+
+          expect(tearDown1Run, isFalse);
+          expect(tearDown2Run, isFalse);
+          expect(tearDown3Run, isFalse);
+        });
+
         test("test", () {
-          addTearDown(() => throw "error");
-          addTearDown(shouldRun);
+          expect(tearDown1Run, isFalse);
+          expect(tearDown2Run, isFalse);
+          expect(tearDown3Run, isFalse);
         });
       });
 
-      expect(await engine.run(), isFalse);
+      expect(tearDown1Run, isTrue);
+      expect(tearDown2Run, isTrue);
+      expect(tearDown3Run, isTrue);
     });
 
-    test("runs further group tearDowns", () async {
-      // Declare this in the outer test so if it doesn't run, the outer test
-      // will fail.
-      var shouldRun = expectAsync0(() {});
+    test("can be called in addTearDown", () async {
+      var tearDown2Run = false;
+      var tearDown3Run = false;
+      await expectTestsPass(() {
+        setUpAll(() {
+          addTearDown(() {
+            expect(tearDown2Run, isTrue);
+            expect(tearDown3Run, isFalse);
+            tearDown3Run = true;
+          });
 
-      var engine = declareEngine(() {
-        tearDown(shouldRun);
+          addTearDown(() {
+            addTearDown(() {
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+              tearDown2Run = true;
+            });
+          });
+        });
 
         test("test", () {
-          addTearDown(() => throw "error");
+          expect(tearDown2Run, isFalse);
+          expect(tearDown3Run, isFalse);
         });
       });
 
-      expect(await engine.run(), isFalse);
+      expect(tearDown2Run, isTrue);
+      expect(tearDown3Run, isTrue);
+    });
+
+    test("can be called in tearDownAll", () async {
+      var tearDown2Run = false;
+      var tearDown3Run = false;
+      await expectTestsPass(() {
+        tearDownAll(() {
+          expect(tearDown2Run, isTrue);
+          expect(tearDown3Run, isFalse);
+          tearDown3Run = true;
+        });
+
+        tearDownAll(() {
+          tearDown2Run = false;
+          tearDown3Run = false;
+
+          addTearDown(() {
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+            tearDown2Run = true;
+          });
+        });
+
+        test("test", () {});
+      });
+
+      expect(tearDown2Run, isTrue);
+      expect(tearDown3Run, isTrue);
+    });
+
+    test("runs before a normal tearDownAll", () async {
+      var groupTearDownRun = false;
+      var testTearDownRun = false;
+      await expectTestsPass(() {
+        tearDownAll(() {
+          expect(testTearDownRun, isTrue);
+          expect(groupTearDownRun, isFalse);
+          groupTearDownRun = true;
+        });
+
+        setUpAll(() {
+          addTearDown(() {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+            testTearDownRun = true;
+          });
+        });
+
+        test("test", () {
+          expect(groupTearDownRun, isFalse);
+          expect(testTearDownRun, isFalse);
+        });
+      });
+
+      expect(groupTearDownRun, isTrue);
+      expect(testTearDownRun, isTrue);
+    });
+
+    test("runs in the same error zone as the setUpAll", () async {
+      return expectTestsPass(() {
+        setUpAll(() {
+          var future = new Future.error("oh no");
+          expect(future, throwsA("oh no"));
+
+          addTearDown(() {
+            // If the tear-down is in a different error zone than the setUpAll,
+            // the error will try to cross the zone boundary and get
+            // top-leveled.
+            expect(future, throwsA("oh no"));
+          });
+        });
+
+        test("test", () {});
+      });
+    });
+
+    group("asynchronously", () {
+      test("blocks additional tearDowns on in-band async", () async {
+        var tearDown1Run = false;
+        var tearDown2Run = false;
+        var tearDown3Run = false;
+        await expectTestsPass(() {
+          setUpAll(() {
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isTrue);
+              expect(tearDown3Run, isTrue);
+              await pumpEventQueue();
+              tearDown1Run = true;
+            });
+
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isTrue);
+              await pumpEventQueue();
+              tearDown2Run = true;
+            });
+
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+              await pumpEventQueue();
+              tearDown3Run = true;
+            });
+          });
+
+          test("test", () {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+          });
+        });
+
+        expect(tearDown1Run, isTrue);
+        expect(tearDown2Run, isTrue);
+        expect(tearDown3Run, isTrue);
+      });
+
+      test("doesn't block additional tearDowns on out-of-band async", () async {
+        var tearDown1Run = false;
+        var tearDown2Run = false;
+        var tearDown3Run = false;
+        await expectTestsPass(() {
+          setUpAll(() {
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(new Future(() {
+                tearDown1Run = true;
+              }), completes);
+            });
+
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(new Future(() {
+                tearDown2Run = true;
+              }), completes);
+            });
+
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(new Future(() {
+                tearDown3Run = true;
+              }), completes);
+            });
+          });
+
+          test("test", () {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+          });
+        });
+
+        expect(tearDown1Run, isTrue);
+        expect(tearDown2Run, isTrue);
+        expect(tearDown3Run, isTrue);
+      });
+
+      test("blocks additional tearDownAlls on in-band async", () async {
+        var groupTearDownRun = false;
+        var testTearDownRun = false;
+        await expectTestsPass(() {
+          tearDownAll(() async {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isTrue);
+            await pumpEventQueue();
+            groupTearDownRun = true;
+          });
+
+          setUpAll(() {
+            addTearDown(() async {
+              expect(groupTearDownRun, isFalse);
+              expect(testTearDownRun, isFalse);
+              await pumpEventQueue();
+              testTearDownRun = true;
+            });
+          });
+
+          test("test", () {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+          });
+        });
+
+        expect(groupTearDownRun, isTrue);
+        expect(testTearDownRun, isTrue);
+      });
+
+      test("doesn't block additional tearDownAlls on out-of-band async",
+          () async {
+        var groupTearDownRun = false;
+        var testTearDownRun = false;
+        await expectTestsPass(() {
+          tearDownAll(() {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+
+            expect(new Future(() {
+              groupTearDownRun = true;
+            }), completes);
+          });
+
+          setUpAll(() {
+            addTearDown(() {
+              expect(groupTearDownRun, isFalse);
+              expect(testTearDownRun, isFalse);
+
+              expect(new Future(() {
+                testTearDownRun = true;
+              }), completes);
+            });
+          });
+
+          test("test", () {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+          });
+        });
+
+        expect(groupTearDownRun, isTrue);
+        expect(testTearDownRun, isTrue);
+      });
+    });
+
+    group("with an error", () {
+      test("reports the error", () async {
+        var engine = declareEngine(() {
+          setUpAll(() {
+            addTearDown(() => throw new TestFailure("fail"));
+          });
+
+          test("test", () {});
+        });
+
+        var queue = new StreamQueue(engine.onTestStarted);
+        queue.skip(2);
+        var liveTestFuture = queue.next;
+
+        expect(await engine.run(), isFalse);
+
+        var liveTest = await liveTestFuture;
+        expect(liveTest.test.name, equals("(tearDownAll)"));
+        expectTestFailed(liveTest, "fail");
+      });
+
+      test("runs further tearDowns", () async {
+        // Declare this in the outer test so if it doesn't run, the outer test
+        // will fail.
+        var shouldRun = expectAsync0(() {});
+
+        var engine = declareEngine(() {
+          setUpAll(() {
+            addTearDown(() => throw "error");
+            addTearDown(shouldRun);
+          });
+
+          test("test", () {});
+        });
+
+        expect(await engine.run(), isFalse);
+      });
+
+      test("runs further tearDownAlls", () async {
+        // Declare this in the outer test so if it doesn't run, the outer test
+        // will fail.
+        var shouldRun = expectAsync0(() {});
+
+        var engine = declareEngine(() {
+          tearDownAll(shouldRun);
+
+          setUpAll(() {
+            addTearDown(() => throw "error");
+          });
+
+          test("test", () {});
+        });
+
+        expect(await engine.run(), isFalse);
+      });
     });
   });
 }
diff --git a/test/runner/json_reporter_test.dart b/test/runner/json_reporter_test.dart
index e6cb35a..daa936d 100644
--- a/test/runner/json_reporter_test.dart
+++ b/test/runner/json_reporter_test.dart
@@ -447,6 +447,8 @@
         _testDone(3, hidden: true),
         _testStart(4, "success", line: 9, column: 9),
         _testDone(4),
+        _testStart(5, "(tearDownAll)"),
+        _testDone(5, hidden: true),
         _done()
       ]);
     });