Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take callbacks for actual and which #1901

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 42 additions & 34 deletions pkgs/checks/lib/src/checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ extension Skip<T> on Subject<T> {
Subject<T> check<T>(T value, {String? because}) => Subject._(_TestContext._root(
value: _Present(value),
// TODO - switch between "a" and "an"
label: 'a $T',
label: () => ['a $T'],
fail: (f) {
final which = f.rejection.which;
final which = f.rejection.which?.call();
throw TestFailure([
...prefixFirst('Expected: ', f.detail.expected),
...prefixFirst('Actual: ', f.detail.actual),
...indent(
prefixFirst('Actual: ', f.rejection.actual), f.detail.depth),
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',
Expand Down Expand Up @@ -261,7 +261,8 @@ abstract class Context<T> {
/// 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,
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.
Expand All @@ -277,10 +278,12 @@ abstract class Context<T> {
/// 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);
Future<Subject<R>> nestAsync<R>(Iterable<String> Function() label,
FutureOr<Extracted<R>> Function(T) extract);
}

Iterable<String> _empty() => const [];

/// A property extracted from a value being checked, or a rejection.
class Extracted<T> {
final Rejection? rejection;
Expand All @@ -292,7 +295,8 @@ class Extracted<T> {
/// 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})
{Iterable<String> Function() actual = _empty,
Iterable<String> Function()? which})
: rejection = Rejection(actual: actual, which: which),
value = null;
Extracted.value(T this.value) : rejection = null;
Expand All @@ -305,10 +309,11 @@ class Extracted<T> {
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);
Extracted<T> _fillActual(Object? actual) =>
rejection == null || rejection!.actual != _empty
? this
: Extracted.rejection(
actual: () => literal(actual), which: rejection!.which);
}

abstract class _Optional<T> {
Expand Down Expand Up @@ -363,7 +368,7 @@ class _TestContext<T> implements Context<T>, _ClauseDescription {
final List<_TestContext> _aliases;

// The "a value" in "a value that:".
final String _label;
final Iterable<String> Function() _label;

final void Function(CheckFailure) _fail;

Expand All @@ -375,9 +380,9 @@ class _TestContext<T> implements Context<T>, _ClauseDescription {
required void Function(CheckFailure) fail,
required bool allowAsync,
required bool allowUnawaited,
String? label,
Iterable<String> Function()? label,
}) : _value = value,
_label = label ?? '',
_label = label ?? (() => ['']),
_fail = fail,
_allowAsync = allowAsync,
_allowUnawaited = allowUnawaited,
Expand All @@ -394,7 +399,7 @@ class _TestContext<T> implements Context<T>, _ClauseDescription {
_allowUnawaited = original._allowUnawaited,
// Never read from an aliased context because they are never present in
// `_clauses`.
_label = '';
_label = (() => ['']);

_TestContext._child(this._value, this._label, _TestContext<dynamic> parent)
: _parent = parent,
Expand Down Expand Up @@ -444,20 +449,21 @@ class _TestContext<T> implements Context<T>, _ClauseDescription {
}

@override
Subject<R> nest<R>(String label, Extracted<R> Function(T) extract,
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(_StringClause(() => [label]));
_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]));
_clauses.add(_StringClause(label));
} else {
context = _TestContext._child(value, label, this);
_clauses.add(context);
Expand All @@ -466,8 +472,8 @@ class _TestContext<T> implements Context<T>, _ClauseDescription {
}

@override
Future<Subject<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T) extract) async {
Future<Subject<R>> nestAsync<R>(Iterable<String> Function() label,
FutureOr<Extracted<R>> Function(T) extract) async {
if (!_allowAsync) {
throw StateError(
'Async expectations cannot be used on a synchronous subject');
Expand All @@ -478,7 +484,7 @@ class _TestContext<T> implements Context<T>, _ClauseDescription {
outstandingWork.complete();
final rejection = result.rejection;
if (rejection != null) {
_clauses.add(_StringClause(() => [label]));
_clauses.add(_StringClause(label));
_fail(_failure(rejection));
}
final value = result.value ?? _Absent<R>();
Expand Down Expand Up @@ -507,9 +513,9 @@ class _TestContext<T> implements Context<T>, _ClauseDescription {
var successfulOverlap = 0;
final expected = <String>[];
if (_clauses.isEmpty) {
expected.add(_label);
expected.addAll(_label());
} else {
expected.add('$_label that:');
expected.addAll(postfixLast(' that:', _label()));
for (var clause in _clauses) {
final details = clause.detail(failingContext);
expected.addAll(indent(details.expected));
Expand Down Expand Up @@ -550,14 +556,15 @@ class _SkippedContext<T> implements Context<T> {
}

@override
Subject<R> nest<R>(String label, Extracted<R> Function(T p1) extract,
Subject<R> nest<R>(
Iterable<String> Function() 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 {
Future<Subject<R>> nestAsync<R>(Iterable<String> Function() label,
FutureOr<Extracted<R>> Function(T p1) extract) async {
return Subject._(_SkippedContext());
}
}
Expand Down Expand Up @@ -679,7 +686,7 @@ class Rejection {
/// 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;
final Iterable<String> Function() actual;

/// A description of the way that [actual] failed to meet the expectation.
///
Expand All @@ -693,13 +700,13 @@ class Rejection {
///
/// When provided, this is printed following a "Which: " label at the end of
/// the output for the failure message.
final Iterable<String>? which;
final Iterable<String> Function()? which;

Rejection _fillActual(Object? value) => actual.isNotEmpty
Rejection _fillActual(Object? value) => actual != _empty
? this
: Rejection(actual: literal(value), which: which);
: Rejection(actual: () => literal(value), which: which);

Rejection({this.actual = const [], this.which});
Rejection({this.actual = _empty, this.which});
}

class ConditionSubject<T> implements Subject<T>, Condition<T> {
Expand Down Expand Up @@ -766,7 +773,8 @@ class _ReplayContext<T> implements Context<T>, Condition<T> {
}

@override
Subject<R> nest<R>(String label, Extracted<R> Function(T p1) extract,
Subject<R> nest<R>(
Iterable<String> Function() label, Extracted<R> Function(T p1) extract,
{bool atSameLevel = false}) {
final nestedContext = _ReplayContext<R>();
_interactions.add((c) {
Expand All @@ -777,8 +785,8 @@ class _ReplayContext<T> implements Context<T>, Condition<T> {
}

@override
Future<Subject<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T) extract) async {
Future<Subject<R>> nestAsync<R>(Iterable<String> Function() label,
FutureOr<Extracted<R>> Function(T) extract) async {
final nestedContext = _ReplayContext<R>();
_interactions.add((c) async {
var result = await c.nestAsync(label, extract);
Expand Down
70 changes: 36 additions & 34 deletions pkgs/checks/lib/src/collection_equality.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,20 @@ import 'package:checks/context.dart';
/// Collections may be nested to a maximum depth of 1000. Recursive collections
/// are not allowed.
/// {@endtemplate}
Iterable<String>? deepCollectionEquals(Object actual, Object expected) {
Iterable<String> Function()? deepCollectionEquals(
Object actual, Object expected) {
try {
return _deepCollectionEquals(actual, expected, 0);
} on _ExceededDepthError {
return ['exceeds the depth limit of $_maxDepth'];
return () => ['exceeds the depth limit of $_maxDepth'];
}
}

const _maxDepth = 1000;

class _ExceededDepthError extends Error {}

Iterable<String>? _deepCollectionEquals(
Iterable<String> Function()? _deepCollectionEquals(
Object actual, Object expected, int depth) {
assert(actual is Iterable || actual is Map);
assert(expected is Iterable || expected is Map);
Expand All @@ -50,7 +51,7 @@ Iterable<String>? _deepCollectionEquals(
final currentExpected = toCheck.expected;
final path = toCheck.path;
final currentDepth = toCheck.depth;
Iterable<String>? rejectionWhich;
Iterable<String> Function()? rejectionWhich;
if (currentExpected is Set) {
rejectionWhich = _findSetDifference(
currentActual, currentExpected, path, currentDepth);
Expand All @@ -67,10 +68,10 @@ Iterable<String>? _deepCollectionEquals(
return null;
}

List<String>? _findIterableDifference(Object? actual,
List<String> Function()? _findIterableDifference(Object? actual,
Iterable<Object?> expected, _Path path, Queue<_Search> queue, int depth) {
if (actual is! Iterable) {
return ['${path}is not an Iterable'];
return () => ['${path}is not an Iterable'];
}
var actualIterator = actual.iterator;
var expectedIterator = expected.iterator;
Expand All @@ -79,16 +80,16 @@ List<String>? _findIterableDifference(Object? actual,
var expectedNext = expectedIterator.moveNext();
if (!expectedNext && !actualNext) break;
if (!expectedNext) {
return [
'${path}has more elements than expected',
'expected an iterable with $index element(s)'
];
return () => [
'${path}has more elements than expected',
'expected an iterable with $index element(s)'
];
}
if (!actualNext) {
return [
'${path}has too few elements',
'expected an iterable with at least ${index + 1} element(s)'
];
return () => [
'${path}has too few elements',
'expected an iterable with at least ${index + 1} element(s)'
];
}
var actualValue = actualIterator.current;
var expectedValue = expectedIterator.current;
Expand All @@ -99,22 +100,23 @@ List<String>? _findIterableDifference(Object? actual,
} else if (expectedValue is Condition) {
final failure = softCheck(actualValue, expectedValue);
if (failure != null) {
final which = failure.rejection.which;
return [
'has an element ${path.append(index)}that:',
...indent(failure.detail.actual.skip(1)),
...indent(prefixFirst('Actual: ', failure.rejection.actual),
failure.detail.depth + 1),
if (which != null)
...indent(prefixFirst('which ', which), failure.detail.depth + 1)
];
final which = failure.rejection.which?.call();
return () => [
'has an element ${path.append(index)}that:',
...indent(failure.detail.actual.skip(1)),
...indent(prefixFirst('Actual: ', failure.rejection.actual()),
failure.detail.depth + 1),
if (which != null)
...indent(
prefixFirst('which ', which), failure.detail.depth + 1)
];
}
} else {
if (actualValue != expectedValue) {
return [
...prefixFirst('${path.append(index)}is ', literal(actualValue)),
...prefixFirst('which does not equal ', literal(expectedValue))
];
return () => [
...prefixFirst('${path.append(index)}is ', literal(actualValue)),
...prefixFirst('which does not equal ', literal(expectedValue))
];
}
}
}
Expand All @@ -134,10 +136,10 @@ bool _elementMatches(Object? actual, Object? expected, int depth) {
return expected == actual;
}

Iterable<String>? _findSetDifference(
Iterable<String> Function()? _findSetDifference(
Object? actual, Set<Object?> expected, _Path path, int depth) {
if (actual is! Set) {
return ['${path}is not a Set'];
return () => ['${path}is not a Set'];
}
return unorderedCompare(
actual,
Expand All @@ -154,10 +156,10 @@ Iterable<String>? _findSetDifference(
);
}

Iterable<String>? _findMapDifference(
Iterable<String> Function()? _findMapDifference(
Object? actual, Map<Object?, Object?> expected, _Path path, int depth) {
if (actual is! Map) {
return ['${path}is not a Map'];
return () => ['${path}is not a Map'];
}
Iterable<String> describeEntry(MapEntry<Object?, Object?> entry) {
final key = literal(entry.key);
Expand Down Expand Up @@ -241,7 +243,7 @@ class _Search {
/// Runtime is at least `O(|actual||expected|)`, and for collections with many
/// elements which compare as equal the runtime can reach
/// `O((|actual| + |expected|)^2.5)`.
Iterable<String>? unorderedCompare<T, E>(
Iterable<String> Function()? unorderedCompare<T, E>(
Iterable<T> actual,
Iterable<E> expected,
bool Function(T, E) elementsEqual,
Expand All @@ -261,12 +263,12 @@ Iterable<String>? unorderedCompare<T, E>(
final unpaired = _findUnpaired(adjacency, indexedActual.length);
if (unpaired.first.isNotEmpty) {
final firstUnmatched = indexedExpected[unpaired.first.first];
return unmatchedExpected(
return () => unmatchedExpected(
firstUnmatched, unpaired.first.first, unpaired.first.length);
}
if (unpaired.last.isNotEmpty) {
final firstUnmatched = indexedActual[unpaired.last.first];
return unmatchedActual(
return () => unmatchedActual(
firstUnmatched, unpaired.last.first, unpaired.last.length);
}
return null;
Expand Down
Loading