Switch from mustWaitFor to markPending (#1487)
Previously the call to `Invoker.current.removeOutstandingCallbacks`
happened synchronously when the callback was called. After the change to
use `testHandle.mustWaitFor(Future)` the callback would no longer be
called in a `FakeAsync` zone that does not have any further async
progression.
Avoid the complication with a more forgiving non-Future API which
abstracts the trickiness around zones (other than that the
`markPending` call must be in the test zone, which is the case for
all APIs) and errors.
diff --git a/pkgs/test_api/lib/hooks.dart b/pkgs/test_api/lib/hooks.dart
index c22fe55..cdfff57 100644
--- a/pkgs/test_api/lib/hooks.dart
+++ b/pkgs/test_api/lib/hooks.dart
@@ -2,6 +2,8 @@
// 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 'src/backend/closed_exception.dart';
@@ -45,14 +47,13 @@
_invoker.skip(message);
}
- /// Indicates that this test should not be considered done until [future]
- /// completes.
+ /// Indicates that this test should not be considered done until the returned
+ /// [OutstandingWork] is marked as complete.
///
- /// The test may time out before [future] completes.
- Future<T> mustWaitFor<T>(Future<T> future) {
+ /// The test may time out before the outstanding work completes.
+ OutstandingWork markPending() {
if (_invoker.closed) throw ClosedException();
- _invoker.addOutstandingCallback();
- return future.whenComplete(_invoker.removeOutstandingCallback);
+ return OutstandingWork._(_invoker, Zone.current);
}
/// Converts [stackTrace] to a [Chain] according to the current test's
@@ -61,6 +62,20 @@
_stackTraceFormatter.formatStackTrace(stackTrace);
}
+class OutstandingWork {
+ final Invoker _invoker;
+ final Zone _zone;
+ var _isComplete = false;
+ OutstandingWork._(this._invoker, this._zone) {
+ _invoker.addOutstandingCallback();
+ }
+ void complete() {
+ if (_isComplete) return;
+ _isComplete = true;
+ _zone.run(_invoker.removeOutstandingCallback);
+ }
+}
+
class OutsideTestException implements Exception {}
/// An exception thrown when a test assertion fails.
diff --git a/pkgs/test_api/lib/src/expect/async_matcher.dart b/pkgs/test_api/lib/src/expect/async_matcher.dart
index 236e0fd..2d920d4 100644
--- a/pkgs/test_api/lib/src/expect/async_matcher.dart
+++ b/pkgs/test_api/lib/src/expect/async_matcher.dart
@@ -29,17 +29,19 @@
@override
bool matches(item, Map matchState) {
- var result = matchAsync(item);
+ final result = matchAsync(item);
expect(result,
anyOf([equals(null), TypeMatcher<Future>(), TypeMatcher<String>()]),
reason: 'matchAsync() may only return a String, a Future, or null.');
if (result is Future) {
- TestHandle.current.mustWaitFor(result.then((realResult) {
+ final outstandingWork = TestHandle.current.markPending();
+ result.then((realResult) {
if (realResult != null) {
fail(formatFailure(this, item, realResult as String));
}
- }));
+ outstandingWork.complete();
+ });
} else if (result is String) {
matchState[this] = result;
return false;
diff --git a/pkgs/test_api/lib/src/expect/expect.dart b/pkgs/test_api/lib/src/expect/expect.dart
index 0342e87..a1da26a 100644
--- a/pkgs/test_api/lib/src/expect/expect.dart
+++ b/pkgs/test_api/lib/src/expect/expect.dart
@@ -106,11 +106,16 @@
if (result is String) {
fail(formatFailure(matcher, actual, result, reason: reason));
} else if (result is Future) {
- return test.mustWaitFor(result.then((realResult) {
+ final outstandingWork = test.markPending();
+ return result.then((realResult) {
if (realResult == null) return;
fail(formatFailure(matcher as Matcher, actual, realResult as String,
reason: reason));
- }));
+ }).whenComplete(() {
+ // Always remove this, in case the failure is caught and handled
+ // gracefully.
+ outstandingWork.complete();
+ });
}
return Future.sync(() {});
diff --git a/pkgs/test_api/lib/src/expect/expect_async.dart b/pkgs/test_api/lib/src/expect/expect_async.dart
index e326e47..35a0e6f 100644
--- a/pkgs/test_api/lib/src/expect/expect_async.dart
+++ b/pkgs/test_api/lib/src/expect/expect_async.dart
@@ -2,8 +2,6 @@
// 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:test_api/hooks.dart';
import 'util/placeholder.dart';
@@ -67,7 +65,7 @@
/// Whether this function has been called the requisite number of times.
late bool _complete;
- Completer<void>? _expectationSatisfied;
+ OutstandingWork? _outstandingWork;
/// Wraps [callback] in a function that asserts that it's called at least
/// [minExpected] times and no more than [maxExpected] times.
@@ -96,8 +94,7 @@
}
if (isDone != null || minExpected > 0) {
- var completer = _expectationSatisfied = Completer<void>();
- _test.mustWaitFor(completer.future);
+ _outstandingWork = _test.markPending();
_complete = false;
} else {
_complete = true;
@@ -137,7 +134,7 @@
if (_callback is Function(Never)) return max1;
if (_callback is Function()) return max0;
- _expectationSatisfied?.complete();
+ _outstandingWork?.complete();
throw ArgumentError(
'The wrapped function has more than 6 required arguments');
}
@@ -210,7 +207,7 @@
// Mark this callback as complete and remove it from the test case's
// oustanding callback count; if that hits zero the test is done.
_complete = true;
- _expectationSatisfied?.complete();
+ _outstandingWork?.complete();
}
}
diff --git a/pkgs/test_api/pubspec.yaml b/pkgs/test_api/pubspec.yaml
index e4c35ae..da7d4ec 100644
--- a/pkgs/test_api/pubspec.yaml
+++ b/pkgs/test_api/pubspec.yaml
@@ -22,6 +22,7 @@
matcher: '>=0.12.10 <0.12.11'
dev_dependencies:
+ fake_async: ^1.2.0
pedantic: ^1.10.0
test: any
test_core: any
diff --git a/pkgs/test_api/test/frontend/expect_async_test.dart b/pkgs/test_api/test/frontend/expect_async_test.dart
index fb68246..0b40a42 100644
--- a/pkgs/test_api/test/frontend/expect_async_test.dart
+++ b/pkgs/test_api/test/frontend/expect_async_test.dart
@@ -4,9 +4,10 @@
import 'dart:async';
+import 'package:fake_async/fake_async.dart';
+import 'package:test/test.dart';
import 'package:test_api/src/backend/live_test.dart';
import 'package:test_api/src/backend/state.dart';
-import 'package:test/test.dart';
import '../utils.dart';
@@ -316,6 +317,16 @@
expectTestPassed(liveTest);
});
+ test('may be called in a FakeAsync zone that does not run further', () async {
+ var liveTest = await runTestBody(() {
+ FakeAsync().run((_) {
+ var callback = expectAsync0(() {});
+ callback();
+ });
+ });
+ expectTestPassed(liveTest);
+ });
+
group('old-style expectAsync()', () {
test('works with no arguments', () async {
var callbackRun = false;