| // 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(); |
| } |