| // Copyright (c) 2022, 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:meta/meta.dart' as meta; | |
| import 'package:test_api/hooks.dart'; | |
| import 'describe.dart'; | |
| import 'extensions/async.dart'; | |
| import 'extensions/core.dart'; | |
| import 'extensions/iterable.dart'; | |
| /// A target for checking expectations against a value in a test. | |
| /// | |
| /// A subject my have a real value, in which case the expectations can be | |
| /// validated or rejected; or it may be a placeholder, in which case | |
| /// expectations describe what would be checked but cannot be rejected. | |
| /// | |
| /// Expectation methods are defined in extensions `on Subject`, specialized on | |
| /// the generic [T]. | |
| /// Expectation extension methods can use the [ContextExtension] to interact | |
| /// with the [Context] for this subject. | |
| /// | |
| /// Create a subject that throws an exception for missed expectations with the | |
| /// [check] function. | |
| final class Subject<T> { | |
| final Context<T> _context; | |
| Subject._(this._context); | |
| } | |
| /// A callback that synchronously checks expectations against a subject. | |
| /// | |
| /// Asynchronous expectations should not be used within a `Condition` callback. | |
| typedef Condition<T> = void Function(Subject<T>); | |
| /// A callback that asynchronously checks expectations against a subject. | |
| /// | |
| /// Any expectations may be used within an `AsyncCondition` callback. | |
| typedef AsyncCondition<T> = FutureOr<void> Function(Subject<T>); | |
| extension SkipExtension<T> on Subject<T> { | |
| /// Mark the currently running test as skipped and return a [Subject] that | |
| /// will ignore all expectations. | |
| /// | |
| /// Any expectations against the return value will not be checked and will not | |
| /// be included in the "Expected" or "Actual" string representations of a | |
| /// failure. | |
| /// | |
| /// ```dart | |
| /// check(actual) | |
| /// ..stillChecked() | |
| /// ..skip('reason the expectation is temporarily not met').notChecked(); | |
| /// ``` | |
| /// | |
| /// If `skip` is used in a callback passed to `softCheck` or `describe` it | |
| /// will still mark the test as skipped, even though failing the expectation | |
| /// would not have otherwise caused the test to fail. | |
| Subject<T> skip(String message) { | |
| TestHandle.current.markSkipped(message); | |
| return Subject._(_SkippedContext()); | |
| } | |
| } | |
| /// Creates a [Subject] that can be used to validate expectations against | |
| /// [value], with an exception upon a failed expectation. | |
| /// | |
| /// Expectations that are not satisfied throw a [TestFailure] to interrupt the | |
| /// currently running test and mark it as failed. | |
| /// | |
| /// If [because] is passed it will be included as a "Reason:" line in failure | |
| /// messages. | |
| /// | |
| /// ```dart | |
| /// check(actual).equals(expected); | |
| /// ``` | |
| @meta.useResult | |
| Subject<T> check<T>(T value, {String? because}) => Subject._( | |
| _TestContext._root( | |
| value: _Present(value), | |
| // TODO - switch between "a" and "an" | |
| label: 'a $T', | |
| fail: (f) { | |
| final which = f.rejection.which; | |
| throw TestFailure( | |
| [ | |
| ...prefixFirst('Expected: ', f.detail.expected), | |
| ...prefixFirst('Actual: ', f.detail.actual), | |
| ...indent( | |
| prefixFirst('Actual: ', f.rejection.actual), | |
| f.detail.depth, | |
| ), | |
| if (which != null && which.isNotEmpty) | |
| ...indent(prefixFirst('Which: ', which), f.detail.depth), | |
| if (because != null) 'Reason: $because', | |
| ].join('\n'), | |
| ); | |
| }, | |
| allowAsync: true, | |
| allowUnawaited: true, | |
| ), | |
| ); | |
| /// Checks whether [value] satisfies all expectations invoked in [condition], | |
| /// without throwing an exception. | |
| /// | |
| /// Returns `null` if all expectations are satisfied, otherwise returns the | |
| /// [CheckFailure] for the first expectation that fails. | |
| /// | |
| /// Asynchronous expectations are not allowed in [condition] and will cause a | |
| /// runtime error if they are used. | |
| CheckFailure? softCheck<T>(T value, Condition<T> condition) { | |
| CheckFailure? failure; | |
| final subject = Subject<T>._( | |
| _TestContext._root( | |
| value: _Present(value), | |
| fail: (f) { | |
| failure ??= f; | |
| }, | |
| allowAsync: false, | |
| allowUnawaited: false, | |
| ), | |
| ); | |
| condition(subject); | |
| return failure; | |
| } | |
| /// Checks whether [value] satisfies all expectations invoked in [condition], | |
| /// without throwing an exception. | |
| /// | |
| /// The future will complete to `null` if all expectations are satisfied, | |
| /// otherwise it will complete to the [CheckFailure] for the first expectation | |
| /// that fails. | |
| /// | |
| /// In contrast to [softCheck], asynchronous expectations are allowed in | |
| /// [condition]. | |
| Future<CheckFailure?> softCheckAsync<T>( | |
| T value, | |
| AsyncCondition<T> condition, | |
| ) async { | |
| CheckFailure? failure; | |
| final subject = Subject<T>._( | |
| _TestContext._root( | |
| value: _Present(value), | |
| fail: (f) { | |
| failure ??= f; | |
| }, | |
| allowAsync: true, | |
| allowUnawaited: false, | |
| ), | |
| ); | |
| await condition(subject); | |
| return failure; | |
| } | |
| /// Creates a description of the expectations checked by [condition]. | |
| /// | |
| /// The strings are individual lines of a description. | |
| /// The description of an expectation may be one or more adjacent lines. | |
| /// | |
| /// Matches the "Expected: " lines in the output of a failure message if a value | |
| /// did not meet the last expectation in [condition], without the first labeled | |
| /// line. | |
| /// | |
| /// Asynchronous expectations are not allowed in [condition], for async | |
| /// conditions use [describeAsync]. | |
| Iterable<String> describe<T>(Condition<T> condition) { | |
| final context = _TestContext<T>._root( | |
| value: _Absent(), | |
| fail: (_) { | |
| throw UnimplementedError(); | |
| }, | |
| allowAsync: false, | |
| allowUnawaited: true, | |
| ); | |
| condition(Subject._(context)); | |
| return context.detail(context).expected.skip(1); | |
| } | |
| /// Creates a description of the expectations checked by [condition]. | |
| /// | |
| /// The strings are individual lines of a description. | |
| /// The description of an expectation may be one or more adjacent lines. | |
| /// | |
| /// Matches the "Expected: " lines in the output of a failure message if a value | |
| /// did not meet the last expectation in [condition], without the first labeled | |
| /// line. | |
| /// | |
| /// In contrast to [describe], asynchronous expectations are allowed in | |
| /// [condition]. | |
| Future<Iterable<String>> describeAsync<T>(AsyncCondition<T> condition) async { | |
| final context = _TestContext<T>._root( | |
| value: _Absent(), | |
| fail: (_) { | |
| throw UnimplementedError(); | |
| }, | |
| allowAsync: true, | |
| allowUnawaited: true, | |
| ); | |
| await condition(Subject._(context)); | |
| return context.detail(context).expected.skip(1); | |
| } | |
| extension ContextExtension<T> on Subject<T> { | |
| /// The expectations and nesting context for this subject. | |
| Context<T> get context => _context; | |
| } | |
| /// The context for a [Subject] that allows asserting expectations and creating | |
| /// nested subjects. | |
| /// | |
| /// A [Subject] is the target for checking expectations in a test. | |
| /// Every subject has a [Context] which holds the "actual" value, tracks how the | |
| /// value was obtained, and can check expectations about the value. | |
| /// | |
| /// The user focused APIs called within tests are expectation extension methods | |
| /// written in an extension `on Subject`, typically specialized to a specific | |
| /// generic. | |
| /// | |
| /// Expectation extension methods will make a call to one of the APIs on the | |
| /// subject's [Context], and can perform one of two types of operations: | |
| /// | |
| /// - Expect something of the current value (such as [CoreChecks.equals] or | |
| /// [IterableChecks.contains]) by calling [expect], [expectAsync], or | |
| /// [expectUnawaited]. | |
| /// - Expect that a new subject can be extracted from the current value (such | |
| /// as [CoreChecks.has] or [FutureChecks.completes]) by calling [nest] or | |
| /// [nestAsync]. | |
| /// | |
| /// | |
| /// Whichever type of operation, an expectation extension method provides two | |
| /// callbacks. | |
| /// The first callback is an `Iterable<String> Function()` returning a | |
| /// description of the expectation. | |
| /// The second callback always takes the actual value as an argument, and the | |
| /// specific signature varies by operation. | |
| /// | |
| /// | |
| /// In expectation extension methods calling [expect], [expectAsync], or | |
| /// [expectUnawaited], the `predicate` callback can report a [Rejection] if the | |
| /// value fails to satisfy the expectation. | |
| /// The description will be passed in a "clause" callback. | |
| /// {@template clause_description} | |
| /// The clause callback returns a description of what is checked which stands | |
| /// on its own. | |
| /// For instance the `is equal to <1>` in: | |
| /// | |
| /// ``` | |
| /// Expected: a int that: | |
| /// is equal to <1> | |
| /// ``` | |
| /// {@endtemplate} | |
| /// | |
| /// | |
| /// In expectation extension methods calling [nest] or [nestAsync], the | |
| /// `extract` callback can return a [Extracted.rejection] if the value fails to | |
| /// satisfy an expectation which disallows extracting the value, or an | |
| /// [Extracted.value] to become the value in a nested subject. | |
| /// The description will be passed in a "label" callback. | |
| /// {@template label_description} | |
| /// The label callback returns a description of the extracted subject as it | |
| /// relates to the original subject. | |
| /// For instance the `completes to a value` in: | |
| /// | |
| /// ``` | |
| /// Expected a Future<int> that: | |
| /// completes to a value that: | |
| /// is equal to <1> | |
| /// ``` | |
| /// | |
| /// A label should also be sensible when it is read as a clause. | |
| /// If no further expectations are checked on the extracted subject, or if the | |
| /// extraction is rejected, the "that:" is omitted in the output. | |
| /// | |
| /// ``` | |
| /// Expected a Future<int> that: | |
| /// completes to a value | |
| /// ``` | |
| /// {@endtemplate} | |
| /// | |
| /// | |
| /// A rejection carries two descriptions, one description of the "actual" value | |
| /// that was tested, and an optional "which" with further details about how the | |
| /// result different from the expectation. | |
| /// If the "actual" argument is omitted it will be filled with a representation | |
| /// of the value passed to the expectation callback formatted with [literal]. | |
| /// If an expectation extension method is written on a type of subject without a | |
| /// useful `toString()`, the rejection can provide a string representation to | |
| /// use instead. | |
| /// The "which" argument may be omitted if the reason is very obvious based on | |
| /// the clause and "actual" description, but most expectations should include a | |
| /// "which". | |
| /// | |
| /// The behavior of a context following a rejection depends on the source of the | |
| /// [Subject]. | |
| /// | |
| /// When an expectation is rejected for a [check] subject, an exception is | |
| /// thrown to interrupt the test, so no further checks should happen. The | |
| /// failure message will include: | |
| /// - An "Expected" section with descriptions of all the expectations that | |
| /// were checked, including the ones that passed, and the last one that | |
| /// failed. | |
| /// - An "Actual" section, which may be the description directly from the | |
| /// [Rejection] if the failure was on the root subject, or may start with a | |
| /// partial version of the "Expected" description up to the label for the | |
| /// nesting subject that saw a failure, then the "actual" from the rejection. | |
| /// - A "Which" description from the rejection, if it was included. | |
| /// | |
| /// For example, if a failure happens on the root subject, the "actual" is taken | |
| /// directly from the rejection. | |
| /// | |
| /// ``` | |
| /// Expected: a Future<int> that: | |
| /// completes to a value | |
| /// Actual: a future that completes as an error | |
| /// Which: threw <UnimplementedError> at: | |
| /// <stack trace> | |
| /// ``` | |
| /// | |
| /// But if the failure happens on a nested subject, the actual starts with a | |
| /// description of the nesting or non-nesting expectations that succeeded, up | |
| /// to nesting point of the failure, then the "actual" and "which" from the | |
| /// rejection are indented to that level of nesting. | |
| /// | |
| /// ``` | |
| /// Expected: a Future<int> that: | |
| /// completes to a value that: | |
| /// equals <1> | |
| /// Actual: a Future<int> that: | |
| /// completes to a value that: | |
| /// Actual: <0> | |
| /// Which: are not equal | |
| /// ``` | |
| /// | |
| /// ```dart | |
| /// extension CustomChecks on Subject<CustomType> { | |
| /// void someExpectation() { | |
| /// context.expect(() => ['meets this expectation'], (actual) { | |
| /// if (_expectationIsMet(actual)) return null; | |
| /// return Rejection(which: ['does not meet this expectation']); | |
| /// }); | |
| /// } | |
| /// | |
| /// Subject<Foo> get someDerivedValue => | |
| /// context.nest('has someDerivedValue', (actual) { | |
| /// if (_cannotReadDerivedValue(actual)) { | |
| /// return Extracted.rejection(which: ['cannot read someDerivedValue']); | |
| /// } | |
| /// return Extracted.value(_readDerivedValue(actual)); | |
| /// }); | |
| /// | |
| /// // for field reads that will not get rejected, use `has` | |
| /// Subject<Bar> get someField => has((a) => a.someField, 'someField'); | |
| /// } | |
| /// ``` | |
| /// | |
| /// When an expectation is rejected for a subject within a call to [softCheck] | |
| /// or [softCheckAsync] a [CheckFailure] will be returned with the rejection, as | |
| /// well as a [FailureDetail] which could be used to format the same failure | |
| /// message thrown by the [check] subject. | |
| /// | |
| /// {@template callbacks_may_be_unused} | |
| /// The description of an expectation may never be shown to the user, so the | |
| /// callback may never be invoked. | |
| /// If all the conditions on a subject succeed, or if the failure detail for a | |
| /// failed [softCheck] is never read, the descriptions will be unused. | |
| /// String formatting for the descriptions should be performed in the callback, | |
| /// not ahead of time. | |
| /// | |
| /// | |
| /// The context for a subject may hold a real "actual" value to test against, or | |
| /// it may have a placeholder within a call to [describe]. | |
| /// A context with a placeholder value will not invoke the callback to check | |
| /// expectations. | |
| /// | |
| /// If both callbacks are invoked, the description callback will always be | |
| /// called strictly after the expectation callback is called. | |
| /// | |
| /// Callbacks passed to a context should not throw. | |
| /// {@endtemplate} | |
| /// | |
| /// | |
| /// Some contexts disallow certain interactions. | |
| /// {@template async_limitations} | |
| /// Calls to [expectAsync] or [nestAsync] must not be performed by a condition | |
| /// callback passed to [softCheck] or [describe]. | |
| /// Use [softCheckAsync] or [describeAsync] for any condition which checks async | |
| /// expectations. | |
| /// {@endtemplate} | |
| /// {@template unawaited_limitations} | |
| /// Calls to [expectUnawaited] may not be performed by a condition callback | |
| /// passed to [softCheck] or [softCheckAsync]. | |
| /// {@endtemplate} | |
| /// | |
| /// Expectation extension methods can access the context for the subject with | |
| /// the [ContextExtension]. | |
| /// | |
| /// {@template description_lines} | |
| /// Description callbacks return an `Iterable<String>` where each element is a | |
| /// line in the output. Individual elements should not contain newlines. | |
| /// Utilities such as [prefixFirst], [postfixLast], and [literal] may be useful | |
| /// to format values which are potentially multiline. | |
| /// {@endtemplate} | |
| abstract final class Context<T> { | |
| /// Expect that [predicate] will not return a [Rejection] for the checked | |
| /// value. | |
| /// | |
| /// {@macro clause_description} | |
| /// | |
| /// {@macro description_lines} | |
| /// | |
| /// {@macro callbacks_may_be_unused} | |
| /// | |
| /// ```dart | |
| /// void someExpectation() { | |
| /// context.expect(() => ['meets this expectation'], (actual) { | |
| /// if (_expectationIsMet(actual)) return null; | |
| /// return Rejection(which: ['does not meet this expectation']); | |
| /// }); | |
| /// } | |
| /// ``` | |
| void expect( | |
| Iterable<String> Function() clause, | |
| Rejection? Function(T) predicate, | |
| ); | |
| /// Expect that [predicate] will not result in a [Rejection] for the checked | |
| /// value. | |
| /// | |
| /// {@macro clause_description} | |
| /// | |
| /// {@macro description_lines} | |
| /// | |
| /// {@macro callbacks_may_be_unused} | |
| /// | |
| /// {@macro async_limitations} | |
| /// | |
| /// Extensions which use `expectAsync` should always make that call | |
| /// synchronously and return the result so that exceptions stemming from use | |
| /// in a synchronous context can be reported synchronously. | |
| /// | |
| /// ```dart | |
| /// extension CustomChecks on Subject<CustomType> { | |
| /// Future<void> someAsyncExpectation() { | |
| /// return context.expectAsync(() => ['meets this async expectation'], | |
| /// (actual) async { | |
| /// if (await _expectationIsMet(actual)) return null; | |
| /// return Rejection(which: ['does not meet this async expectation']); | |
| /// }); | |
| /// } | |
| /// } | |
| /// ``` | |
| Future<void> expectAsync( | |
| Iterable<String> Function() clause, | |
| FutureOr<Rejection?> Function(T) predicate, | |
| ); | |
| /// Expect that [predicate] will not invoke the passed callback with a | |
| /// [Rejection] at any point. | |
| /// | |
| /// In contrast to [expectAsync], a rejection is reported through a | |
| /// callback instead of through a returned Future. The callback may be invoked | |
| /// at any point that the failure surfaces. | |
| /// | |
| /// This may be useful for a condition checking that some event _never_ | |
| /// happens. If there is no specific point where it is know to be safe to stop | |
| /// listening for the event, there is no way to complete a returned future and | |
| /// consider the check "complete". | |
| /// | |
| /// {@macro clause_description} | |
| /// | |
| /// {@macro description_lines} | |
| /// | |
| /// {@macro callbacks_may_be_unused} | |
| /// | |
| /// {@macro unawaited_limitations} | |
| /// The only useful effect of a late rejection is to throw a [TestFailure] | |
| /// when used with a [check] subject. Most conditions should prefer to use | |
| /// [expect] or [expectAsync]. | |
| /// | |
| /// ```dart | |
| /// void someUnawaitableExpectation() async { | |
| /// await context.expectUnawaited( | |
| /// () => ['meets this unawaitable expectation'], (actual, reject) { | |
| /// final failureSignal = _completeIfFailed(actual); | |
| /// unawaited(failureSignal.then((_) { | |
| /// reject(Reject( | |
| /// which: ['unexpectedly failed this unawaited expectation'])); | |
| /// })); | |
| /// }); | |
| /// } | |
| /// ``` | |
| void expectUnawaited( | |
| Iterable<String> Function() clause, | |
| void Function(T, void Function(Rejection)) predicate, | |
| ); | |
| /// Extract a property from the value for further checking. | |
| /// | |
| /// If the property cannot be extracted, [extract] should return an | |
| /// [Extracted.rejection] describing the problem. Otherwise it should return | |
| /// an [Extracted.value]. | |
| /// | |
| /// Subsequent expectations can be checked for the extracted value on the | |
| /// returned [Subject]. | |
| /// | |
| /// {@macro label_description} | |
| /// | |
| /// If [atSameLevel] is true then the returned [Extracted.value] should hold | |
| /// the same instance as the passed value, or an object which is is equivalent | |
| /// but has a type that is more convenient to test. | |
| /// In this case expectations applied to the returned [Subject] will behave as | |
| /// if they were applied to the subject for this context. | |
| /// The [label] will be used as if it were a "clause" argument passed to | |
| /// [expect]. | |
| /// If the label returns an empty iterable, the clause will be omitted. | |
| /// The label should only be left empty if the value extraction cannot be | |
| /// rejected. | |
| /// | |
| /// {@macro description_lines} | |
| /// | |
| /// {@macro callbacks_may_be_unused} | |
| /// | |
| /// ```dart | |
| /// Subject<Foo> get someDerivedValue => | |
| /// context.nest(() => ['has someDerivedValue'], (actual) { | |
| /// if (_cannotReadDerivedValue(actual)) { | |
| /// return Extracted.rejection( | |
| /// which: ['cannot read someDerivedValue']); | |
| /// } | |
| /// return Extracted.value(_readDerivedValue(actual)); | |
| /// }); | |
| /// ``` | |
| Subject<R> nest<R>( | |
| Iterable<String> Function() label, | |
| Extracted<R> Function(T) extract, { | |
| bool atSameLevel = false, | |
| }); | |
| /// Extract an asynchronous property from the value for further checking. | |
| /// | |
| /// If the property cannot be extracted, [extract] should return an | |
| /// [Extracted.rejection] describing the problem. Otherwise it should return | |
| /// an [Extracted.value]. | |
| /// | |
| /// In contrast to [nest], subsequent expectations need to be passed in | |
| /// [nestedCondition] which will be applied to the subject for the extracted | |
| /// value. | |
| /// | |
| /// {@macro label_description} | |
| /// | |
| /// {@macro description_lines} | |
| /// | |
| /// {@macro callbacks_may_be_unused} | |
| /// | |
| /// {@macro async_limitations} | |
| /// | |
| /// Extensions which use `nestAsync` should always make that call | |
| /// synchronously and return the result so that exceptions stemming from use | |
| /// in a synchronous context can be reported synchronously. | |
| /// | |
| /// ```dart | |
| /// Future<void> someAsyncResult( | |
| /// [AsyncCondition<Result> resultCondition]) { | |
| /// return context.nestAsync(() => ['has someAsyncResult'], (actual) async { | |
| /// if (await _asyncOperationFailed(actual)) { | |
| /// return Extracted.rejection(which: ['cannot read someAsyncResult']); | |
| /// } | |
| /// return Extracted.value(await _readAsyncResult(actual)); | |
| /// }, resultCondition); | |
| /// } | |
| /// ``` | |
| Future<void> nestAsync<R>( | |
| Iterable<String> Function() label, | |
| FutureOr<Extracted<R>> Function(T) extract, | |
| AsyncCondition<R>? nestedCondition, | |
| ); | |
| } | |
| /// A property extracted from a value being checked, or a rejection. | |
| final class Extracted<T> { | |
| final Rejection? _rejection; | |
| final T? _value; | |
| /// Creates a rejected extraction to indicate a failure trying to read the | |
| /// value. | |
| /// | |
| /// When a nesting is rejected with an omitted or empty [actual] argument, it | |
| /// will be filled in with the [literal] representation of the value. | |
| Extracted.rejection({ | |
| Iterable<String> actual = const [], | |
| Iterable<String>? which, | |
| }) : _rejection = Rejection(actual: actual, which: which), | |
| _value = null; | |
| Extracted.value(T this._value) : _rejection = null; | |
| Extracted._(Rejection this._rejection) : _value = null; | |
| Extracted<R> _map<R>(R Function(T) transform) { | |
| final rejection = _rejection; | |
| if (rejection != null) return Extracted._(rejection); | |
| return Extracted.value(transform(_value as T)); | |
| } | |
| Extracted<T> _fillActual(Object? actual) => | |
| _rejection == null || _rejection.actual.isNotEmpty | |
| ? this | |
| : Extracted.rejection(actual: literal(actual), which: _rejection.which); | |
| } | |
| abstract interface class _Optional<T> { | |
| R? apply<R extends FutureOr<Rejection?>>(R Function(T) callback); | |
| Future<Extracted<_Optional<R>>> mapAsync<R>( | |
| FutureOr<Extracted<R>> Function(T) transform, | |
| ); | |
| Extracted<_Optional<R>> map<R>(Extracted<R> Function(T) transform); | |
| } | |
| class _Present<T> implements _Optional<T> { | |
| final T value; | |
| _Present(this.value); | |
| @override | |
| R? apply<R extends FutureOr<Rejection?>>(R Function(T) c) => c(value); | |
| @override | |
| Future<Extracted<_Present<R>>> mapAsync<R>( | |
| FutureOr<Extracted<R>> Function(T) transform, | |
| ) async { | |
| final transformed = await transform(value); | |
| return transformed._map(_Present.new); | |
| } | |
| @override | |
| Extracted<_Present<R>> map<R>(Extracted<R> Function(T) transform) => | |
| transform(value)._map(_Present.new); | |
| } | |
| class _Absent<T> implements _Optional<T> { | |
| @override | |
| R? apply<R extends FutureOr<Rejection?>>(R Function(T) c) => null; | |
| @override | |
| Future<Extracted<_Absent<R>>> mapAsync<R>( | |
| FutureOr<Extracted<R>> Function(T) transform, | |
| ) async => Extracted.value(_Absent<R>()); | |
| @override | |
| Extracted<_Absent<R>> map<R>(FutureOr<Extracted<R>> Function(T) transform) => | |
| Extracted.value(_Absent<R>()); | |
| } | |
| final class _TestContext<T> implements Context<T>, _ClauseDescription { | |
| final _Optional<T> _value; | |
| /// A reference to find the root context which this context is nested under. | |
| /// | |
| /// null only for the root context. | |
| final _TestContext<dynamic>? _parent; | |
| final List<_ClauseDescription> _clauses; | |
| final List<_TestContext> _aliases; | |
| final void Function(CheckFailure) _fail; | |
| final bool _allowAsync; | |
| final bool _allowUnawaited; | |
| /// A callback that returns a label for this context. | |
| /// | |
| /// If this context is the root the label should return a phrase like | |
| /// "a List" in | |
| /// | |
| /// ``` | |
| /// Expected: a List that: | |
| /// ``` | |
| /// | |
| /// If this context is nested under another context the lable should return a | |
| /// phrase like "completes to a value" in | |
| /// | |
| /// | |
| /// ``` | |
| /// Expected: a Future<int> that: | |
| /// completes to a value that: | |
| /// ``` | |
| /// | |
| /// In cases where a nested context does not have any expectations checked on | |
| /// it, the "that:" will be will be omitted. | |
| final Iterable<String> Function() _label; | |
| static Iterable<String> _emptyLabel() => const []; | |
| /// Create a context appropriate for a subject which is not nested under any | |
| /// other subject. | |
| _TestContext._root({ | |
| required _Optional<T> value, | |
| required void Function(CheckFailure) fail, | |
| required bool allowAsync, | |
| required bool allowUnawaited, | |
| String? label, | |
| }) : _value = value, | |
| _label = (() => [label ?? '']), | |
| _fail = fail, | |
| _allowAsync = allowAsync, | |
| _allowUnawaited = allowUnawaited, | |
| _parent = null, | |
| _clauses = [], | |
| _aliases = []; | |
| _TestContext._alias(_TestContext original, this._value) | |
| : _parent = original, | |
| _clauses = original._clauses, | |
| _aliases = original._aliases, | |
| _fail = original._fail, | |
| _allowAsync = original._allowAsync, | |
| _allowUnawaited = original._allowUnawaited, | |
| // Never read from an aliased context because they are never present in | |
| // `_clauses`. | |
| _label = _emptyLabel; | |
| /// Create a context nested under [parent]. | |
| /// | |
| /// The [_label] callback should not return an empty iterable. | |
| _TestContext._child(this._value, this._label, _TestContext<dynamic> parent) | |
| : _parent = parent, | |
| _fail = parent._fail, | |
| _allowAsync = parent._allowAsync, | |
| _allowUnawaited = parent._allowUnawaited, | |
| _clauses = [], | |
| _aliases = []; | |
| @override | |
| void expect( | |
| Iterable<String> Function() clause, | |
| Rejection? Function(T) predicate, | |
| ) { | |
| _clauses.add(_ExpectationClause(clause)); | |
| final rejection = _value.apply( | |
| (actual) => predicate(actual)?._fillActual(actual), | |
| ); | |
| if (rejection != null) { | |
| _fail(_failure(rejection)); | |
| } | |
| } | |
| @override | |
| Future<void> expectAsync( | |
| Iterable<String> Function() clause, | |
| FutureOr<Rejection?> Function(T) predicate, | |
| ) { | |
| if (!_allowAsync) throw AsyncConditionDisallowed._('Async'); | |
| return _expectAsync(clause, predicate); | |
| } | |
| Future<void> _expectAsync( | |
| Iterable<String> Function() clause, | |
| FutureOr<Rejection?> Function(T) predicate, | |
| ) async { | |
| _clauses.add(_ExpectationClause(clause)); | |
| final outstandingWork = TestHandle.current.markPending(); | |
| try { | |
| final rejection = await _value.apply( | |
| (actual) async => (await predicate(actual))?._fillActual(actual), | |
| ); | |
| if (rejection == null) return; | |
| _fail(_failure(rejection)); | |
| } finally { | |
| outstandingWork.complete(); | |
| } | |
| } | |
| @override | |
| void expectUnawaited( | |
| Iterable<String> Function() clause, | |
| void Function(T actual, void Function(Rejection) reject) predicate, | |
| ) { | |
| if (!_allowUnawaited) throw AsyncConditionDisallowed._('Unawaited'); | |
| _clauses.add(_ExpectationClause(clause)); | |
| _value.apply((actual) { | |
| predicate(actual, (r) => _fail(_failure(r._fillActual(actual)))); | |
| }); | |
| } | |
| @override | |
| Subject<R> nest<R>( | |
| Iterable<String> Function() label, | |
| Extracted<R> Function(T) extract, { | |
| bool atSameLevel = false, | |
| }) { | |
| final result = _value.map((actual) => extract(actual)._fillActual(actual)); | |
| final rejection = result._rejection; | |
| if (rejection != null) { | |
| _clauses.add(_ExpectationClause(label)); | |
| _fail(_failure(rejection)); | |
| } | |
| final value = result._value ?? _Absent<R>(); | |
| final _TestContext<R> context; | |
| if (atSameLevel) { | |
| context = _TestContext._alias(this, value); | |
| _aliases.add(context); | |
| _clauses.add(_ExpectationClause(label)); | |
| } else { | |
| context = _TestContext._child(value, label, this); | |
| _clauses.add(context); | |
| } | |
| return Subject._(context); | |
| } | |
| @override | |
| Future<void> nestAsync<R>( | |
| Iterable<String> Function() label, | |
| FutureOr<Extracted<R>> Function(T) extract, [ | |
| AsyncCondition<R>? nestedCondition, | |
| ]) { | |
| if (!_allowAsync) throw AsyncConditionDisallowed._('Async'); | |
| return _nestAsync(label, extract, nestedCondition); | |
| } | |
| Future<void> _nestAsync<R>( | |
| Iterable<String> Function() label, | |
| FutureOr<Extracted<R>> Function(T) extract, | |
| AsyncCondition<R>? nestedCondition, | |
| ) async { | |
| final outstandingWork = TestHandle.current.markPending(); | |
| try { | |
| final result = await _value.mapAsync( | |
| (actual) async => (await extract(actual))._fillActual(actual), | |
| ); | |
| final rejection = result._rejection; | |
| if (rejection != null) { | |
| _clauses.add(_ExpectationClause(label)); | |
| _fail(_failure(rejection)); | |
| } | |
| final value = result._value ?? _Absent<R>(); | |
| final context = _TestContext<R>._child(value, label, this); | |
| _clauses.add(context); | |
| await nestedCondition?.call(Subject<R>._(context)); | |
| } finally { | |
| outstandingWork.complete(); | |
| } | |
| } | |
| CheckFailure _failure(Rejection rejection) => | |
| CheckFailure(rejection, () => _root.detail(this)); | |
| _TestContext get _root { | |
| _TestContext<dynamic> current = this; | |
| while (current._parent != null) { | |
| current = current._parent; | |
| } | |
| return current; | |
| } | |
| @override | |
| FailureDetail detail(_TestContext failingContext) { | |
| final thisContextFailed = | |
| identical(failingContext, this) || _aliases.contains(failingContext); | |
| var foundDepth = thisContextFailed ? 0 : -1; | |
| var foundOverlap = thisContextFailed ? 0 : -1; | |
| var successfulOverlap = 0; | |
| final expected = <String>[]; | |
| if (_clauses.isEmpty) { | |
| expected.addAll(_label()); | |
| } else { | |
| final label = postfixLast(' that:', _label()); | |
| expected.addAll(label); | |
| final labelLines = label.length; | |
| for (var clause in _clauses) { | |
| final details = clause.detail(failingContext); | |
| expected.addAll(indent(details.expected)); | |
| if (details.depth >= 0) { | |
| assert(foundDepth == -1); | |
| assert(foundOverlap == -1); | |
| foundDepth = details.depth + 1; | |
| foundOverlap = | |
| details._actualOverlap + successfulOverlap + labelLines; | |
| } else { | |
| if (foundDepth == -1) { | |
| successfulOverlap += details.expected.length; | |
| } | |
| } | |
| } | |
| } | |
| return FailureDetail(expected, foundOverlap, foundDepth); | |
| } | |
| } | |
| /// A context which never runs expectations and can never fail. | |
| final class _SkippedContext<T> implements Context<T> { | |
| @override | |
| void expect( | |
| Iterable<String> Function() clause, | |
| Rejection? Function(T) predicate, | |
| ) { | |
| // no-op | |
| } | |
| @override | |
| Future<void> expectAsync( | |
| Iterable<String> Function() clause, | |
| FutureOr<Rejection?> Function(T) predicate, | |
| ) async { | |
| // no-op | |
| } | |
| @override | |
| void expectUnawaited( | |
| Iterable<String> Function() clause, | |
| void Function(T actual, void Function(Rejection) reject) predicate, | |
| ) { | |
| // no-op | |
| } | |
| @override | |
| Subject<R> nest<R>( | |
| Iterable<String> Function() label, | |
| Extracted<R> Function(T p1) extract, { | |
| bool atSameLevel = false, | |
| }) { | |
| return Subject._(_SkippedContext()); | |
| } | |
| @override | |
| Future<void> nestAsync<R>( | |
| Iterable<String> Function() label, | |
| FutureOr<Extracted<R>> Function(T p1) extract, | |
| AsyncCondition<R>? nestedCondition, | |
| ) async { | |
| // no-op | |
| } | |
| } | |
| abstract interface class _ClauseDescription { | |
| FailureDetail detail(_TestContext failingContext); | |
| } | |
| class _ExpectationClause implements _ClauseDescription { | |
| final Iterable<String> Function() _expected; | |
| _ExpectationClause(this._expected); | |
| @override | |
| FailureDetail detail(_TestContext failingContext) => | |
| FailureDetail(_expected(), -1, -1); | |
| } | |
| /// The result an expectation that failed for a subject.. | |
| final class CheckFailure { | |
| /// The specific rejected value within the overall subject that caused the | |
| /// failure. | |
| /// | |
| /// The [Rejection.actual] may be a property derived from the value at the | |
| /// root of the subject, for instance a field or an element in a collection. | |
| final Rejection rejection; | |
| /// The context within the overall subject where an expectation resulted in | |
| /// the [rejection]. | |
| late final FailureDetail detail = _readDetail(); | |
| final FailureDetail Function() _readDetail; | |
| CheckFailure(this.rejection, this._readDetail); | |
| } | |
| /// The context for a failed expectation. | |
| /// | |
| /// A subject may have some number of succeeding expectations, and the failure may | |
| /// be for an expectation against a property derived from the value at the root | |
| /// of the subject. For example, in `check([]).length.equals(1)` the | |
| /// specific value that gets rejected is `0` from the length of the list, and | |
| /// the subject that sees the rejection is nested with the label "has length". | |
| final class FailureDetail { | |
| /// A description of all the conditions the subject was expected to satisfy. | |
| /// | |
| /// Each subject has a label. At the root the label is typically "a | |
| /// <Type>" and nested subjects get a label based on the condition | |
| /// which extracted a property for further checks. Each level of nesting is | |
| /// described as "<label> that:" followed by an indented list of the | |
| /// expectations for that property. | |
| /// | |
| /// For example: | |
| /// | |
| /// a List that: | |
| /// has length that: | |
| /// equals <3> | |
| final Iterable<String> expected; | |
| /// A description of the conditions the checked value satisfied. | |
| /// | |
| /// Matches the format of [expected], except it will be cut off after the | |
| /// label for the subject that had a failing expectation. For example, if the | |
| /// equality check for the length of a list fails: | |
| /// | |
| /// a List that: | |
| /// has length that: | |
| /// | |
| /// If the subject with a failing expectation is the root, returns an empty | |
| /// list. Instead the "Actual: " value from the rejection can be used without | |
| /// indentation. | |
| Iterable<String> get actual => | |
| _actualOverlap > 0 ? expected.take(_actualOverlap + 1) : const []; | |
| /// The number of lines from [expected] which describe conditions that were | |
| /// successful. | |
| /// | |
| /// A failed expectation on a derived property may have some number of | |
| /// expectations that were checked and satisfied starting from the root | |
| /// subject. This field indicates how many lines of expectations were | |
| /// successful. | |
| final int _actualOverlap; | |
| /// The number of times the failing subject was nested from the root subject. | |
| /// | |
| /// Indicates how far the "Actual: " and "Which: " lines from the [Rejection] | |
| /// should be indented so that they are at the same level of indentation as | |
| /// the label for the subject where the expectation failed. | |
| /// | |
| /// For example, if a `List` is expected to and have a certain length | |
| /// [expected] may be: | |
| /// | |
| /// a List that: | |
| /// has length that: | |
| /// equals <3> | |
| /// | |
| /// If the actual value had an incorrect length, the [depth] will be `1` to | |
| /// indicate that the failure occurred checking one of the expectations | |
| /// against the `has length` label. | |
| final int depth; | |
| FailureDetail(this.expected, this._actualOverlap, this.depth); | |
| } | |
| /// A description of a value that failed an expectation. | |
| final class Rejection { | |
| /// A description of the actual value as it relates to the expectation. | |
| /// | |
| /// This may use [literal] to show a String representation of the value, or it | |
| /// may be a description of a specific aspect of the value. For instance an | |
| /// expectation that a Future completes to a value may describe the actual as | |
| /// "A Future that completes to an error". | |
| /// | |
| /// When a value is rejected with no [actual] argument, it will be filled in | |
| /// with the [literal] representation of the value. | |
| /// | |
| /// Lines should be split to separate elements, and individual strings should | |
| /// not contain newlines. | |
| /// | |
| /// This is printed following an "Actual: " label in the output of a failure | |
| /// message. All lines in the message will be indented to the level of the | |
| /// expectation in the description, and printed following the descriptions of | |
| /// any expectations that have already passed. | |
| final Iterable<String> actual; | |
| /// A description of the way that [actual] failed to meet the expectation. | |
| /// | |
| /// An expectation can provide extra detail, or focus attention on a specific | |
| /// part of the value. For instance when comparing multiple elements in a | |
| /// collection, the rejection may describe that the value "has an unequal | |
| /// value at index 3". | |
| /// | |
| /// Lines should be separate values in the iterable, if any element contains a | |
| /// newline it may cause problems with indentation in the output. | |
| /// | |
| /// When provided, this is printed following a "Which: " label at the end of | |
| /// the output for the failure message. | |
| final Iterable<String>? which; | |
| Rejection _fillActual(Object? value) => actual.isNotEmpty | |
| ? this | |
| : Rejection(actual: literal(value), which: which); | |
| Rejection({this.actual = const [], this.which}); | |
| } | |
| class AsyncConditionDisallowed implements Exception { | |
| final String flavor; | |
| AsyncConditionDisallowed._(this.flavor); | |
| @override | |
| String toString() => '$flavor expectations cannot be used on this subject'; | |
| } |