blob: 3a43fd177cdb465b4fc59908a435a2b3966e09cf [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.
// TODO Add doc about how failure strings work.
import 'dart:async';
import 'package:meta/meta.dart' as meta;
import 'package:test_api/hooks.dart';
import 'describe.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.
///
/// Expectations are defined as extension methods specialized on the generic
/// [T]. Expectations can use the [ContextExtension] to interact with the
/// [Context] for this subject.
class Subject<T> {
final Context<T> _context;
Subject._(this._context);
}
extension Skip<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
/// checkThat(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
/// checkThat(actual).equals(expected);
/// ```
@meta.useResult
Subject<T> checkThat<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.apply(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, Condition<T> condition) async {
CheckFailure? failure;
final subject = Subject<T>._(_TestContext._root(
value: _Present(value),
fail: (f) {
failure = f;
},
allowAsync: true,
allowUnawaited: false,
));
await condition.applyAsync(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.apply(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>(Condition<T> condition) async {
final context = _TestContext<T>._root(
value: _Absent(),
fail: (_) {
throw UnimplementedError();
},
allowAsync: true,
allowUnawaited: true,
);
await condition.applyAsync(Subject._(context));
return context.detail(context).expected.skip(1);
}
/// A set of expectations that are checked against the value when applied to a
/// [Subject].
abstract class Condition<T> {
void apply(Subject<T> subject);
Future<void> applyAsync(Subject<T> subject);
}
ConditionSubject<T> it<T>() => ConditionSubject._();
extension ContextExtension<T> on Subject<T> {
/// The expectations and nesting context for this subject.
Context<T> get context => _context;
}
/// The expectation and nesting context already applied to a [Subject].
///
/// This is the surface of interaction for expectation extension method
/// implementations.
///
/// The `expect` and `expectAsync` can test the value and optionally reject it.
/// The `nest` and `nestAsync` can test the value, and also extract some other
/// property from it for further checking.
abstract class Context<T> {
/// Expect that [predicate] will not return a [Rejection] for the checked
/// value.
///
/// The property that is asserted by this expectation is described by
/// [clause]. Often this is a single statement like "equals <1>" or "is
/// greater than 10", but it may be multiple lines such as describing that an
/// Iterable contains an element meeting a complex expectation. If any element
/// in the returned iterable contains a newline it may cause problems with
/// indentation in the output.
void expect(
Iterable<String> Function() clause, Rejection? Function(T) predicate);
/// Expect that [predicate] will not result in a [Rejection] for the checked
/// value.
///
/// The property that is asserted by this expectation is described by
/// [clause]. Often this is a single statement like "equals <1>" or "is
/// greater than 10", but it may be multiple lines such as describing that an
/// Iterable contains an element meeting a complex expectation. If any element
/// in the returned iterable contains a newline it may cause problems with
/// indentation in the output.
///
/// Some context may disallow asynchronous expectations, for instance in
/// [softCheck] which must synchronously check the value. In those contexts
/// this method will throw.
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".
///
/// May not be used from the context for a [Subject] created by [softCheck] or
/// [softCheckAsync]. The only useful effect of a late rejection is to throw a
/// [TestFailure] when used with a [checkThat] subject. Most conditions should
/// prefer to use [expect] or [expectAsync].
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].
///
/// The [label] will be used preceding "that:" in a description. Expectations
/// applied to the returned [Subject] will follow the label, indented by two
/// more spaces.
///
/// If [atSameLevel] is true then [R] should be a subtype of [T], and a
/// returned [Extracted.value] should be the same instance as the passed
/// value, or an object which is is equivalent but has a type which is more
/// convenient to test. In this case expectations applied to the return
/// [Subject] will behave as if they were applied to the subject for this
/// context. The [label] will be used as if it were a single line "clause"
/// passed to [expect]. If the label is empty, the clause will be omitted. The
/// label should only be left empty if the value extraction cannot fail.
Subject<R> nest<R>(String 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].
///
/// The [label] will be used preceding "that:" in a description. Expectations
/// applied to the returned [Subject] will follow the label, indented by two
/// more spaces.
///
/// Some context may disallow asynchronous expectations, for instance in
/// [softCheck] which must synchronously check the value. In those contexts
/// this method will throw.
Future<Subject<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T) extract);
}
/// A property extracted from a value being checked, or a rejection.
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 = this.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 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((v) => _Present(v));
}
@override
Extracted<_Present<R>> map<R>(Extracted<R> Function(T) transform) =>
transform(value)._map((v) => _Present(v));
}
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>());
}
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;
// The "a value" in "a value that:".
final String _label;
final void Function(CheckFailure) _fail;
final bool _allowAsync;
final bool _allowUnawaited;
_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 = '';
_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(_StringClause(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(_StringClause(clause));
final outstandingWork = TestHandle.current.markPending();
final rejection = await _value.apply(
(actual) async => (await predicate(actual))?._fillActual(actual));
outstandingWork.complete();
if (rejection == null) return;
_fail(_failure(rejection));
}
@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(_StringClause(clause));
_value.apply((actual) {
predicate(actual, (r) => _fail(_failure(r._fillActual(actual))));
});
}
@override
Subject<R> nest<R>(String 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(_StringClause(() => [label]));
_fail(_failure(rejection));
}
final value = result.value ?? _Absent<R>();
final _TestContext<R> context;
if (atSameLevel) {
context = _TestContext._alias(this, value);
_aliases.add(context);
if (label.isNotEmpty) _clauses.add(_StringClause(() => [label]));
} else {
context = _TestContext._child(value, label, this);
_clauses.add(context);
}
return Subject._(context);
}
@override
Future<Subject<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T) extract) async {
if (!_allowAsync) {
throw StateError(
'Async expectations cannot be used on a synchronous subject');
}
final outstandingWork = TestHandle.current.markPending();
final result = await _value.mapAsync(
(actual) async => (await extract(actual))._fillActual(actual));
outstandingWork.complete();
final rejection = result.rejection;
if (rejection != null) {
_clauses.add(_StringClause(() => [label]));
_fail(_failure(rejection));
}
final value = result.value ?? _Absent<R>();
final context = _TestContext<R>._child(value, label, this);
_clauses.add(context);
return Subject._(context);
}
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.add(_label);
} else {
expected.add('$_label that:');
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.
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>(String label, Extracted<R> Function(T p1) extract,
{bool atSameLevel = false}) {
return Subject._(_SkippedContext());
}
@override
Future<Subject<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T p1) extract) async {
return Subject._(_SkippedContext());
}
}
abstract class _ClauseDescription {
FailureDetail detail(_TestContext failingContext);
}
class _StringClause implements _ClauseDescription {
final Iterable<String> Function() _expected;
_StringClause(this._expected);
@override
FailureDetail detail(_TestContext failingContext) =>
FailureDetail(_expected(), -1, -1);
}
/// The result an expectation that failed for a subject..
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 `checkThat([]).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".
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.
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 ConditionSubject<T> implements Subject<T>, Condition<T> {
ConditionSubject._();
@override
void apply(Subject<T> subject) {
_context.apply(subject);
}
@override
Future<void> applyAsync(Subject<T> subject) async {
await _context.applyAsync(subject);
}
@override
final _ReplayContext<T> _context = _ReplayContext();
@override
String toString() {
return ['A value that:', ...describe(_context)].join('\n');
}
}
class _ReplayContext<T> implements Context<T>, Condition<T> {
final _interactions = <FutureOr<void> Function(Context<T>)>[];
@override
void apply(Subject<T> subject) {
for (var interaction in _interactions) {
interaction(subject.context);
}
}
@override
Future<void> applyAsync(Subject<T> subject) async {
for (var interaction in _interactions) {
await interaction(subject.context);
}
}
@override
void expect(
Iterable<String> Function() clause, Rejection? Function(T) predicate) {
_interactions.add((c) {
c.expect(clause, predicate);
});
}
@override
Future<void> expectAsync<R>(Iterable<String> Function() clause,
FutureOr<Rejection?> Function(T) predicate) async {
_interactions.add((c) async {
await c.expectAsync(clause, predicate);
});
}
@override
void expectUnawaited(Iterable<String> Function() clause,
void Function(T, void Function(Rejection)) predicate) {
_interactions.add((c) {
c.expectUnawaited(clause, predicate);
});
}
@override
Subject<R> nest<R>(String label, Extracted<R> Function(T p1) extract,
{bool atSameLevel = false}) {
final nestedContext = _ReplayContext<R>();
_interactions.add((c) {
var result = c.nest(label, extract, atSameLevel: atSameLevel);
nestedContext.apply(result);
});
return Subject._(nestedContext);
}
@override
Future<Subject<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T) extract) async {
final nestedContext = _ReplayContext<R>();
_interactions.add((c) async {
var result = await c.nestAsync(label, extract);
await nestedContext.applyAsync(result);
});
return Subject._(nestedContext);
}
}