Skip to content

Commit 8537739

Browse files
committed
Add an Invoker class that manages a running test.
[email protected] See #2 Review URL: https://codereview.chromium.org//916533003
1 parent 1e93b9b commit 8537739

File tree

5 files changed

+758
-0
lines changed

5 files changed

+758
-0
lines changed

lib/src/invoker.dart

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
library unittest.invoker;
6+
7+
import 'dart:async';
8+
9+
import 'package:stack_trace/stack_trace.dart';
10+
11+
import 'expect.dart';
12+
import 'live_test.dart';
13+
import 'live_test_controller.dart';
14+
import 'state.dart';
15+
import 'suite.dart';
16+
import 'test.dart';
17+
import 'utils.dart';
18+
19+
/// A test in this isolate.
20+
class LocalTest implements Test {
21+
/// The name of the test.
22+
final String name;
23+
24+
/// The test body.
25+
final AsyncFunction _body;
26+
27+
/// The callback used to clean up after the test.
28+
///
29+
/// This is separated out from [_body] because it needs to run once the test's
30+
/// asynchronous computation has finished, even if that's different from the
31+
/// completion of the main body of the test.
32+
final AsyncFunction _tearDown;
33+
34+
LocalTest(this.name, body(), {tearDown()})
35+
: _body = body,
36+
_tearDown = tearDown;
37+
38+
/// Loads a single runnable instance of this test.
39+
LiveTest load(Suite suite) {
40+
var invoker = new Invoker._(suite, this);
41+
return invoker.liveTest;
42+
}
43+
}
44+
45+
/// The class responsible for managing the lifecycle of a single local test.
46+
///
47+
/// The current invoker is accessible within the zone scope of the running test
48+
/// using [Invoker.current]. It's used to track asynchronous callbacks and
49+
/// report asynchronous errors.
50+
class Invoker {
51+
/// The live test being driven by the invoker.
52+
///
53+
/// This provides a view into the state of the test being executed.
54+
LiveTest get liveTest => _controller.liveTest;
55+
LiveTestController _controller;
56+
57+
/// The test being run.
58+
LocalTest get _test => liveTest.test as LocalTest;
59+
60+
/// Note that this is meaningless once [_onCompleteCompleter] is complete.
61+
var _outstandingCallbacks = 0;
62+
63+
/// The completer to complete once the test body finishes.
64+
///
65+
/// This is distinct from [_controller.completer] because a tear-down may need
66+
/// to run before the test is truly finished.
67+
final _completer = new Completer();
68+
69+
/// The current invoker, or `null` if none is defined.
70+
///
71+
/// An invoker is only set within the zone scope of a running test.
72+
static Invoker get current => Zone.current[#unittest._invoker];
73+
74+
Invoker._(Suite suite, LocalTest test) {
75+
_controller = new LiveTestController(suite, test, _onRun);
76+
}
77+
78+
/// Tells the invoker that there's a callback running that it should wait for
79+
/// before considering the test successful.
80+
///
81+
/// Each call to [addOutstandingCallback] should be followed by a call to
82+
/// [removeOutstandingCallback] once the callbak is no longer running. Note
83+
/// that only successful tests wait for outstanding callbacks; as soon as a
84+
/// test experiences an error, any further calls to [addOutstandingCallback]
85+
/// or [removeOutstandingCallback] will do nothing.
86+
void addOutstandingCallback() {
87+
_outstandingCallbacks++;
88+
}
89+
90+
/// Tells the invoker that a callback declared with [addOutstandingCallback]
91+
/// is no longer running.
92+
void removeOutstandingCallback() {
93+
_outstandingCallbacks--;
94+
95+
if (_outstandingCallbacks != 0) return;
96+
if (_completer.isCompleted) return;
97+
98+
// The test must be passing if we get here, because if there were an error
99+
// the completer would already be completed.
100+
assert(liveTest.state.result == Result.success);
101+
_completer.complete();
102+
}
103+
104+
/// Notifies the invoker of an asynchronous error.
105+
///
106+
/// Note that calling this explicitly is rarely necessary, since any
107+
/// otherwise-uncaught errors will be forwarded to the invoker anyway.
108+
void handleError(error, [StackTrace stackTrace]) {
109+
if (stackTrace == null) stackTrace = new Chain.current();
110+
111+
var afterSuccess = liveTest.isComplete &&
112+
liveTest.state.result == Result.success;
113+
114+
if (error is! TestFailure) {
115+
_controller.setState(const State(Status.complete, Result.error));
116+
} else if (liveTest.state.result != Result.error) {
117+
_controller.setState(const State(Status.complete, Result.failure));
118+
}
119+
120+
_controller.addError(error, stackTrace);
121+
122+
if (!_completer.isCompleted) _completer.complete();
123+
124+
// If a test was marked as success but then had an error, that indicates
125+
// that it was poorly-written and could be flaky.
126+
if (!afterSuccess) return;
127+
handleError(
128+
"This test failed after it had already completed. Make sure to use "
129+
"[expectAsync]\n"
130+
"or the [completes] matcher when testing async code.",
131+
stackTrace);
132+
}
133+
134+
/// The method that's run when the test is started.
135+
void _onRun() {
136+
_controller.setState(const State(Status.running, Result.success));
137+
138+
Chain.capture(() {
139+
runZoned(() {
140+
// TODO(nweiz): Make the timeout configurable.
141+
// TODO(nweiz): Reset this timer whenever the user's code interacts with
142+
// the library.
143+
var timer = new Timer(new Duration(seconds: 30), () {
144+
if (liveTest.isComplete) return;
145+
handleError(
146+
new TimeoutException(
147+
"Test timed out after 30 seconds.",
148+
new Duration(seconds: 30)));
149+
});
150+
151+
addOutstandingCallback();
152+
153+
// Run the test asynchronously so that the "running" state change has a
154+
// chance to hit its event handler(s) before the test produces an error.
155+
// If an error is emitted before the first state change is handled, we
156+
// can end up with [onError] callbacks firing before the corresponding
157+
// [onStateChange], which violates the timing guarantees.
158+
new Future(_test._body)
159+
.then((_) => removeOutstandingCallback());
160+
161+
// Explicitly handle an error here so that we can return the [Future].
162+
// If a [Future] returned from an error zone would throw an error
163+
// through the zone boundary, it instead never completes, and we want to
164+
// avoid that.
165+
_completer.future.then((_) {
166+
if (_test._tearDown == null) return null;
167+
return new Future.sync(_test._tearDown);
168+
}).catchError(Zone.current.handleUncaughtError).then((_) {
169+
timer.cancel();
170+
_controller.setState(
171+
new State(Status.complete, liveTest.state.result));
172+
_controller.completer.complete();
173+
});
174+
}, zoneValues: {#unittest._invoker: this}, onError: handleError);
175+
});
176+
}
177+
}

lib/src/utils.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@
44

55
library unittest.utils;
66

7+
import 'dart:async';
8+
79
import 'package:stack_trace/stack_trace.dart';
810

11+
/// A typedef for a possibly-asynchronous function.
12+
///
13+
/// The return type should only ever by [Future] or void.
14+
typedef AsyncFunction();
15+
916
/// Indent each line in [str] by two spaces.
1017
String indent(String str) =>
1118
str.replaceAll(new RegExp("^", multiLine: true), " ");

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies:
1313
# features it provides.
1414
matcher: '>=0.11.4 <0.11.5'
1515
dev_dependencies:
16+
fake_async: '>=0.1.2 <0.2.0'
1617
metatest:
1718
git:
1819
ref: b6348d7e7f3c5b00a48aa579694457d1abd36b69

0 commit comments

Comments
 (0)