Update Future.onError extension method.
Now works better with futures that have been up-cast. CoreLibraryReviewExempt: Rewrite of existing pure Dart function. Change-Id: Iefd05b9cfd8ff0cc3e27fc1122a670030e0901ff Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/283680 Reviewed-by: Nate Bosch <nbosch@google.com> Commit-Queue: Lasse Nielsen <lrn@google.com>
This commit is contained in:
committed by
Commit Queue
parent
7c6423cb32
commit
61a4fb4f9e
@@ -748,8 +748,11 @@ abstract class Future<T> {
|
||||
/// and a stack trace for the error.
|
||||
/// In the case of `onError`,
|
||||
/// if the exception thrown is `identical` to the error argument to `onError`,
|
||||
/// and it is thrown *synchronously*
|
||||
/// the throw is considered a rethrow,
|
||||
/// and the original stack trace is used instead.
|
||||
/// To rethrow with the same stack trace in an asynchronous callback,
|
||||
/// use [Error.throwWithStackTrace].
|
||||
///
|
||||
/// If the callback returns a [Future],
|
||||
/// the future returned by `then` will be completed with
|
||||
@@ -1054,12 +1057,20 @@ extension FutureExtensions<T> on Future<T> {
|
||||
Future<T> onError<E extends Object>(
|
||||
FutureOr<T> handleError(E error, StackTrace stackTrace),
|
||||
{bool test(E error)?}) {
|
||||
// There are various ways to optimize this to avoid the double is E/as E
|
||||
// type check, but for now we are not optimizing the error path.
|
||||
return this.catchError(
|
||||
(Object error, StackTrace stackTrace) =>
|
||||
handleError(error as E, stackTrace),
|
||||
test: (Object error) => error is E && (test == null || test(error)));
|
||||
FutureOr<T> onError(Object error, StackTrace stackTrace) {
|
||||
if (error is! E || test != null && !test(error)) {
|
||||
// Counts as rethrow, preserves stack trace.
|
||||
throw error;
|
||||
}
|
||||
return handleError(error, stackTrace);
|
||||
}
|
||||
if (this is _Future<Object?>) {
|
||||
// Internal method working like `catchError`,
|
||||
// but allows specifying a different result future type.
|
||||
return unsafeCast<_Future<T>>(this)._safeOnError<T>(onError);
|
||||
}
|
||||
|
||||
return this.then<T>((T value) => value, onError: onError);
|
||||
}
|
||||
|
||||
/// Completely ignores this future and its result.
|
||||
@@ -1110,7 +1121,7 @@ class TimeoutException implements Exception {
|
||||
/// A way to produce Future objects and to complete them later
|
||||
/// with a value or error.
|
||||
///
|
||||
/// Most of the time, the simplest way to create a future is to just use
|
||||
/// Most of the time, the simples t way to create a future is to just use
|
||||
/// one of the [Future] constructors to capture the result of a single
|
||||
/// asynchronous computation:
|
||||
/// ```dart
|
||||
|
||||
@@ -349,6 +349,23 @@ class _Future<T> implements Future<T> {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Used by extension method `onError` to up-cast the result to `R`.
|
||||
//
|
||||
// Not public because we cannot statically ensure that [R] is a supertype
|
||||
// of [T].
|
||||
//
|
||||
// Avoids needing to allocate an extra value handler to do the up cast,
|
||||
// which is needed if using `.then`.
|
||||
Future<R> _safeOnError<R>(FutureOr<R> Function(Object, StackTrace) onError) {
|
||||
assert(this is _Future<R>); // Is up-cast.
|
||||
_Future<R> result = _Future<R>();
|
||||
if (!identical(result._zone, _rootZone)) {
|
||||
onError = result._zone.registerBinaryCallback(onError);
|
||||
}
|
||||
_addListener(_FutureListener<T, R>.catchError(result, onError, null));
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<T> whenComplete(dynamic action()) {
|
||||
_Future<T> result = new _Future<T>();
|
||||
if (!identical(result._zone, _rootZone)) {
|
||||
|
||||
@@ -2,114 +2,213 @@
|
||||
// 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 'package:async_helper/async_helper.dart';
|
||||
import "dart:async";
|
||||
|
||||
import "package:async_helper/async_helper.dart";
|
||||
import "package:expect/expect.dart";
|
||||
|
||||
main() {
|
||||
// Values used.
|
||||
var error = StateError("test");
|
||||
var stack = StackTrace.fromString("for testing");
|
||||
var error = Object();
|
||||
var stateError = StateError("test");
|
||||
var expectedError = AsyncError(error, stack);
|
||||
|
||||
var otherError = ArgumentError("Other error");
|
||||
var expectedOtherError = AsyncError(otherError, StackTrace.empty);
|
||||
|
||||
var errorFuture = Future<int?>.error(error, stack)..ignore();
|
||||
var nonNativeErrorFuture = NonNativeFuture<int?>._(errorFuture);
|
||||
|
||||
asyncStart();
|
||||
|
||||
{
|
||||
// Match any error.
|
||||
/// Perform `onError` on [future] with arguments, and expect [result].
|
||||
///
|
||||
/// If [result] is [AsyncError], expecte an error result with
|
||||
/// that error and stack trace.
|
||||
/// If [result.stackTrace] is [StackTrace.empty],
|
||||
/// check that the stack trace is *not* the original [stack].
|
||||
///
|
||||
/// Otherwise expect the future to complete with value equal to [result].
|
||||
void test<E extends Object, T>(
|
||||
String testName,
|
||||
Future<T> future,
|
||||
FutureOr<T> Function(E, StackTrace) onError, {
|
||||
bool Function(E)? test,
|
||||
Object? result,
|
||||
}) {
|
||||
asyncStart();
|
||||
var future = Future.error(error, stack);
|
||||
future.onError((e, s) {
|
||||
Expect.identical(error, e);
|
||||
Expect.identical(stack, s);
|
||||
asyncEnd();
|
||||
});
|
||||
var resultFuture = future.onError(onError, test: test);
|
||||
|
||||
if (result is AsyncError) {
|
||||
resultFuture.then((value) {
|
||||
Expect.fail("$testName: Did not throw, value: $value");
|
||||
}, onError: (Object error, StackTrace stackTrace) {
|
||||
Expect.identical(result.error, error, testName);
|
||||
if (result.stackTrace != StackTrace.empty) {
|
||||
Expect.identical(result.stackTrace, stackTrace, testName);
|
||||
} else {
|
||||
Expect.notEquals(stack, stackTrace, testName);
|
||||
}
|
||||
asyncEnd();
|
||||
});
|
||||
} else {
|
||||
resultFuture.then((value) {
|
||||
Expect.equals(result, value);
|
||||
asyncEnd();
|
||||
}, onError: (error, stackTrace) {
|
||||
Expect.fail("$testName: Threw $error");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// With matching test.
|
||||
asyncStart();
|
||||
var future = Future.error(error, stack);
|
||||
future.onError((e, s) {
|
||||
Expect.identical(error, e);
|
||||
Expect.identical(stack, s);
|
||||
asyncEnd();
|
||||
}, test: (o) => true);
|
||||
// Go through all the valid permutations of the following options:
|
||||
//
|
||||
// * Native future or non-native future.
|
||||
// * The error type parameter matches the error or not.
|
||||
// * If type matches, test function is provided or not.
|
||||
// * If test function provided, it returns either true or false.
|
||||
// * If type and test both succeeds,
|
||||
// * Error handler is synchronous or asynchronous
|
||||
// * Error handler returns a value, throws new error,
|
||||
// or rethrows same error.
|
||||
// * The future is up-cast to a supertype or not when `onError` is called.
|
||||
// (Since `onError` is an extension method, it affects the method.)
|
||||
//
|
||||
// The expected result is computed for each combination,
|
||||
// and checked against the behavior:
|
||||
// * If type or test fails to match,
|
||||
// the original error and stack is the error result
|
||||
// * If type matches and test doesn't reject,
|
||||
// the error handler is invoked.
|
||||
// * If it returns a value, that becomes the result.
|
||||
// * If upcast, a return value of the supertype is accepted.
|
||||
// * If it throws a new error, that is the error result.
|
||||
// * If it throws the original error *synchronously*,
|
||||
// the original stack trace is retained.
|
||||
// * If it throws the original error *asynchronously*,
|
||||
// the original stack trace is not expected to be retained.
|
||||
for (var native in [true, false]) {
|
||||
var nativity = native ? "native" : "non-native";
|
||||
var future = native ? errorFuture : nonNativeErrorFuture;
|
||||
for (var upcast in [true, false]) {
|
||||
var upcastText = upcast ? " as Future<num?>" : "";
|
||||
// "fails" means not to match type parameter.
|
||||
// Rest means match type parameter, a bool is what `test` returns,
|
||||
// `null` means no test function.
|
||||
for (var testKind in ["fails", true, false, null]) {
|
||||
var matches = testKind == true || testKind == null;
|
||||
// If `matches` is false, the error handler should not be invoked.
|
||||
// Don't bother with creating sync/async versions of it then,
|
||||
// just one which fails if called.
|
||||
for (var async in [if (matches) true, false]) {
|
||||
var asyncText = async ? "async" : "sync";
|
||||
// Action of the handler body. If test doesn't match, it will fail.
|
||||
for (var action
|
||||
in matches ? ["return", "throw", "rethrow"] : ["fail"]) {
|
||||
// Called to provide the necessary types as type parameters.
|
||||
void doTest<E extends Object, T>() {
|
||||
String testName =
|
||||
"$nativity Future<int?>.error(StateError(..))$upcastText ";
|
||||
|
||||
bool Function(E)? testFunction;
|
||||
switch (testKind) {
|
||||
case "fails":
|
||||
testFunction = (E value) {
|
||||
Expect.fail("$testName: Matched error");
|
||||
throw "unreachable"; // Expect.fail throws.
|
||||
};
|
||||
break;
|
||||
case true:
|
||||
case false:
|
||||
var testResult = testKind == true;
|
||||
testFunction = (E value) => testResult;
|
||||
testName +=
|
||||
"matches type $E ${testResult ? "and test" : "but not test"}";
|
||||
break;
|
||||
default:
|
||||
testName += "matches type with no test";
|
||||
}
|
||||
Object? expectation = expectedError;
|
||||
FutureOr<T> Function(E, StackTrace) onError;
|
||||
switch (action) {
|
||||
case "return":
|
||||
expectation = upcast ? 3.14 : 42;
|
||||
testName += " and returns $expectation";
|
||||
onError = (E e, StackTrace s) {
|
||||
return expectation as T;
|
||||
};
|
||||
break;
|
||||
case "throw":
|
||||
expectation = expectedOtherError;
|
||||
onError = (E e, StackTrace s) {
|
||||
throw otherError;
|
||||
};
|
||||
testName += " and throws new error";
|
||||
break;
|
||||
case "rethrow":
|
||||
onError = (E e, StackTrace s) {
|
||||
throw e;
|
||||
};
|
||||
testName += " and rethrows";
|
||||
// Asynchronous callbacks rethrowing the error
|
||||
// doesn't count as a rethrow, only synchronous throws.
|
||||
if (async) expectation = AsyncError(error, StackTrace.empty);
|
||||
break;
|
||||
case "fail":
|
||||
onError = (E e, StackTrace s) {
|
||||
Expect.fail("$testName: Matched error");
|
||||
throw "unreachable"; // Expect.fail throws.
|
||||
};
|
||||
testName += " and rethrows original error";
|
||||
break;
|
||||
default:
|
||||
throw "unreachable";
|
||||
}
|
||||
if (async) {
|
||||
testName += " asynchronously";
|
||||
var originalOnError = onError;
|
||||
onError = (E e, StackTrace s) async => originalOnError(e, s);
|
||||
}
|
||||
test<E, T>(testName, future as Future<T>, onError,
|
||||
test: testFunction, result: expectation);
|
||||
}
|
||||
|
||||
// Find the types to use.
|
||||
if (testKind == "fails") {
|
||||
if (upcast) {
|
||||
doTest<Exception, num?>();
|
||||
} else {
|
||||
doTest<Exception, int?>();
|
||||
}
|
||||
} else {
|
||||
if (upcast) {
|
||||
doTest<StateError, num?>();
|
||||
} else {
|
||||
doTest<StateError, int?>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// With failing test.
|
||||
asyncStart();
|
||||
var future = Future.error(error, stack);
|
||||
var onErrorFuture = future.onError((e, s) {
|
||||
Expect.fail('unreachable');
|
||||
}, test: (o) => false);
|
||||
onErrorFuture.catchError((e, s) {
|
||||
Expect.identical(error, e);
|
||||
Expect.identical(stack, s);
|
||||
asyncEnd();
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// With matching type.
|
||||
asyncStart();
|
||||
var future = Future.error(stateError, stack);
|
||||
future.onError<StateError>((e, s) {
|
||||
Expect.identical(stateError, e);
|
||||
Expect.identical(stack, s);
|
||||
asyncEnd();
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// With non-matching type.
|
||||
asyncStart();
|
||||
var future = Future.error(stateError, stack);
|
||||
var onErrorFuture = future.onError<ArgumentError>((e, s) {
|
||||
Expect.fail('unreachable');
|
||||
});
|
||||
onErrorFuture.catchError((e, s) {
|
||||
Expect.identical(stateError, e);
|
||||
Expect.identical(stack, s);
|
||||
asyncEnd();
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// With non-matching type and matching test.
|
||||
asyncStart();
|
||||
var future = Future.error(stateError, stack);
|
||||
var onErrorFuture = future.onError<ArgumentError>((e, s) {
|
||||
Expect.fail('unreachable');
|
||||
}, test: (ArgumentError e) => true);
|
||||
onErrorFuture.catchError((e, s) {
|
||||
Expect.identical(stateError, e);
|
||||
Expect.identical(stack, s);
|
||||
asyncEnd();
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// With matching type and matching test.
|
||||
asyncStart();
|
||||
var future = Future.error(stateError, stack);
|
||||
future.onError<StateError>((e, s) {
|
||||
Expect.identical(stateError, e);
|
||||
Expect.identical(stack, s);
|
||||
asyncEnd();
|
||||
}, test: (StateError e) => true);
|
||||
}
|
||||
|
||||
{
|
||||
// With matching type and non-matching test.
|
||||
asyncStart();
|
||||
var future = Future.error(stateError, stack);
|
||||
var onErrorFuture = future.onError<StateError>((e, s) {
|
||||
Expect.fail('unreachable');
|
||||
}, test: (StateError e) => false);
|
||||
onErrorFuture.catchError((e, s) {
|
||||
Expect.identical(stateError, e);
|
||||
Expect.identical(stack, s);
|
||||
asyncEnd();
|
||||
});
|
||||
}
|
||||
asyncEnd();
|
||||
}
|
||||
|
||||
class NonNativeFuture<T> implements Future<T> {
|
||||
Future<T> _original;
|
||||
NonNativeFuture.value(T value) : _original = Future<T>.value(value);
|
||||
NonNativeFuture.error(Object error, [StackTrace? stack])
|
||||
: _original = Future<T>.error(error, stack)..ignore();
|
||||
NonNativeFuture._(this._original);
|
||||
Future<R> then<R>(FutureOr<R> Function(T) handleValue, {Function? onError}) =>
|
||||
NonNativeFuture<R>._(_original.then(handleValue, onError: onError));
|
||||
Future<T> catchError(Function handleError, {bool Function(Object)? test}) =>
|
||||
NonNativeFuture<T>._(_original.catchError(handleError, test: test));
|
||||
Future<T> whenComplete(FutureOr<void> Function() onDone) =>
|
||||
NonNativeFuture<T>._(_original.whenComplete(onDone));
|
||||
Future<T> timeout(Duration timeLimit, {FutureOr<T> Function()? onTimeout}) =>
|
||||
NonNativeFuture<T>._(_original.timeout(timeLimit, onTimeout: onTimeout));
|
||||
Stream<T> asStream() => _original.asStream();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user