-
Notifications
You must be signed in to change notification settings - Fork 210
/
invoker.dart
297 lines (254 loc) · 10.4 KB
/
invoker.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
// 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:stack_trace/stack_trace.dart';
import '../backend/group.dart';
import '../frontend/expect.dart';
import '../utils.dart';
import 'closed_exception.dart';
import 'live_test.dart';
import 'live_test_controller.dart';
import 'metadata.dart';
import 'operating_system.dart';
import 'outstanding_callback_counter.dart';
import 'state.dart';
import 'suite.dart';
import 'test.dart';
import 'test_platform.dart';
/// A test in this isolate.
class LocalTest extends Test {
final String name;
final Metadata metadata;
/// The test body.
final AsyncFunction _body;
LocalTest(this.name, this.metadata, body())
: _body = body;
/// Loads a single runnable instance of this test.
LiveTest load(Suite suite, {Iterable<Group> groups}) {
var invoker = new Invoker._(suite, this, groups: groups);
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);
}
}
/// The class responsible for managing the lifecycle of a single local test.
///
/// The current invoker is accessible within the zone scope of the running test
/// using [Invoker.current]. It's used to track asynchronous callbacks and
/// report asynchronous errors.
class Invoker {
/// The live test being driven by the invoker.
///
/// This provides a view into the state of the test being executed.
LiveTest get liveTest => _controller.liveTest;
LiveTestController _controller;
bool get _closable => Zone.current[_closableKey];
/// An opaque object used as a key in the zone value map to identify
/// [_closable].
///
/// This is an instance variable to ensure that multiple invokers don't step
/// on one anothers' toes.
final _closableKey = new Object();
/// Whether the test has been closed.
///
/// Once the test is closed, [expect] and [expectAsync] will throw
/// [ClosedException]s whenever accessed to help the test stop executing as
/// soon as possible.
bool get closed => _closable && _onCloseCompleter.isCompleted;
/// A future that completes once the test has been closed.
Future get onClose => _closable
? _onCloseCompleter.future
// If we're in an unclosable block, return a future that will never
// complete.
: new Completer().future;
final _onCloseCompleter = new Completer();
/// The test being run.
LocalTest get _test => liveTest.test as LocalTest;
/// The outstanding callback counter for the current zone.
OutstandingCallbackCounter get _outstandingCallbacks {
var counter = Zone.current[_counterKey];
if (counter != null) return counter;
throw new StateError("Can't add or remove outstanding callbacks outside "
"of a test body.");
}
/// All the zones created by [waitForOutstandingCallbacks], in the order they
/// were created.
///
/// This is used to throw timeout errors in the most recent zone.
final _outstandingCallbackZones = <Zone>[];
/// An opaque object used as a key in the zone value map to identify
/// [_outstandingCallbacks].
///
/// This is an instance variable to ensure that multiple invokers don't step
/// on one anothers' toes.
final _counterKey = new Object();
/// The current invoker, or `null` if none is defined.
///
/// An invoker is only set within the zone scope of a running test.
static Invoker get current {
// TODO(nweiz): Use a private symbol when dart2js supports it (issue 17526).
return Zone.current[#test.invoker];
}
/// The zone that the top level of [_test.body] is running in.
///
/// Tracking this ensures that [_timeoutTimer] isn't created in a
/// timer-mocking zone created by the test.
Zone _invokerZone;
/// The timer for tracking timeouts.
///
/// This will be `null` until the test starts running.
Timer _timeoutTimer;
Invoker._(Suite suite, LocalTest test, {Iterable<Group> groups}) {
_controller = new LiveTestController(
suite, test, _onRun, _onCloseCompleter.complete, groups: groups);
}
/// Tells the invoker that there's a callback running that it should wait for
/// before considering the test successful.
///
/// Each call to [addOutstandingCallback] should be followed by a call to
/// [removeOutstandingCallback] once the callbak is no longer running. Note
/// that only successful tests wait for outstanding callbacks; as soon as a
/// test experiences an error, any further calls to [addOutstandingCallback]
/// or [removeOutstandingCallback] will do nothing.
///
/// Throws a [ClosedException] if this test has been closed.
void addOutstandingCallback() {
if (closed) throw new ClosedException();
_outstandingCallbacks.addOutstandingCallback();
}
/// Tells the invoker that a callback declared with [addOutstandingCallback]
/// is no longer running.
void removeOutstandingCallback() {
heartbeat();
_outstandingCallbacks.removeOutstandingCallback();
}
/// Removes all outstanding callbacks, for example when an error occurs.
///
/// Future calls to [addOutstandingCallback] and [removeOutstandingCallback]
/// will be ignored.
void removeAllOutstandingCallbacks() =>
_outstandingCallbacks.removeAllOutstandingCallbacks();
/// Runs [fn] and returns once all (registered) outstanding callbacks it
/// transitively invokes have completed.
///
/// If [fn] itself returns a future, this will automatically wait until that
/// future completes as well. Note that outstanding callbacks registered
/// within [fn] will *not* be registered as outstanding callback outside of
/// [fn].
///
/// If [fn] produces an unhandled error, this marks the current test as
/// failed, removes all outstanding callbacks registered within [fn], and
/// completes the returned future. It does not remove any outstanding
/// callbacks registered outside of [fn].
///
/// If the test times out, the *most recent* call to
/// [waitForOutstandingCallbacks] will treat that error as occurring within
/// [fn]—that is, it will complete immediately.
Future waitForOutstandingCallbacks(fn()) {
heartbeat();
var zone;
var counter = new OutstandingCallbackCounter();
runZoned(() {
runZoned(() async {
zone = Zone.current;
_outstandingCallbackZones.add(zone);
await fn();
counter.removeOutstandingCallback();
}, onError: _handleError);
}, zoneValues: {
_counterKey: counter
});
return counter.noOutstandingCallbacks.whenComplete(() {
_outstandingCallbackZones.remove(zone);
});
}
/// Runs [fn] in a zone where [closed] is always `false`.
///
/// This is useful for running code that should be able to register callbacks
/// and interact with the test framework normally even when the invoker is
/// closed, for example cleanup code.
unclosable(fn()) {
heartbeat();
return runZoned(fn, zoneValues: {
_closableKey: false
});
}
/// Notifies the invoker that progress is being made.
///
/// Each heartbeat resets the timeout timer. This helps ensure that
/// long-running tests that still make progress don't time out.
void heartbeat() {
if (liveTest.isComplete) return;
if (_timeoutTimer != null) _timeoutTimer.cancel();
var timeout = liveTest.test.metadata.timeout
.apply(new Duration(seconds: 30));
if (timeout == null) return;
_timeoutTimer = _invokerZone.createTimer(timeout, () {
_outstandingCallbackZones.last.run(() {
if (liveTest.isComplete) return;
_handleError(
new TimeoutException(
"Test timed out after ${niceDuration(timeout)}.", timeout));
});
});
}
/// Notifies the invoker of an asynchronous error.
void _handleError(error, [StackTrace stackTrace]) {
if (stackTrace == null) stackTrace = new Chain.current();
var afterSuccess = liveTest.isComplete &&
liveTest.state.result == Result.success;
if (error is! TestFailure) {
_controller.setState(const State(Status.complete, Result.error));
} else if (liveTest.state.result != Result.error) {
_controller.setState(const State(Status.complete, Result.failure));
}
_controller.addError(error, stackTrace);
removeAllOutstandingCallbacks();
// If a test was marked as success but then had an error, that indicates
// that it was poorly-written and could be flaky.
if (!afterSuccess) return;
_handleError(
"This test failed after it had already completed. Make sure to use "
"[expectAsync]\n"
"or the [completes] matcher when testing async code.",
stackTrace);
}
/// The method that's run when the test is started.
void _onRun() {
_controller.setState(const State(Status.running, Result.success));
var outstandingCallbacksForBody = new OutstandingCallbackCounter();
Chain.capture(() {
runZonedWithValues(() 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 [onStateChange], which violates the timing
// guarantees.
new Future(_test._body)
.then((_) => removeOutstandingCallback());
await _outstandingCallbacks.noOutstandingCallbacks;
if (_timeoutTimer != null) _timeoutTimer.cancel();
_controller.setState(
new State(Status.complete, liveTest.state.result));
// Use [Timer.run] here to avoid starving the DOM or other
// 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.
_counterKey: outstandingCallbacksForBody,
_closableKey: true
},
zoneSpecification: new ZoneSpecification(
print: (self, parent, zone, line) => _controller.print(line)),
onError: _handleError);
});
}
}