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 all 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
28 changes: 16 additions & 12 deletions pkgs/checks/lib/src/checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ Subject<T> check<T>(T value, {String? because}) => Subject._(_TestContext._root(
// TODO - switch between "a" and "an"
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 @@ -560,6 +560,8 @@ abstract final class Context<T> {
Condition<R>? nestedCondition);
}

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

/// A property extracted from a value being checked, or a rejection.
final class Extracted<T> {
final Rejection? _rejection;
Expand All @@ -571,7 +573,8 @@ final 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 @@ -584,10 +587,11 @@ final 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 interface class _Optional<T> {
Expand Down Expand Up @@ -996,7 +1000,7 @@ final 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 @@ -1010,13 +1014,13 @@ final 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});
}

/// A [Subject] which records expectations and can replay them as a [Condition].
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 @@ -30,19 +30,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 @@ -54,7 +55,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 @@ -71,10 +72,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 @@ -83,16 +84,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 @@ -103,22 +104,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 @@ -138,10 +140,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 @@ -158,10 +160,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 @@ -245,7 +247,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 @@ -265,12 +267,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