blob: 75107ba248720527034fd652821d840b7844bd9c [file] [log] [blame]
// 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], [expectAync], 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}
///
/// ```dart
/// extension CustomChecks on Subject<CustomType> {
/// Future<void> someAsyncExpectation() async {
/// await 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<R>(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}
///
/// ```dart
/// Future<void> someAsyncResult(
/// [AsyncCondition<Result> resultCondition]) async {
/// await 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<R>(Iterable<String> Function() clause,
FutureOr<Rejection?> Function(T) predicate) async {
if (!_allowAsync) {
throw StateError(
'Async expectations cannot be used on a synchronous subject');
}
_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 StateError('Late expectations cannot be used for soft checks');
}
_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) async {
if (!_allowAsync) {
throw StateError(
'Async expectations cannot be used on a synchronous subject');
}
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 {
expected.addAll(postfixLast(' that:', _label()));
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 + 1;
} 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<R>(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});
}