Skip to content

Commit ec1b576

Browse files
authored
Merge pull request flutter#713 from dart-lang/add-tear-down-all
Make addTearDown() play nice with setUpAll()
2 parents 28dfb44 + 4f701e5 commit ec1b576

8 files changed

Lines changed: 804 additions & 303 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## 0.12.27
22

3+
* When `addTearDown()` is called within a call to `setUpAll()`, it runs its
4+
callback after *all* tests instead of running it after the `setUpAll()`
5+
callback.
6+
37
* When running in an interactive terminal, the test runner now prints status
48
lines as wide as the terminal and no wider.
59

lib/src/backend/declarer.dart

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,15 @@ class Declarer {
155155
}
156156
}
157157

158-
await Invoker.current.waitForOutstandingCallbacks(() async {
159-
await _runSetUps();
160-
await body();
161-
});
162-
}, trace: _collectTraces ? new Trace.current(2) : null));
158+
await runZoned(
159+
() => Invoker.current.waitForOutstandingCallbacks(() async {
160+
await _runSetUps();
161+
await body();
162+
}),
163+
// Make the declarer visible to running tests so that they'll throw
164+
// useful errors when calling `test()` and `group()` within a test.
165+
zoneValues: {#test.declarer: this});
166+
}, trace: _collectTraces ? new Trace.current(2) : null, guarded: false));
163167
}
164168

165169
/// Creates a group of tests.
@@ -224,7 +228,14 @@ class Declarer {
224228
_tearDownAlls.add(callback);
225229
}
226230

231+
/// Like [tearDownAll], but called from within a running [setUpAll] test to
232+
/// dynamically add a [tearDownAll].
233+
void addTearDownAll(callback()) => _tearDownAlls.add(callback);
234+
227235
/// Finalizes and returns the group being declared.
236+
///
237+
/// **Note**: The tests in this group must be run in a [Invoker.guard]
238+
/// context; otherwise, test errors won't be captured.
228239
Group build() {
229240
_checkNotBuilt("build");
230241

@@ -258,18 +269,30 @@ class Declarer {
258269
if (_setUpAlls.isEmpty) return null;
259270

260271
return new LocalTest(_prefix("(setUpAll)"), _metadata, () {
261-
return Future.forEach(_setUpAlls, (setUp) => setUp());
262-
}, trace: _setUpAllTrace);
272+
return runZoned(() => Future.forEach(_setUpAlls, (setUp) => setUp()),
273+
// Make the declarer visible to running scaffolds so they can add to
274+
// the declarer's `tearDownAll()` list.
275+
zoneValues: {#test.declarer: this});
276+
}, trace: _setUpAllTrace, guarded: false, isScaffoldAll: true);
263277
}
264278

265279
/// Returns a [Test] that runs the callbacks in [_tearDownAll].
266280
Test get _tearDownAll {
267-
if (_tearDownAlls.isEmpty) return null;
281+
// We have to create a tearDownAll if there's a setUpAll, since it might
282+
// dynamically add tear-down code using [addTearDownAll].
283+
if (_setUpAlls.isEmpty && _tearDownAlls.isEmpty) return null;
268284

269285
return new LocalTest(_prefix("(tearDownAll)"), _metadata, () {
270-
return Invoker.current.unclosable(() {
271-
return Future.forEach(_tearDownAlls.reversed, errorsDontStopTest);
272-
});
273-
}, trace: _tearDownAllTrace);
286+
return runZoned(() {
287+
return Invoker.current.unclosable(() async {
288+
while (_tearDownAlls.isNotEmpty) {
289+
await errorsDontStopTest(_tearDownAlls.removeLast());
290+
}
291+
});
292+
},
293+
// Make the declarer visible to running scaffolds so they can add to
294+
// the declarer's `tearDownAll()` list.
295+
zoneValues: {#test.declarer: this});
296+
}, trace: _tearDownAllTrace, guarded: false, isScaffoldAll: true);
274297
}
275298
}

lib/src/backend/invoker.dart

Lines changed: 118 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import '../frontend/expect.dart';
1010
import '../runner/load_suite.dart';
1111
import '../utils.dart';
1212
import 'closed_exception.dart';
13+
import 'declarer.dart';
1314
import 'group.dart';
1415
import 'live_test.dart';
1516
import 'live_test_controller.dart';
@@ -28,21 +29,38 @@ class LocalTest extends Test {
2829
final Metadata metadata;
2930
final Trace trace;
3031

32+
/// Whether this is a test defined using `setUpAll()` or `tearDownAll()`.
33+
final bool isScaffoldAll;
34+
3135
/// The test body.
32-
final AsyncFunction _body;
36+
final Function() _body;
37+
38+
/// Whether the test is run in its own error zone.
39+
final bool _guarded;
3340

34-
LocalTest(this.name, this.metadata, body(), {this.trace}) : _body = body;
41+
/// Creates a new [LocalTest].
42+
///
43+
/// If [guarded] is `true`, the test is run in its own error zone, and any
44+
/// errors that escape that zone cause the test to fail. If it's `false`, it's
45+
/// the caller's responsiblity to invoke [LiveTest.run] in the context of a
46+
/// call to [Invoker.guard].
47+
LocalTest(this.name, this.metadata, this._body,
48+
{this.trace, bool guarded: true, this.isScaffoldAll: false})
49+
: _guarded = guarded;
50+
51+
LocalTest._(this.name, this.metadata, this._body, this.trace, this._guarded,
52+
this.isScaffoldAll);
3553

3654
/// Loads a single runnable instance of this test.
3755
LiveTest load(Suite suite, {Iterable<Group> groups}) {
38-
var invoker = new Invoker._(suite, this, groups: groups);
56+
var invoker = new Invoker._(suite, this, groups: groups, guarded: _guarded);
3957
return invoker.liveTest;
4058
}
4159

4260
Test forPlatform(TestPlatform platform, {OperatingSystem os}) {
4361
if (!metadata.testOn.evaluate(platform, os: os)) return null;
44-
return new LocalTest(name, metadata.forPlatform(platform, os: os), _body,
45-
trace: trace);
62+
return new LocalTest._(name, metadata.forPlatform(platform, os: os), _body,
63+
trace, _guarded, isScaffoldAll);
4664
}
4765
}
4866

@@ -58,6 +76,9 @@ class Invoker {
5876
LiveTest get liveTest => _controller.liveTest;
5977
LiveTestController _controller;
6078

79+
/// Whether to run this test in its own error zone.
80+
final bool _guarded;
81+
6182
/// Whether the test can be closed in the current zone.
6283
bool get _closable => Zone.current[_closableKey];
6384

@@ -118,6 +139,22 @@ class Invoker {
118139
return Zone.current[#test.invoker];
119140
}
120141

142+
/// Runs [callback] in a zone where unhandled errors from [LiveTest]s are
143+
/// caught and dispatched to the appropriate [Invoker].
144+
static T guard<T>(T callback()) =>
145+
runZoned(callback, zoneSpecification: new ZoneSpecification(
146+
// Use [handleUncaughtError] rather than [onError] so we can
147+
// capture [zone] and with it the outstanding callback counter for
148+
// the zone in which [error] was thrown.
149+
handleUncaughtError: (self, _, zone, error, stackTrace) {
150+
var invoker = zone[#test.invoker];
151+
if (invoker != null) {
152+
self.parent.run(() => invoker._handleError(zone, error, stackTrace));
153+
} else {
154+
self.parent.handleUncaughtError(error, stackTrace);
155+
}
156+
}));
157+
121158
/// The zone that the top level of [_test.body] is running in.
122159
///
123160
/// Tracking this ensures that [_timeoutTimer] isn't created in a
@@ -135,7 +172,9 @@ class Invoker {
135172
/// Messages to print if and when this test fails.
136173
final _printsOnFailure = <String>[];
137174

138-
Invoker._(Suite suite, LocalTest test, {Iterable<Group> groups}) {
175+
Invoker._(Suite suite, LocalTest test,
176+
{Iterable<Group> groups, bool guarded: true})
177+
: _guarded = guarded {
139178
_controller = new LiveTestController(
140179
suite, test, _onRun, _onCloseCompleter.complete,
141180
groups: groups);
@@ -147,7 +186,12 @@ class Invoker {
147186
/// run in the reverse of the order they're declared.
148187
void addTearDown(callback()) {
149188
if (closed) throw new ClosedException();
150-
_tearDowns.add(callback);
189+
190+
if (_test.isScaffoldAll) {
191+
Declarer.current.addTearDownAll(callback);
192+
} else {
193+
_tearDowns.add(callback);
194+
}
151195
}
152196

153197
/// Tells the invoker that there's a callback running that it should wait for
@@ -282,7 +326,15 @@ class Invoker {
282326
void _handleError(Zone zone, error, [StackTrace stackTrace]) {
283327
// Ignore errors propagated from previous test runs
284328
if (_runCount != zone[#runCount]) return;
285-
if (stackTrace == null) stackTrace = new Chain.current();
329+
330+
// Get the chain information from the zone in which the error was thrown.
331+
zone.run(() {
332+
if (stackTrace == null) {
333+
stackTrace = new Chain.current();
334+
} else {
335+
stackTrace = new Chain.forTrace(stackTrace);
336+
}
337+
});
286338

287339
// Store these here because they'll change when we set the state below.
288340
var shouldBeDone = liveTest.state.shouldBeDone;
@@ -334,60 +386,68 @@ class Invoker {
334386

335387
_runCount++;
336388
Chain.capture(() {
337-
runZoned(() async {
338-
_invokerZone = Zone.current;
339-
_outstandingCallbackZones.add(Zone.current);
340-
341-
// Run the test asynchronously so that the "running" state change has
342-
// a chance to hit its event handler(s) before the test produces an
343-
// error. If an error is emitted before the first state change is
344-
// handled, we can end up with [onError] callbacks firing before the
345-
// corresponding [onStateChkange], which violates the timing
346-
// guarantees.
347-
//
348-
// Using [new Future] also avoids starving the DOM or other
349-
// microtask-level events.
350-
new Future(() async {
351-
await _test._body();
352-
await unclosable(_runTearDowns);
353-
removeOutstandingCallback();
354-
});
355-
356-
await _outstandingCallbacks.noOutstandingCallbacks;
357-
if (_timeoutTimer != null) _timeoutTimer.cancel();
358-
359-
if (liveTest.state.result != Result.success &&
360-
_runCount < liveTest.test.metadata.retry + 1) {
389+
_guardIfGuarded(() {
390+
runZoned(() async {
391+
_invokerZone = Zone.current;
392+
_outstandingCallbackZones.add(Zone.current);
393+
394+
// Run the test asynchronously so that the "running" state change
395+
// has a chance to hit its event handler(s) before the test produces
396+
// an error. If an error is emitted before the first state change is
397+
// handled, we can end up with [onError] callbacks firing before the
398+
// corresponding [onStateChkange], which violates the timing
399+
// guarantees.
400+
//
401+
// Using [new Future] also avoids starving the DOM or other
402+
// microtask-level events.
403+
new Future(() async {
404+
await _test._body();
405+
await unclosable(_runTearDowns);
406+
removeOutstandingCallback();
407+
});
408+
409+
await _outstandingCallbacks.noOutstandingCallbacks;
410+
if (_timeoutTimer != null) _timeoutTimer.cancel();
411+
412+
if (liveTest.state.result != Result.success &&
413+
_runCount < liveTest.test.metadata.retry + 1) {
414+
_controller
415+
.message(new Message.print("Retry: ${liveTest.test.name}"));
416+
_onRun();
417+
return;
418+
}
419+
361420
_controller
362-
.message(new Message.print("Retry: ${liveTest.test.name}"));
363-
_onRun();
364-
return;
365-
}
421+
.setState(new State(Status.complete, liveTest.state.result));
422+
423+
_controller.completer.complete();
424+
},
425+
zoneValues: {
426+
#test.invoker: this,
427+
// Use the invoker as a key so that multiple invokers can have
428+
// different outstanding callback counters at once.
429+
_counterKey: outstandingCallbacksForBody,
430+
_closableKey: true,
431+
#runCount: _runCount,
432+
},
433+
zoneSpecification: new ZoneSpecification(
434+
print: (_, __, ___, line) => _print(line)));
435+
});
436+
}, when: liveTest.test.metadata.chainStackTraces, errorZone: false);
437+
}
366438

367-
_controller.setState(new State(Status.complete, liveTest.state.result));
368-
369-
_controller.completer.complete();
370-
},
371-
zoneValues: {
372-
#test.invoker: this,
373-
// Use the invoker as a key so that multiple invokers can have different
374-
// outstanding callback counters at once.
375-
_counterKey: outstandingCallbacksForBody,
376-
_closableKey: true,
377-
#runCount: _runCount
378-
},
379-
zoneSpecification: new ZoneSpecification(
380-
print: (self, parent, zone, line) =>
381-
_controller.message(new Message.print(line)),
382-
// Use [handleUncaughtError] rather than [onError] so we can
383-
// capture [zone] and with it the outstanding callback counter for
384-
// the zone in which [error] was thrown.
385-
handleUncaughtError: (self, _, zone, error, stackTrace) => self
386-
.parent
387-
.run(() => _handleError(zone, error, stackTrace))));
388-
}, when: liveTest.test.metadata.chainStackTraces);
439+
/// Runs [callback], in a [Invoker.guard] context if [_guarded] is `true`.
440+
void _guardIfGuarded(void callback()) {
441+
if (_guarded) {
442+
Invoker.guard(callback);
443+
} else {
444+
callback();
445+
}
389446
}
390447

448+
/// Prints [text] as a message to [_controller].
449+
void _print(String text) => _controller.message(new Message.print(text));
450+
391451
/// Run [_tearDowns] in reverse order.
392452
Future _runTearDowns() async {
393453
while (_tearDowns.isNotEmpty) {

lib/src/runner/remote_listener.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:term_glyph/term_glyph.dart' as glyph;
99

1010
import '../backend/declarer.dart';
1111
import '../backend/group.dart';
12+
import '../backend/invoker.dart';
1213
import '../backend/live_test.dart';
1314
import '../backend/metadata.dart';
1415
import '../backend/operating_system.dart';
@@ -97,7 +98,14 @@ class RemoteListener {
9798
: OperatingSystem.find(message['os']),
9899
path: message['path']);
99100

100-
new RemoteListener._(suite, printZone)._listen(channel);
101+
runZoned(() {
102+
Invoker.guard(
103+
() => new RemoteListener._(suite, printZone)._listen(channel));
104+
},
105+
// Make the declarer visible to running tests so that they'll throw
106+
// useful errors when calling `test()` and `group()` within a test,
107+
// and so they can add to the declarer's `tearDownAll()` list.
108+
zoneValues: {#test.declarer: declarer});
101109
}, onError: (error, stackTrace) {
102110
_sendError(channel, error, stackTrace, verboseChain);
103111
}, zoneSpecification: new ZoneSpecification(print: (_, __, ___, line) {

lib/test.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ Declarer get _declarer {
6767
ExpandedReporter.watch(engine,
6868
color: true, printPath: false, printPlatform: false);
6969

70-
var success = await engine.run();
70+
var success = await runZoned(() => Invoker.guard(engine.run),
71+
zoneValues: {#test.declarer: _globalDeclarer});
7172
// TODO(nweiz): Set the exit code on the VM when issue 6943 is fixed.
7273
if (success) return null;
7374
print('');
@@ -252,6 +253,9 @@ void tearDown(callback()) => _declarer.tearDown(callback);
252253
///
253254
/// The [callback] is run before any callbacks registered with [tearDown]. Like
254255
/// [tearDown], the most recently registered callback is run first.
256+
///
257+
/// If this is called from within a [setUpAll] or [tearDownAll] callback, it
258+
/// instead runs the function after *all* tests in the current test suite.
255259
void addTearDown(callback()) {
256260
if (Invoker.current == null) {
257261
throw new StateError("addTearDown() may only be called within a test.");

pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: test
2-
version: 0.12.27-dev
2+
version: 0.12.27
33
author: Dart Team <misc@dartlang.org>
44
description: A library for writing dart unit tests.
55
homepage: https://github.com/dart-lang/test
@@ -29,7 +29,7 @@ dependencies:
2929
source_map_stack_trace: '^1.1.4'
3030
source_maps: '^0.10.2'
3131
source_span: '^1.4.0'
32-
stack_trace: '^1.6.0'
32+
stack_trace: '^1.9.0'
3333
stream_channel: '^1.6.0'
3434
string_scanner: '>=0.1.1 <2.0.0'
3535
term_glyph: '^1.0.0'

0 commit comments

Comments
 (0)