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:
Lasse R.H. Nielsen
2023-02-18 12:01:45 +00:00
committed by Commit Queue
parent 7c6423cb32
commit 61a4fb4f9e
3 changed files with 231 additions and 104 deletions
+18 -7
View File
@@ -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
+17
View File
@@ -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)) {
+196 -97
View File
@@ -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();
}