Index: lib/src/backend/invoker.dart |
diff --git a/lib/src/backend/invoker.dart b/lib/src/backend/invoker.dart |
index 81a931e4eec7e2315f5df21b9b3daedc69294847..a0151c385ec722bcb38e6d045e5f747d9111a1ba 100644 |
--- a/lib/src/backend/invoker.dart |
+++ b/lib/src/backend/invoker.dart |
@@ -14,6 +14,7 @@ import 'closed_exception.dart'; |
import 'live_test.dart'; |
import 'live_test_controller.dart'; |
import 'metadata.dart'; |
+import 'outstanding_callback_counter.dart'; |
import 'state.dart'; |
import 'suite.dart'; |
import 'test.dart'; |
@@ -76,14 +77,13 @@ class Invoker { |
/// The test metadata merged with the suite metadata. |
final Metadata metadata; |
- /// Note that this is meaningless once [_onCompleteCompleter] is complete. |
- var _outstandingCallbacks = 0; |
- |
- /// The completer to complete once the test body finishes. |
- /// |
- /// This is distinct from [_controller.completer] because a tear-down may need |
- /// to run before the test is truly finished. |
- final _completer = new Completer(); |
+ /// The outstanding callback counter for the current zone. |
+ OutstandingCallbackCounter get _outstandingCallbacks { |
+ var counter = Zone.current[this]; |
+ if (counter != null) return counter; |
+ throw new StateError("Can't add or remove outstanding callbacks outside " |
+ "of a test body."); |
+ } |
/// The current invoker, or `null` if none is defined. |
/// |
@@ -112,22 +112,13 @@ class Invoker { |
/// Throws a [ClosedException] if this test has been closed. |
void addOutstandingCallback() { |
if (closed) throw new ClosedException(); |
- _outstandingCallbacks++; |
+ _outstandingCallbacks.addOutstandingCallback(); |
} |
/// Tells the invoker that a callback declared with [addOutstandingCallback] |
/// is no longer running. |
- void removeOutstandingCallback() { |
- _outstandingCallbacks--; |
- |
- if (_outstandingCallbacks != 0) return; |
- if (_completer.isCompleted) return; |
- |
- // The test must be passing if we get here, because if there were an error |
- // the completer would already be completed. |
- assert(liveTest.state.result == Result.success); |
- _completer.complete(); |
- } |
+ void removeOutstandingCallback() => |
+ _outstandingCallbacks.removeOutstandingCallback(); |
/// Notifies the invoker of an asynchronous error. |
/// |
@@ -146,8 +137,7 @@ class Invoker { |
} |
_controller.addError(error, stackTrace); |
- |
- if (!_completer.isCompleted) _completer.complete(); |
+ _outstandingCallbacks.removeAllOutstandingCallbacks(); |
// If a test was marked as success but then had an error, that indicates |
// that it was poorly-written and could be flaky. |
@@ -163,11 +153,12 @@ class Invoker { |
void _onRun() { |
_controller.setState(const State(Status.running, Result.success)); |
+ var outstandingCallbacksForBody = new OutstandingCallbackCounter(); |
+ |
Chain.capture(() { |
- runZoned(() { |
- // TODO(nweiz): Make the timeout configurable. |
- // TODO(nweiz): Reset this timer whenever the user's code interacts with |
- // the library. |
+ runZonedWithValues(() { |
+ // TODO(nweiz): Reset this timer whenever the user's code interacts |
+ // with the library. |
var timeout = metadata.timeout.apply(new Duration(seconds: 30)); |
var timer = new Timer(timeout, () { |
if (liveTest.isComplete) return; |
@@ -176,20 +167,30 @@ class Invoker { |
"Test timed out after ${niceDuration(timeout)}.", timeout)); |
}); |
- addOutstandingCallback(); |
- |
- // 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 |
- // [onStateChange], which violates the timing guarantees. |
+ // 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 [onStateChange], which violates the timing |
+ // guarantees. |
new Future(_test._body) |
.then((_) => removeOutstandingCallback()); |
- _completer.future.then((_) { |
+ _outstandingCallbacks.noOutstandingCallbacks.then((_) { |
if (_test._tearDown == null) return null; |
- return new Future.sync(_test._tearDown); |
- }).catchError(Zone.current.handleUncaughtError).then((_) { |
+ |
+ // Reset the outstanding callback counter to wait for callbacks from |
+ // the test's `tearDown` to complete. |
+ var outstandingCallbacksForTearDown = new OutstandingCallbackCounter(); |
+ runZonedWithValues(() { |
+ new Future.sync(_test._tearDown) |
+ .then((_) => removeOutstandingCallback()); |
+ }, onError: handleError, zoneValues: { |
+ this: outstandingCallbacksForTearDown |
+ }); |
+ |
+ return outstandingCallbacksForTearDown.noOutstandingCallbacks; |
+ }).then((_) { |
timer.cancel(); |
_controller.setState( |
new State(Status.complete, liveTest.state.result)); |
@@ -198,10 +199,14 @@ class Invoker { |
// non-microtask events. |
Timer.run(_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. |
+ this: outstandingCallbacksForBody |
}, |
zoneSpecification: new ZoneSpecification( |
print: (self, parent, zone, line) => _controller.print(line)), |
- zoneValues: {#test.invoker: this}, |
onError: handleError); |
}); |
} |