blob: 307f76e9dac2ed77acb9bf9ea86aa5be86fefe85 [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// 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:async_helper/async_helper.dart";
import "package:expect/expect.dart";
main() {
// Values used.
var error = StateError("test");
var stack = StackTrace.fromString("for testing");
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();
/// 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 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");
});
}
}
// 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?>();
}
}
}
}
}
}
}
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();
}