Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 63bff1a

Browse files
munificentcommit-bot@chromium.org
authored andcommitted
Improve asynchronous test failures and async_minitest.
There's a bundle of improvements here: - Show the stack trace of the failure when a test using async_minitest fails. The trade-off is that it now only reports the first failure. Collecting all of them didn't play nice with the stack trace deobfuscation the test runner does. - Report the group and name of the test that failed when known. - Fix as many double-reporting test result bugs as I could find. Also, in case that still happens, don't spew out a pile of useless JSON. - Correctly wait for a test to complete even if it schedules its own asynchronous operations without returning futures. - Generally simplify and clean some stuff up. Change-Id: Ie020ae0b80a11764c455cf0ce24dfea09ca0adce Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/138903 Reviewed-by: Srujan Gaddam <[email protected]> Reviewed-by: Sigmund Cherem <[email protected]> Commit-Queue: Bob Nystrom <[email protected]>
1 parent d7f697f commit 63bff1a

File tree

7 files changed

+91
-140
lines changed

7 files changed

+91
-140
lines changed

pkg/async_helper/lib/async_helper.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
/// If a test is asynchronous, it needs to notify the testing driver
77
/// about this (otherwise tests may get reported as passing [after main()
88
/// finished] even if the asynchronous operations fail).
9-
/// Tests which can't use the unittest framework should use the helper functions
10-
/// in this library.
9+
///
1110
/// This library provides four methods
1211
/// - asyncStart(): Needs to be called before an asynchronous operation is
1312
/// scheduled.

pkg/async_helper/lib/async_minitest.dart

Lines changed: 46 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
/// unittest/test API used by the language and core library.
77
///
88
/// Compared with `minitest.dart`, this library supports and expects
9-
/// asynchronous tests. It uses a `Zone` per test to collect errors in the
10-
/// correct test.
9+
/// asynchronous tests. It uses a `Zone` per test to associate a test name with
10+
/// the failure.
11+
///
1112
/// It does not support `setUp` or `tearDown` methods,
1213
/// and matchers are severely restricted.
1314
///
@@ -42,17 +43,18 @@ void group(String name, body()) {
4243

4344
void test(String name, body()) {
4445
var oldName = _pushName(name);
45-
var test = new _Test(_currentName)..asyncWait();
46-
var result =
47-
runZoned(body, zoneValues: {_testToken: test}, onError: test.fail);
46+
47+
asyncStart();
48+
var result = runZoned(body, zoneValues: {_testToken: _currentName});
4849
if (result is Future) {
4950
result.then((_) {
50-
test.asyncDone();
51-
}, onError: test.fail);
51+
asyncEnd();
52+
});
5253
} else {
5354
// Ensure all tests get to be set up first.
54-
scheduleMicrotask(test.asyncDone);
55+
scheduleMicrotask(asyncEnd);
5556
}
57+
5658
_popName(oldName);
5759
}
5860

@@ -69,85 +71,84 @@ void expect(dynamic value, dynamic matcher, {String reason}) {
6971
}
7072

7173
R Function() expectAsync0<R>(R Function() f, {int count = 1}) {
72-
var test = _currentTest..asyncWait(count);
74+
asyncStart(count);
7375
return () {
7476
var result = f();
75-
test.asyncDone();
77+
asyncEnd();
7678
return result;
7779
};
7880
}
7981

8082
R Function(A) expectAsync1<R, A>(R Function(A) f, {int count = 1}) {
81-
var test = _currentTest..asyncWait(count);
83+
asyncStart(count);
8284
return (A a) {
8385
var result = f(a);
84-
test.asyncDone();
86+
asyncEnd();
8587
return result;
8688
};
8789
}
8890

8991
R Function(A, B) expectAsync2<R, A, B>(R Function(A, B) f, {int count = 1}) {
90-
var test = _currentTest..asyncWait(count);
92+
asyncStart(count);
9193
return (A a, B b) {
9294
var result = f(a, b);
93-
test.asyncDone();
95+
asyncEnd();
9496
return result;
9597
};
9698
}
9799

98100
dynamic expectAsync(Function f, {int count = 1}) {
99101
var f2 = f; // Avoid type-promoting f, we want dynamic invocations.
100-
var test = _currentTest;
101102
if (f2 is Function(Null, Null, Null, Null, Null)) {
102-
test.asyncWait(count);
103+
asyncStart(count);
103104
return ([a, b, c, d, e]) {
104105
var result = f(a, b, c, d, e);
105-
test.asyncDone();
106+
asyncEnd();
106107
return result;
107108
};
108109
}
109110
if (f2 is Function(Null, Null, Null, Null)) {
110-
test.asyncWait(count);
111+
asyncStart(count);
111112
return ([a, b, c, d]) {
112113
var result = f(a, b, c, d);
113-
test.asyncDone();
114+
asyncEnd();
114115
return result;
115116
};
116117
}
117118
if (f2 is Function(Null, Null, Null)) {
118-
test.asyncWait(count);
119+
asyncStart(count);
119120
return ([a, b, c]) {
120121
var result = f(a, b, c);
121-
test.asyncDone();
122+
asyncEnd();
122123
return result;
123124
};
124125
}
125126
if (f2 is Function(Null, Null)) {
126-
test.asyncWait(count);
127+
asyncStart(count);
127128
return ([a, b]) {
128129
var result = f(a, b);
129-
test.asyncDone();
130+
asyncEnd();
130131
return result;
131132
};
132133
}
133134
if (f2 is Function(Null)) {
134-
test.asyncWait(count);
135+
asyncStart(count);
135136
return ([a]) {
136137
var result = f(a);
137-
test.asyncDone();
138+
asyncEnd();
138139
return result;
139140
};
140141
}
141142
if (f2 is Function()) {
142-
test.asyncWait(count);
143+
asyncStart(count);
143144
return () {
144145
var result = f();
145-
test.asyncDone();
146+
asyncEnd();
146147
return result;
147148
};
148149
}
149150
throw new UnsupportedError(
150-
"expectAsync only accepts up to five arguemnt functions");
151+
"expectAsync only accepts up to five argument functions");
151152
}
152153

153154
// Matchers
@@ -212,13 +213,13 @@ bool isStateError(dynamic o) {
212213

213214
void _checkThrow<T>(dynamic v, void onError(error)) {
214215
if (v is Future) {
215-
var test = _currentTest..asyncWait();
216+
asyncStart();
216217
v.then((_) {
217218
Expect.fail("Did not throw");
218219
}, onError: (e, s) {
219220
if (e is! T) throw e;
220221
if (onError != null) onError(e);
221-
test.asyncDone();
222+
asyncEnd();
222223
});
223224
return;
224225
}
@@ -251,20 +252,18 @@ Matcher throwsA(matcher) => (dynamic o) {
251252
Matcher completion(matcher) => (dynamic o) {
252253
Expect.type<Future>(o);
253254
Future future = o;
254-
_currentTest.asyncWait();
255+
asyncStart();
255256
future.then((value) {
256257
expect(value, matcher);
257-
_currentTest.asyncDone();
258+
asyncEnd();
258259
});
259260
};
260261

261262
void completes(dynamic o) {
262263
Expect.type<Future>(o);
263264
Future future = o;
264-
_currentTest.asyncWait();
265-
future.then((_) {
266-
_currentTest.asyncDone();
267-
});
265+
asyncStart();
266+
future.then(asyncSuccess);
268267
}
269268

270269
void isMap(dynamic o) {
@@ -297,12 +296,21 @@ String fail(String message) {
297296
Expect.fail("$message");
298297
}
299298

300-
// Internal helpers.
299+
/// Key for zone value holding current test name.
300+
final _testToken = Object();
301+
302+
bool _initializedTestNameCallback = false;
301303

302-
// The current combined name of the nesting [group] or [test].
304+
/// The current combined name of the nesting [group] or [test].
303305
String _currentName = null;
304306

305307
String _pushName(String newName) {
308+
// Look up the current test name from the zone created for the test.
309+
if (!_initializedTestNameCallback) {
310+
ExpectException.setTestNameCallback(() => Zone.current[_testToken]);
311+
_initializedTestNameCallback = true;
312+
}
313+
306314
var oldName = _currentName;
307315
if (oldName == null) {
308316
_currentName = newName;
@@ -315,73 +323,3 @@ String _pushName(String newName) {
315323
void _popName(String oldName) {
316324
_currentName = oldName;
317325
}
318-
319-
// Key for zone value holding current test object.
320-
final Object _testToken = new Object();
321-
322-
_Test get _currentTest =>
323-
Zone.current[_testToken] ?? (throw new StateError("Not inside test!"));
324-
325-
class _Test {
326-
static int activeTests = 0;
327-
static int failedTests = 0;
328-
329-
final String name;
330-
bool completed = false;
331-
bool failed = false;
332-
int asyncExpected = 0;
333-
_Test(this.name) {
334-
activeTests++;
335-
}
336-
337-
void asyncWait([int n = 1]) {
338-
if (completed) {
339-
print("ERROR: $name: New operations started after completion."
340-
"${StackTrace.current}");
341-
} else if (asyncExpected == 0) {
342-
asyncStart(); // Matched by asyncEnd in [_complete];
343-
}
344-
asyncExpected += n;
345-
}
346-
347-
void asyncDone() {
348-
if (asyncExpected == 0) {
349-
print("ERROR: $name: More asyncEnds than asyncStarts.\n"
350-
"${StackTrace.current}");
351-
} else {
352-
asyncExpected--;
353-
if (asyncExpected == 0 && !completed) {
354-
print("SUCCESS: $name");
355-
_complete();
356-
}
357-
}
358-
}
359-
360-
void fail(Object error, StackTrace stack) {
361-
if (!completed) {
362-
failed = true;
363-
failedTests++;
364-
print("FAILURE: $name: $error\n$stack");
365-
_complete();
366-
} else {
367-
if (!failed) {
368-
failed = true;
369-
failedTests++;
370-
}
371-
print("FAILURE: $name: (after completion) $error\n$stack");
372-
}
373-
}
374-
375-
void _complete() {
376-
assert(!completed);
377-
completed = true;
378-
activeTests--;
379-
if (failedTests == 0) {
380-
asyncEnd();
381-
} else if (activeTests == 0) {
382-
Zone.root.scheduleMicrotask(() {
383-
Expect.fail("$failedTests tests failed");
384-
});
385-
}
386-
}
387-
}

pkg/expect/lib/expect.dart

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -690,9 +690,29 @@ bool _identical(a, b) => identical(a, b);
690690
///
691691
/// Always recognized by [Expect.throws] as an unexpected error.
692692
class ExpectException {
693+
/// Call this to provide a function that associates a test name with this
694+
/// failure.
695+
///
696+
/// Used by async_helper/async_minitest.dart to inject logic to bind the
697+
/// `group()` and `test()` name strings to a test failure.
698+
static void setTestNameCallback(String Function() getName) {
699+
_getTestName = getName;
700+
}
701+
702+
// TODO(rnystrom): Type this `String Function()?` once this library doesn't
703+
// need to be NNBD-agnostic.
704+
static dynamic _getTestName;
705+
693706
final String message;
694-
ExpectException(this.message);
695-
String toString() => message;
707+
final String name;
708+
709+
ExpectException(this.message)
710+
: name = (_getTestName == null) ? null : _getTestName();
711+
712+
String toString() {
713+
if (name != null) return 'In test "$name" $message';
714+
return message;
715+
}
696716
}
697717

698718
/// Is true iff type assertions are enabled.

pkg/test_runner/lib/src/browser.dart

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -218,32 +218,17 @@ requirejs(["$testName", "dart_sdk", "async_helper"],
218218
return lines.join("\\n");
219219
};
220220
221-
let pendingCallbacks = 0;
222-
let waitForDone = false, isDone = false;
223-
224221
sdk.dart.addAsyncCallback = function() {
225-
pendingCallbacks++;
226-
if (!waitForDone) {
227-
// When the first callback is added, signal that test_controller.js
228-
// should wait until done.
229-
waitForDone = true;
230-
dartPrint('unittest-suite-wait-for-done');
231-
}
222+
async_helper.async_helper.asyncStart();
232223
};
233224
234225
sdk.dart.removeAsyncCallback = function() {
235-
if (--pendingCallbacks <= 0) {
236-
// We might be done with async callbacks. Schedule a task to check.
237-
// Note: can't use a Promise here, because the unhandled rejection event
238-
// is fired as a task, rather than a microtask. `setTimeout` will create a
239-
// task, giving an unhandled promise reject time to fire before this does.
240-
setTimeout(() => {
241-
if (pendingCallbacks <= 0 && !isDone) {
242-
isDone = true;
243-
dartPrint('unittest-suite-done');
244-
}
245-
}, 0);
246-
}
226+
// removeAsyncCallback() is called *before* the async operation is
227+
// performed, but we don't want to report the test as being done until
228+
// after that operation completes, so wait for that callback to run.
229+
setTimeout(() => {
230+
async_helper.async_helper.asyncEnd();
231+
}, 0);
247232
};
248233
249234
if ($isNnbd) {

pkg/test_runner/lib/src/browser_controller.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,10 +1120,7 @@ class BrowserTestRunner {
11201120

11211121
DebugLogger.warning("Double reporting tests:");
11221122
for (var id in doubleReportingOutputs.keys) {
1123-
DebugLogger.warning("${testCache[id]}, output: ");
1124-
DebugLogger.warning("${doubleReportingOutputs[id]}");
1125-
DebugLogger.warning("");
1126-
DebugLogger.warning("");
1123+
DebugLogger.warning("${testCache[id]}");
11271124
}
11281125
}
11291126

0 commit comments

Comments
 (0)