Line data Source code
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 : import 'dart:async';
6 :
7 : import 'package:stack_trace/stack_trace.dart';
8 :
9 : import 'closed_exception.dart';
10 : import 'declarer.dart';
11 : import 'group.dart';
12 : import 'live_test.dart';
13 : import 'live_test_controller.dart';
14 : import 'message.dart';
15 : import 'metadata.dart';
16 : import 'state.dart';
17 : import 'suite.dart';
18 : import 'suite_platform.dart';
19 : import 'test.dart';
20 : import 'test_failure.dart';
21 : import 'util/pretty_print.dart';
22 :
23 : /// A test in this isolate.
24 : class LocalTest extends Test {
25 : @override
26 : final String name;
27 :
28 : @override
29 : final Metadata metadata;
30 :
31 : @override
32 : final Trace? trace;
33 :
34 : /// Whether this is a test defined using `setUpAll()` or `tearDownAll()`.
35 : final bool isScaffoldAll;
36 :
37 : /// The test body.
38 : final Function() _body;
39 :
40 : /// Whether the test is run in its own error zone.
41 : final bool _guarded;
42 :
43 : /// Creates a new [LocalTest].
44 : ///
45 : /// If [guarded] is `true`, the test is run in its own error zone, and any
46 : /// errors that escape that zone cause the test to fail. If it's `false`, it's
47 : /// the caller's responsiblity to invoke [LiveTest.run] in the context of a
48 : /// call to [Invoker.guard].
49 11 : LocalTest(this.name, this.metadata, this._body,
50 : {this.trace, bool guarded = true, this.isScaffoldAll = false})
51 : : _guarded = guarded;
52 :
53 11 : LocalTest._(this.name, this.metadata, this._body, this.trace, this._guarded,
54 : this.isScaffoldAll);
55 :
56 : /// Loads a single runnable instance of this test.
57 11 : @override
58 : LiveTest load(Suite suite, {Iterable<Group>? groups}) {
59 22 : var invoker = Invoker._(suite, this, groups: groups, guarded: _guarded);
60 11 : return invoker.liveTest;
61 : }
62 :
63 11 : @override
64 : Test? forPlatform(SuitePlatform platform) {
65 33 : if (!metadata.testOn.evaluate(platform)) return null;
66 66 : return LocalTest._(name, metadata.forPlatform(platform), _body, trace,
67 22 : _guarded, isScaffoldAll);
68 : }
69 : }
70 :
71 : /// The class responsible for managing the lifecycle of a single local test.
72 : ///
73 : /// The current invoker is accessible within the zone scope of the running test
74 : /// using [Invoker.current]. It's used to track asynchronous callbacks and
75 : /// report asynchronous errors.
76 : class Invoker {
77 : /// The live test being driven by the invoker.
78 : ///
79 : /// This provides a view into the state of the test being executed.
80 22 : LiveTest get liveTest => _controller;
81 : late final LiveTestController _controller;
82 :
83 : /// Whether to run this test in its own error zone.
84 : final bool _guarded;
85 :
86 : /// Whether the user code is allowed to interact with the invoker despite it
87 : /// being closed.
88 : ///
89 : /// A test is generally closed because the runner is shutting down (in
90 : /// response to a signal) or because the test's suite is finished.
91 : /// Typically calls to [addTearDown] and [addOutstandingCallback] are only
92 : /// allowed before the test is closed. Tear down callbacks, however, are
93 : /// allowed to perform these interactions to facilitate resource cleanup on a
94 : /// best-effort basis, so the invoker is made to appear open only within the
95 : /// zones running the teardown callbacks.
96 24 : bool get _forceOpen => Zone.current[_forceOpenForTearDownKey] as bool;
97 :
98 : /// An opaque object used as a key in the zone value map to identify
99 : /// [_forceOpen].
100 : ///
101 : /// This is an instance variable to ensure that multiple invokers don't step
102 : /// on one anothers' toes.
103 : final _forceOpenForTearDownKey = Object();
104 :
105 : /// Whether the test has been closed.
106 : ///
107 : /// Once the test is closed, [expect] and [expectAsync] will throw
108 : /// [ClosedException]s whenever accessed to help the test stop executing as
109 : /// soon as possible.
110 24 : bool get closed => !_forceOpen && _onCloseCompleter.isCompleted;
111 :
112 : /// A future that completes once the test has been closed.
113 0 : Future<void> get onClose => _onCloseCompleter.future;
114 : final _onCloseCompleter = Completer<void>();
115 :
116 : /// The test being run.
117 33 : LocalTest get _test => liveTest.test as LocalTest;
118 :
119 : /// The outstanding callback counter for the current zone.
120 6 : _AsyncCounter get _outstandingCallbacks {
121 18 : var counter = Zone.current[_counterKey] as _AsyncCounter?;
122 : if (counter != null) return counter;
123 0 : throw StateError("Can't add or remove outstanding callbacks outside "
124 : 'of a test body.');
125 : }
126 :
127 : /// All the zones created by [_waitForOutstandingCallbacks], in the order they
128 : /// were created.
129 : ///
130 : /// This is used to throw timeout errors in the most recent zone.
131 : final _outstandingCallbackZones = <Zone>[];
132 :
133 : /// An opaque object used as a key in the zone value map to identify
134 : /// [_outstandingCallbacks].
135 : ///
136 : /// This is an instance variable to ensure that multiple invokers don't step
137 : /// on one anothers' toes.
138 : final _counterKey = Object();
139 :
140 : /// The number of times this [liveTest] has been run.
141 : int _runCount = 0;
142 :
143 : /// The current invoker, or `null` if none is defined.
144 : ///
145 : /// An invoker is only set within the zone scope of a running test.
146 11 : static Invoker? get current {
147 : // TODO(nweiz): Use a private symbol when dart2js supports it (issue 17526).
148 22 : return Zone.current[#test.invoker] as Invoker?;
149 : }
150 :
151 : /// Runs [callback] in a zone where unhandled errors from [LiveTest]s are
152 : /// caught and dispatched to the appropriate [Invoker].
153 11 : static T? guard<T>(T Function() callback) =>
154 22 : runZoned<T?>(callback, zoneSpecification: ZoneSpecification(
155 : // Use [handleUncaughtError] rather than [onError] so we can
156 : // capture [zone] and with it the outstanding callback counter for
157 : // the zone in which [error] was thrown.
158 0 : handleUncaughtError: (self, _, zone, error, stackTrace) {
159 0 : var invoker = zone[#test.invoker] as Invoker?;
160 : if (invoker != null) {
161 0 : self.parent!.run(() => invoker._handleError(zone, error, stackTrace));
162 : } else {
163 0 : self.parent!.handleUncaughtError(error, stackTrace);
164 : }
165 : }));
166 :
167 : /// The timer for tracking timeouts.
168 : ///
169 : /// This will be `null` until the test starts running.
170 : Timer? _timeoutTimer;
171 :
172 : /// The tear-down functions to run when this test finishes.
173 : final _tearDowns = <Function()>[];
174 :
175 : /// Messages to print if and when this test fails.
176 : final _printsOnFailure = <String>[];
177 :
178 11 : Invoker._(Suite suite, LocalTest test,
179 : {Iterable<Group>? groups, bool guarded = true})
180 : : _guarded = guarded {
181 22 : _controller = LiveTestController(
182 33 : suite, test, _onRun, _onCloseCompleter.complete,
183 : groups: groups);
184 : }
185 :
186 : /// Runs [callback] after this test completes.
187 : ///
188 : /// The [callback] may return a [Future]. Like all tear-downs, callbacks are
189 : /// run in the reverse of the order they're declared.
190 3 : void addTearDown(dynamic Function() callback) {
191 3 : if (closed) throw ClosedException();
192 :
193 6 : if (_test.isScaffoldAll) {
194 0 : Declarer.current!.addTearDownAll(callback);
195 : } else {
196 6 : _tearDowns.add(callback);
197 : }
198 : }
199 :
200 : /// Tells the invoker that there's a callback running that it should wait for
201 : /// before considering the test successful.
202 : ///
203 : /// Each call to [addOutstandingCallback] should be followed by a call to
204 : /// [removeOutstandingCallback] once the callback is no longer running. Note
205 : /// that only successful tests wait for outstanding callbacks; as soon as a
206 : /// test experiences an error, any further calls to [addOutstandingCallback]
207 : /// or [removeOutstandingCallback] will do nothing.
208 : ///
209 : /// Throws a [ClosedException] if this test has been closed.
210 6 : void addOutstandingCallback() {
211 6 : if (closed) throw ClosedException();
212 12 : _outstandingCallbacks.increment();
213 : }
214 :
215 : /// Tells the invoker that a callback declared with [addOutstandingCallback]
216 : /// is no longer running.
217 6 : void removeOutstandingCallback() {
218 6 : heartbeat();
219 12 : _outstandingCallbacks.decrement();
220 : }
221 :
222 : /// Run [tearDowns] in reverse order.
223 : ///
224 : /// An exception thrown in a tearDown callback will cause the test to fail, if
225 : /// it isn't already failing, but it won't prevent the remaining callbacks
226 : /// from running. This invoker will not be closeable within the zone that the
227 : /// teardowns are running in.
228 11 : Future<void> runTearDowns(List<FutureOr<void> Function()> tearDowns) {
229 11 : heartbeat();
230 22 : return runZoned(() async {
231 11 : while (tearDowns.isNotEmpty) {
232 3 : var completer = Completer();
233 :
234 3 : addOutstandingCallback();
235 6 : _waitForOutstandingCallbacks(() {
236 12 : Future.sync(tearDowns.removeLast()).whenComplete(completer.complete);
237 12 : }).then((_) => removeOutstandingCallback()).unawaited;
238 :
239 6 : await completer.future;
240 : }
241 22 : }, zoneValues: {_forceOpenForTearDownKey: true});
242 : }
243 :
244 : /// Runs [fn] and completes once [fn] and all outstanding callbacks registered
245 : /// within [fn] have completed.
246 : ///
247 : /// Outstanding callbacks registered within [fn] will *not* be registered as
248 : /// outstanding callback outside of [fn].
249 11 : Future<void> _waitForOutstandingCallbacks(FutureOr<void> Function() fn) {
250 11 : heartbeat();
251 :
252 : Zone? zone;
253 11 : var counter = _AsyncCounter();
254 22 : runZoned(() async {
255 11 : zone = Zone.current;
256 22 : _outstandingCallbackZones.add(zone!);
257 11 : await fn();
258 11 : counter.decrement();
259 22 : }, zoneValues: {_counterKey: counter});
260 :
261 33 : return counter.onZero.whenComplete(() {
262 22 : _outstandingCallbackZones.remove(zone!);
263 : });
264 : }
265 :
266 : /// Notifies the invoker that progress is being made.
267 : ///
268 : /// Each heartbeat resets the timeout timer. This helps ensure that
269 : /// long-running tests that still make progress don't time out.
270 11 : void heartbeat() {
271 22 : if (liveTest.isComplete) return;
272 33 : if (_timeoutTimer != null) _timeoutTimer!.cancel();
273 :
274 : const defaultTimeout = Duration(seconds: 30);
275 55 : var timeout = liveTest.test.metadata.timeout.apply(defaultTimeout);
276 : if (timeout == null) return;
277 0 : String message() {
278 0 : var message = 'Test timed out after ${niceDuration(timeout)}.';
279 0 : if (timeout == defaultTimeout) {
280 0 : message += ' See https://pub.dev/packages/test#timeouts';
281 : }
282 : return message;
283 : }
284 :
285 22 : _timeoutTimer = Zone.root.createTimer(timeout, () {
286 0 : _outstandingCallbackZones.last.run(() {
287 0 : _handleError(Zone.current, TimeoutException(message(), timeout));
288 : });
289 : });
290 : }
291 :
292 : /// Marks the current test as skipped.
293 : ///
294 : /// If passed, [message] is emitted as a skip message.
295 : ///
296 : /// Note that this *does not* mark the test as complete. That is, it sets
297 : /// the result to [Result.skipped], but doesn't change the state.
298 0 : void skip([String? message]) {
299 0 : if (liveTest.state.shouldBeDone) {
300 : // Set the state explicitly so we don't get an extra error about the test
301 : // failing after being complete.
302 0 : _controller.setState(const State(Status.complete, Result.error));
303 : throw 'This test was marked as skipped after it had already completed. '
304 : 'Make sure to use\n'
305 : '[expectAsync] or the [completes] matcher when testing async code.';
306 : }
307 :
308 0 : if (message != null) _controller.message(Message.skip(message));
309 : // TODO: error if the test is already complete.
310 0 : _controller.setState(const State(Status.pending, Result.skipped));
311 : }
312 :
313 : /// Prints [message] if and when this test fails.
314 0 : void printOnFailure(String message) {
315 0 : message = message.trim();
316 0 : if (liveTest.state.result.isFailing) {
317 0 : print('\n$message');
318 : } else {
319 0 : _printsOnFailure.add(message);
320 : }
321 : }
322 :
323 : /// Notifies the invoker of an asynchronous error.
324 : ///
325 : /// The [zone] is the zone in which the error was thrown.
326 0 : void _handleError(Zone zone, Object error, [StackTrace? stackTrace]) {
327 : // Ignore errors propagated from previous test runs
328 0 : if (_runCount != zone[#runCount]) return;
329 :
330 : // Get the chain information from the zone in which the error was thrown.
331 0 : zone.run(() {
332 : if (stackTrace == null) {
333 0 : stackTrace = Chain.current();
334 : } else {
335 0 : stackTrace = Chain.forTrace(stackTrace!);
336 : }
337 : });
338 :
339 : // Store these here because they'll change when we set the state below.
340 0 : var shouldBeDone = liveTest.state.shouldBeDone;
341 :
342 0 : if (error is! TestFailure) {
343 0 : _controller.setState(const State(Status.complete, Result.error));
344 0 : } else if (liveTest.state.result != Result.error) {
345 0 : _controller.setState(const State(Status.complete, Result.failure));
346 : }
347 :
348 0 : _controller.addError(error, stackTrace!);
349 0 : zone.run(() => _outstandingCallbacks.complete());
350 :
351 0 : if (_printsOnFailure.isNotEmpty) {
352 0 : print(_printsOnFailure.join('\n\n'));
353 0 : _printsOnFailure.clear();
354 : }
355 :
356 : // If a test was supposed to be done but then had an error, that indicates
357 : // that it was poorly-written and could be flaky.
358 : if (!shouldBeDone) return;
359 :
360 : // However, users don't think of load tests as "tests", so the error isn't
361 : // helpful for them.
362 0 : if (liveTest.suite.isLoadSuite) return;
363 :
364 0 : _handleError(
365 : zone,
366 : 'This test failed after it had already completed. Make sure to use '
367 : '[expectAsync]\n'
368 : 'or the [completes] matcher when testing async code.',
369 : stackTrace);
370 : }
371 :
372 : /// The method that's run when the test is started.
373 11 : void _onRun() {
374 22 : _controller.setState(const State(Status.running, Result.success));
375 :
376 22 : _runCount++;
377 22 : Chain.capture(() {
378 22 : _guardIfGuarded(() {
379 22 : runZoned(() async {
380 : // Run the test asynchronously so that the "running" state change
381 : // has a chance to hit its event handler(s) before the test produces
382 : // an error. If an error is emitted before the first state change is
383 : // handled, we can end up with [onError] callbacks firing before the
384 : // corresponding [onStateChange], which violates the timing
385 : // guarantees.
386 : //
387 : // Use the event loop over the microtask queue to avoid starvation.
388 33 : await Future(() {});
389 :
390 44 : await _waitForOutstandingCallbacks(_test._body);
391 55 : await _waitForOutstandingCallbacks(() => runTearDowns(_tearDowns));
392 :
393 33 : if (_timeoutTimer != null) _timeoutTimer!.cancel();
394 :
395 44 : if (liveTest.state.result != Result.success &&
396 0 : _runCount < liveTest.test.metadata.retry + 1) {
397 0 : _controller.message(Message.print('Retry: ${liveTest.test.name}'));
398 0 : _onRun();
399 : return;
400 : }
401 :
402 66 : _controller.setState(State(Status.complete, liveTest.state.result));
403 :
404 33 : _controller.completer.complete();
405 : },
406 11 : zoneValues: {
407 : #test.invoker: this,
408 11 : _forceOpenForTearDownKey: false,
409 11 : #runCount: _runCount,
410 : },
411 : zoneSpecification:
412 11 : ZoneSpecification(print: (_, __, ___, line) => _print(line)));
413 : });
414 44 : }, when: liveTest.test.metadata.chainStackTraces, errorZone: false);
415 : }
416 :
417 : /// Runs [callback], in a [Invoker.guard] context if [_guarded] is `true`.
418 11 : void _guardIfGuarded(void Function() callback) {
419 11 : if (_guarded) {
420 0 : Invoker.guard(callback);
421 : } else {
422 : callback();
423 : }
424 : }
425 :
426 : /// Prints [text] as a message to [_controller].
427 0 : void _print(String text) => _controller.message(Message.print(text));
428 : }
429 :
430 : /// A manually incremented/decremented counter that completes a [Future] the
431 : /// first time it reaches zero or is forcefully completed.
432 : class _AsyncCounter {
433 : var _count = 1;
434 :
435 : /// A Future that completes the first time the counter reaches 0.
436 33 : Future<void> get onZero => _completer.future;
437 : final _completer = Completer<void>();
438 :
439 6 : void increment() {
440 12 : _count++;
441 : }
442 :
443 11 : void decrement() {
444 22 : _count--;
445 22 : if (_count != 0) return;
446 22 : if (_completer.isCompleted) return;
447 22 : _completer.complete();
448 : }
449 :
450 : /// Force [onZero] to complete.
451 : ///
452 : /// No effect if [onZero] has already completed.
453 0 : void complete() {
454 0 : if (!_completer.isCompleted) _completer.complete();
455 : }
456 : }
457 :
458 : extension<T> on Future<T> {
459 3 : void get unawaited {}
460 : }
|