blob: e5896b9330557d72350c7fe70e260700a849cbf5 [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:test_api/hooks.dart';
import 'describe.dart';
/// A target for checking expectations against a value in a test.
///
/// A Check 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 check.
class Check<T> {
final Context<T> _context;
Check._(this._context);
/// Mark the currently running test as skipped and return a [Check] 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.
Check<T> skip(String message) {
TestHandle.current.markSkipped(message);
return Check._(_SkippedContext());
}
}
/// Creates a [Check] that can be used to validate expectations against [value].
///
/// 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);
/// ```
Check<T> checkThat<T>(T value, {String? because}) => Check._(_TestContext._(
value: _Present(value),
// TODO - switch between "a" and "an"
label: 'a $T',
reason: because,
fail: (m, _) => throw TestFailure(m),
allowAsync: true));
/// Checks whether [value] satisfies all expectations invoked in [condition].
///
/// Returns `null` if all expectations are satisfied, otherwise returns the
/// [Rejection] for the first expectation that fails.
///
/// Asynchronous expectations are not allowed in [condition] and will cause a
/// runtime error if they are used.
Rejection? softCheck<T>(T value, void Function(Check<T>) condition) {
Rejection? rejection;
final check = Check<T>._(_TestContext._(
value: _Present(value),
fail: (_, r) {
rejection = r;
},
allowAsync: false));
condition(check);
return rejection;
}
/// 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.
Iterable<String> describe<T>(void Function(Check<T>) condition) {
final context = _TestContext<T>._(
value: _Absent(),
fail: (_, __) {
throw UnimplementedError();
},
allowAsync: false);
condition(Check._(context));
return context.expected.skip(1);
}
extension ContextExtension<T> on Check<T> {
/// The expectations and nesting context for this check.
Context<T> get context => _context;
}
/// The expectation and nesting context already applied to a [Check].
///
/// 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);
/// 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 [Check] 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 passed value.
/// This may be useful to refine the type for further checks. In this case the
/// label is used like a single line "clause" passed to [expect], and
/// expectations applied to the return [Check] will behave as if they were
/// applied to the Check for this context.
Check<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 [Check] 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<Check<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;
Extracted.rejection({required String actual, Iterable<String>? which})
: this.rejection = Rejection(actual: actual, which: which),
this.value = null;
Extracted.value(this.value) : this.rejection = null;
Extracted._(this.rejection) : this.value = null;
Extracted<R> _map<R>(R Function(T) transform) {
if (rejection != null) return Extracted._(rejection);
return Extracted.value(transform(value as T));
}
}
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;
final _TestContext<dynamic>? _parent;
final List<_ClauseDescription> _clauses;
final List<_TestContext> _aliases;
// The "a value" in "a value that:".
final String _label;
final String? _reason;
final void Function(String, Rejection?) _fail;
final bool _allowAsync;
_TestContext._({
required _Optional<T> value,
required void Function(String, Rejection?) fail,
required bool allowAsync,
String? label,
String? reason,
}) : _value = value,
_label = label ?? '',
_reason = reason,
_fail = fail,
_allowAsync = allowAsync,
_parent = null,
_clauses = [],
_aliases = [];
_TestContext._alias(_TestContext original, this._value)
: _parent = original._parent,
_clauses = original._clauses,
_aliases = original._aliases,
_fail = original._fail,
_allowAsync = original._allowAsync,
// Properties that are never read from an aliased context
_label = '',
_reason = null;
_TestContext._child(this._value, this._label, _TestContext<dynamic> parent)
: _parent = parent,
_fail = parent._fail,
_allowAsync = parent._allowAsync,
_clauses = [],
_aliases = [],
// Properties that are never read from any context other than root
_reason = null;
@override
void expect(
Iterable<String> Function() clause, Rejection? Function(T) predicate) {
_clauses.add(_StringClause(clause));
final rejection = _value.apply(predicate);
if (rejection != null) {
_fail(_failure(rejection), 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 in a synchronous check');
}
_clauses.add(_StringClause(clause));
final outstandingWork = TestHandle.current.markPending();
final rejection = await _value.apply(predicate);
outstandingWork.complete();
if (rejection == null) return;
_fail(_failure(rejection), rejection);
}
String _failure(Rejection rejection) {
final root = _root;
return [
..._prefixFirst('Expected: ', root.expected),
..._prefixFirst('Actual: ', root.actual(rejection, this)),
if (_reason != null) 'Reason: $_reason',
].join('\n');
}
@override
Check<R> nest<R>(String label, Extracted<R> Function(T) extract,
{bool atSameLevel = false}) {
final result = _value.map(extract);
final rejection = result.rejection;
if (rejection != null) {
_clauses.add(_StringClause(() => [label]));
_fail(_failure(rejection), rejection);
}
final value = result.value ?? _Absent<R>();
final _TestContext<R> context;
if (atSameLevel) {
context = _TestContext._alias(this, value);
_aliases.add(context);
_clauses.add(_StringClause(() => [label]));
} else {
context = _TestContext._child(value, label, this);
_clauses.add(context);
}
return Check._(context);
}
@override
Future<Check<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T) extract) async {
if (!_allowAsync) {
throw StateError(
'Async expectations cannot be used in a synchronous check');
}
final outstandingWork = TestHandle.current.markPending();
final result = await _value.mapAsync(extract);
outstandingWork.complete();
final rejection = result.rejection;
if (rejection != null) {
_clauses.add(_StringClause(() => [label]));
_fail(_failure(rejection), rejection);
}
// TODO - does this need null fallback instead?
final value = result.value as _Optional<R>;
final context = _TestContext<R>._child(value, label, this);
_clauses.add(context);
return Check._(context);
}
_TestContext get _root {
_TestContext<dynamic> current = this;
while (current._parent != null) {
current = current._parent!;
}
return current;
}
@override
Iterable<String> get expected {
assert(_clauses.isNotEmpty);
return [
'$_label that:',
for (var clause in _clauses) ...indent(clause.expected),
];
}
@override
Iterable<String> actual(Rejection rejection, Context<dynamic> failedContext) {
if (identical(failedContext, this) || _aliases.contains(failedContext)) {
final which = rejection.which;
return [
if (_parent != null) '$_label that:',
'${_parent != null ? 'Actual: ' : ''}${rejection.actual}',
if (which != null && which.isNotEmpty) ..._prefixFirst('Which: ', which)
];
} else {
return [
'$_label that:',
for (var clause in _clauses)
...indent(clause.actual(rejection, failedContext))
];
}
}
}
class _SkippedContext<T> implements Context<T> {
@override
void expect(
Iterable<String> Function() clause, Rejection? Function(T) predicate) {
// Ignore
}
@override
Future<void> expectAsync<R>(Iterable<String> Function() clause,
FutureOr<Rejection?> Function(T) predicate) async {
// Ignore
}
@override
Check<R> nest<R>(String label, Extracted<R> Function(T p1) extract,
{bool atSameLevel = false}) {
return Check._(_SkippedContext());
}
@override
Future<Check<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T p1) extract) async {
return Check._(_SkippedContext());
}
}
abstract class _ClauseDescription {
Iterable<String> get expected;
Iterable<String> actual(Rejection rejection, Context<dynamic> context);
}
class _StringClause implements _ClauseDescription {
final Iterable<String> Function() _expected;
@override
Iterable<String> get expected => _expected();
_StringClause(this._expected);
// Assumes this will never get called when it was this clause that failed
// because the TestContext that fails never inspect it's clauses and just
// prints the failed one.
// TODO: better way to model this?
@override
Iterable<String> actual(Rejection rejection, Context<dynamic> context) =>
expected;
}
/// 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".
///
/// This is printed following an "Actual: " label in the output of a failure
/// message. 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 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({required this.actual, this.which});
}
Iterable<String> _prefixFirst(String prefix, Iterable<String> lines) sync* {
var isFirst = true;
for (var line in lines) {
if (isFirst) {
yield '$prefix$line';
isFirst = false;
} else {
yield line;
}
}
}