Skip to content

Commit

Permalink
Take callbacks for actual and which
Browse files Browse the repository at this point in the history
This aligns these arguments with all the other failure formatting
arguments like `clause` and `label`. There may be some performance
benefit in some cases where a rejection from `softCheck` is ignored and
expensive String operations are avoid, but in the typical case this just
introduces closures which will be invoked shortly.

Replace the default empty list for `actual` with a default function.
This is a slight behavior change where passing `() => []` will not get
overwritten with the default, but passing `[]` would have. Only
defaulting for when the argument was not passed at all is slightly
better behavior.

Replace a bunch of `Iterable<String>` with `Iterable<String> Function()`
and invoke them at the moment the strings are needed.
  • Loading branch information
natebosch committed Feb 3, 2023
1 parent 8ab184b commit 1079df0
Show file tree
Hide file tree
Showing 11 changed files with 305 additions and 255 deletions.
28 changes: 16 additions & 12 deletions pkgs/checks/lib/src/checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,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 @@ -282,6 +282,8 @@ abstract class Context<T> {
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 @@ -293,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 @@ -306,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 @@ -682,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 @@ -696,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
49 changes: 26 additions & 23 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,13 +80,13 @@ List<String>? _findIterableDifference(Object? actual,
var expectedNext = expectedIterator.moveNext();
if (!expectedNext && !actualNext) break;
if (!expectedNext) {
return [
return () => [
'${path}has more elements than expected',
'expected an iterable with $index element(s)'
];
}
if (!actualNext) {
return [
return () => [
'${path}has too few elements',
'expected an iterable with at least ${index + 1} element(s)'
];
Expand All @@ -99,19 +100,19 @@ List<String>? _findIterableDifference(Object? actual,
} else if (expectedValue is Condition) {
final failure = softCheck(actualValue, expectedValue);
if (failure != null) {
final which = failure.rejection.which;
return [
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),
...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 [
return () => [
...prefixFirst('${path.append(index)}is ', literal(actualValue)),
...prefixFirst('which does not equal ', literal(expectedValue))
];
Expand All @@ -134,30 +135,30 @@ 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,
expected,
(actual, expected) => _elementMatches(actual, expected, depth),
(expected, _, count) => [
(expected, _, count) => () => [
...prefixFirst('${path}has no element to match ', literal(expected)),
if (count > 1) 'or ${count - 1} other elements',
],
(actual, _, count) => [
(actual, _, count) => () => [
...prefixFirst('${path}has an unexpected element ', literal(actual)),
if (count > 1) 'and ${count - 1} other unexpected elements',
],
);
}

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 All @@ -175,12 +176,12 @@ Iterable<String>? _findMapDifference(
(actual, expected) =>
_elementMatches(actual.key, expected.key, depth) &&
_elementMatches(actual.value, expected.value, depth),
(expectedEntry, _, count) => [
(expectedEntry, _, count) => () => [
...prefixFirst(
'${path}has no entry to match ', describeEntry(expectedEntry)),
if (count > 1) 'or ${count - 1} other entries',
],
(actualEntry, _, count) => [
(actualEntry, _, count) => () => [
...prefixFirst(
'${path}has unexpected entry ', describeEntry(actualEntry)),
if (count > 1) 'and ${count - 1} other unexpected entries',
Expand Down Expand Up @@ -241,12 +242,14 @@ 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,
Iterable<String> Function(E, int index, int count) unmatchedExpected,
Iterable<String> Function(T, int index, int count) unmatchedActual) {
Iterable<String> Function() Function(E, int index, int count)
unmatchedExpected,
Iterable<String> Function() Function(T, int index, int count)
unmatchedActual) {
final indexedExpected = expected.toList();
final indexedActual = actual.toList();
final adjacency = <List<int>>[];
Expand Down
Loading

0 comments on commit 1079df0

Please sign in to comment.