Skip to content

Commit ce12670

Browse files
authored
Fix @failingTest not working correctly for out-of-band exceptions (#2216)
1 parent 8bb94d8 commit ce12670

File tree

4 files changed

+52
-19
lines changed

4 files changed

+52
-19
lines changed

pkgs/test_reflective_loader/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.5.0
2+
3+
- Unawaited asynchronous exceptions in tests marked with `@FailingTest()` are
4+
now handled correctly and will allow the test to pass.
5+
- Tests marked with `@FailingTest()` but passing no longer run until the
6+
timeout.
7+
18
## 0.4.0
29

310
- Add support for one-time set up and teardown in test classes via static

pkgs/test_reflective_loader/lib/test_reflective_loader.dart

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -246,29 +246,32 @@ Future<Object?> _invokeSymbolIfExists(
246246
/// - The test fails by throwing an exception
247247
/// - The test returns a future which completes with an error.
248248
/// - An exception is thrown to the zone handler from a timer task.
249-
Future<Object?>? _runFailingTest(ClassMirror classMirror, Symbol symbol) {
250-
var passed = false;
251-
return runZonedGuarded(() {
249+
Future<void> _runFailingTest(ClassMirror classMirror, Symbol symbol) async {
250+
_FailedTestResult? result;
251+
252+
await runZonedGuarded(() {
252253
// ignore: void_checks
253254
return Future.sync(() => _runTest(classMirror, symbol)).then<void>((_) {
254-
passed = true;
255-
test_package.fail('Test passed - expected to fail.');
255+
// We can't throw async exceptions inside here because `runZoneGuarded`
256+
// will never complete (see docs on `runZonedGuarded`), so we need to
257+
// capture this state and throw later if there wasn't otherwise an
258+
// exception.
259+
260+
// If we didn't already have a failure (eg. an unawaited exception) then
261+
// this successful completion is an unexpected pass state.
262+
result ??= _FailedTestResult.pass;
256263
}).catchError((Object e) {
257-
// if passed, and we call fail(), rethrow this exception
258-
if (passed) {
259-
// ignore: only_throw_errors
260-
throw e;
261-
}
262-
// otherwise, an exception is not a failure for _runFailingTest
264+
// an awaited exception is always expected failure.
265+
result = _FailedTestResult.expectedFail;
263266
});
264267
}, (e, st) {
265-
// if passed, and we call fail(), rethrow this exception
266-
if (passed) {
267-
// ignore: only_throw_errors
268-
throw e;
269-
}
270-
// otherwise, an exception is not a failure for _runFailingTest
268+
result = _FailedTestResult.expectedFail;
271269
});
270+
271+
// We can safely throw exceptions back outside of the error zone.
272+
if (result == _FailedTestResult.pass) {
273+
throw test_package.TestFailure('Test passed - expected to fail.');
274+
}
272275
}
273276

274277
Future<void> _runTest(ClassMirror classMirror, Symbol symbol) async {
@@ -389,6 +392,15 @@ class _Test {
389392
timeout = null;
390393
}
391394

395+
/// The result of a test that was expected to fail.
396+
enum _FailedTestResult {
397+
/// The test (unexpectedly) passed.
398+
pass,
399+
400+
/// The test failed as expected.
401+
expectedFail,
402+
}
403+
392404
extension on DeclarationMirror {
393405
test_package.TestLocation? get testLocation {
394406
if (location case var location?) {

pkgs/test_reflective_loader/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: test_reflective_loader
2-
version: 0.4.0
2+
version: 0.5.0
33
description: Support for discovering tests and test suites using reflection.
44
repository: https://github.com/dart-lang/tools/tree/main/pkgs/test_reflective_loader
55
issue_tracker: https://github.com/dart-lang/tools/labels/package%3Atest_reflective_loader

pkgs/test_reflective_loader/test/test_reflective_loader_test.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,24 @@ class TestReflectiveLoaderTest {
7171
}
7272

7373
@failingTest
74-
Future test_fails_throws_async() {
74+
Future<void> test_fails_throws_async() {
7575
return Future.error('foo');
7676
}
7777

78+
@failingTest
79+
Future<void> test_fails_throws_async_unawaitedFuture() async {
80+
var completer = Completer<void>();
81+
// This exception occurs during the test run but isn't directly awaited so
82+
// this method doesn't not itself complete with an error.
83+
//
84+
// This is a legit test failure and will be marked as such without
85+
// the `@failingTest` annotation.
86+
unawaited(Future<void>.value()
87+
.then((_) => throw UnimplementedError('foo'))
88+
.whenComplete(completer.complete));
89+
await completer.future;
90+
}
91+
7892
@failingTest
7993
void test_fails_throws_sync() {
8094
throw StateError('foo');

0 commit comments

Comments
 (0)