Skip to content

Commit 16ebb0b

Browse files
committed
Take callbacks for actual and which
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.
1 parent 8ab184b commit 16ebb0b

File tree

11 files changed

+343
-288
lines changed

11 files changed

+343
-288
lines changed

pkgs/checks/lib/src/checks.dart

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ Subject<T> check<T>(T value, {String? because}) => Subject._(_TestContext._root(
6565
// TODO - switch between "a" and "an"
6666
label: () => ['a $T'],
6767
fail: (f) {
68-
final which = f.rejection.which;
68+
final which = f.rejection.which?.call();
6969
throw TestFailure([
7070
...prefixFirst('Expected: ', f.detail.expected),
7171
...prefixFirst('Actual: ', f.detail.actual),
7272
...indent(
73-
prefixFirst('Actual: ', f.rejection.actual), f.detail.depth),
73+
prefixFirst('Actual: ', f.rejection.actual()), f.detail.depth),
7474
if (which != null && which.isNotEmpty)
7575
...indent(prefixFirst('Which: ', which), f.detail.depth),
7676
if (because != null) 'Reason: $because',
@@ -282,6 +282,8 @@ abstract class Context<T> {
282282
FutureOr<Extracted<R>> Function(T) extract);
283283
}
284284

285+
Iterable<String> _empty() => const [];
286+
285287
/// A property extracted from a value being checked, or a rejection.
286288
class Extracted<T> {
287289
final Rejection? rejection;
@@ -293,7 +295,8 @@ class Extracted<T> {
293295
/// When a nesting is rejected with an omitted or empty [actual] argument, it
294296
/// will be filled in with the [literal] representation of the value.
295297
Extracted.rejection(
296-
{Iterable<String> actual = const [], Iterable<String>? which})
298+
{Iterable<String> Function() actual = _empty,
299+
Iterable<String> Function()? which})
297300
: rejection = Rejection(actual: actual, which: which),
298301
value = null;
299302
Extracted.value(T this.value) : rejection = null;
@@ -306,10 +309,11 @@ class Extracted<T> {
306309
return Extracted.value(transform(value as T));
307310
}
308311

309-
Extracted<T> _fillActual(Object? actual) => rejection == null ||
310-
rejection!.actual.isNotEmpty
311-
? this
312-
: Extracted.rejection(actual: literal(actual), which: rejection!.which);
312+
Extracted<T> _fillActual(Object? actual) =>
313+
rejection == null || rejection!.actual != _empty
314+
? this
315+
: Extracted.rejection(
316+
actual: () => literal(actual), which: rejection!.which);
313317
}
314318

315319
abstract class _Optional<T> {
@@ -682,7 +686,7 @@ class Rejection {
682686
/// message. All lines in the message will be indented to the level of the
683687
/// expectation in the description, and printed following the descriptions of
684688
/// any expectations that have already passed.
685-
final Iterable<String> actual;
689+
final Iterable<String> Function() actual;
686690

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

701-
Rejection _fillActual(Object? value) => actual.isNotEmpty
705+
Rejection _fillActual(Object? value) => actual != _empty
702706
? this
703-
: Rejection(actual: literal(value), which: which);
707+
: Rejection(actual: () => literal(value), which: which);
704708

705-
Rejection({this.actual = const [], this.which});
709+
Rejection({this.actual = _empty, this.which});
706710
}
707711

708712
class ConditionSubject<T> implements Subject<T>, Condition<T> {

pkgs/checks/lib/src/collection_equality.dart

Lines changed: 56 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,20 @@ import 'package:checks/context.dart';
2626
/// Collections may be nested to a maximum depth of 1000. Recursive collections
2727
/// are not allowed.
2828
/// {@endtemplate}
29-
Iterable<String>? deepCollectionEquals(Object actual, Object expected) {
29+
Iterable<String> Function()? deepCollectionEquals(
30+
Object actual, Object expected) {
3031
try {
3132
return _deepCollectionEquals(actual, expected, 0);
3233
} on _ExceededDepthError {
33-
return ['exceeds the depth limit of $_maxDepth'];
34+
return () => ['exceeds the depth limit of $_maxDepth'];
3435
}
3536
}
3637

3738
const _maxDepth = 1000;
3839

3940
class _ExceededDepthError extends Error {}
4041

41-
Iterable<String>? _deepCollectionEquals(
42+
Iterable<String> Function()? _deepCollectionEquals(
4243
Object actual, Object expected, int depth) {
4344
assert(actual is Iterable || actual is Map);
4445
assert(expected is Iterable || expected is Map);
@@ -50,7 +51,7 @@ Iterable<String>? _deepCollectionEquals(
5051
final currentExpected = toCheck.expected;
5152
final path = toCheck.path;
5253
final currentDepth = toCheck.depth;
53-
Iterable<String>? rejectionWhich;
54+
Iterable<String> Function()? rejectionWhich;
5455
if (currentExpected is Set) {
5556
rejectionWhich = _findSetDifference(
5657
currentActual, currentExpected, path, currentDepth);
@@ -67,10 +68,10 @@ Iterable<String>? _deepCollectionEquals(
6768
return null;
6869
}
6970

70-
List<String>? _findIterableDifference(Object? actual,
71+
List<String> Function()? _findIterableDifference(Object? actual,
7172
Iterable<Object?> expected, _Path path, Queue<_Search> queue, int depth) {
7273
if (actual is! Iterable) {
73-
return ['${path}is not an Iterable'];
74+
return () => ['${path}is not an Iterable'];
7475
}
7576
var actualIterator = actual.iterator;
7677
var expectedIterator = expected.iterator;
@@ -79,16 +80,16 @@ List<String>? _findIterableDifference(Object? actual,
7980
var expectedNext = expectedIterator.moveNext();
8081
if (!expectedNext && !actualNext) break;
8182
if (!expectedNext) {
82-
return [
83-
'${path}has more elements than expected',
84-
'expected an iterable with $index element(s)'
85-
];
83+
return () => [
84+
'${path}has more elements than expected',
85+
'expected an iterable with $index element(s)'
86+
];
8687
}
8788
if (!actualNext) {
88-
return [
89-
'${path}has too few elements',
90-
'expected an iterable with at least ${index + 1} element(s)'
91-
];
89+
return () => [
90+
'${path}has too few elements',
91+
'expected an iterable with at least ${index + 1} element(s)'
92+
];
9293
}
9394
var actualValue = actualIterator.current;
9495
var expectedValue = expectedIterator.current;
@@ -99,22 +100,23 @@ List<String>? _findIterableDifference(Object? actual,
99100
} else if (expectedValue is Condition) {
100101
final failure = softCheck(actualValue, expectedValue);
101102
if (failure != null) {
102-
final which = failure.rejection.which;
103-
return [
104-
'has an element ${path.append(index)}that:',
105-
...indent(failure.detail.actual.skip(1)),
106-
...indent(prefixFirst('Actual: ', failure.rejection.actual),
107-
failure.detail.depth + 1),
108-
if (which != null)
109-
...indent(prefixFirst('which ', which), failure.detail.depth + 1)
110-
];
103+
final which = failure.rejection.which?.call();
104+
return () => [
105+
'has an element ${path.append(index)}that:',
106+
...indent(failure.detail.actual.skip(1)),
107+
...indent(prefixFirst('Actual: ', failure.rejection.actual()),
108+
failure.detail.depth + 1),
109+
if (which != null)
110+
...indent(
111+
prefixFirst('which ', which), failure.detail.depth + 1)
112+
];
111113
}
112114
} else {
113115
if (actualValue != expectedValue) {
114-
return [
115-
...prefixFirst('${path.append(index)}is ', literal(actualValue)),
116-
...prefixFirst('which does not equal ', literal(expectedValue))
117-
];
116+
return () => [
117+
...prefixFirst('${path.append(index)}is ', literal(actualValue)),
118+
...prefixFirst('which does not equal ', literal(expectedValue))
119+
];
118120
}
119121
}
120122
}
@@ -134,30 +136,30 @@ bool _elementMatches(Object? actual, Object? expected, int depth) {
134136
return expected == actual;
135137
}
136138

137-
Iterable<String>? _findSetDifference(
139+
Iterable<String> Function()? _findSetDifference(
138140
Object? actual, Set<Object?> expected, _Path path, int depth) {
139141
if (actual is! Set) {
140-
return ['${path}is not a Set'];
142+
return () => ['${path}is not a Set'];
141143
}
142144
return unorderedCompare(
143145
actual,
144146
expected,
145147
(actual, expected) => _elementMatches(actual, expected, depth),
146-
(expected, _, count) => [
147-
...prefixFirst('${path}has no element to match ', literal(expected)),
148-
if (count > 1) 'or ${count - 1} other elements',
149-
],
150-
(actual, _, count) => [
151-
...prefixFirst('${path}has an unexpected element ', literal(actual)),
152-
if (count > 1) 'and ${count - 1} other unexpected elements',
153-
],
148+
(expected, _, count) => () => [
149+
...prefixFirst('${path}has no element to match ', literal(expected)),
150+
if (count > 1) 'or ${count - 1} other elements',
151+
],
152+
(actual, _, count) => () => [
153+
...prefixFirst('${path}has an unexpected element ', literal(actual)),
154+
if (count > 1) 'and ${count - 1} other unexpected elements',
155+
],
154156
);
155157
}
156158

157-
Iterable<String>? _findMapDifference(
159+
Iterable<String> Function()? _findMapDifference(
158160
Object? actual, Map<Object?, Object?> expected, _Path path, int depth) {
159161
if (actual is! Map) {
160-
return ['${path}is not a Map'];
162+
return () => ['${path}is not a Map'];
161163
}
162164
Iterable<String> describeEntry(MapEntry<Object?, Object?> entry) {
163165
final key = literal(entry.key);
@@ -175,16 +177,16 @@ Iterable<String>? _findMapDifference(
175177
(actual, expected) =>
176178
_elementMatches(actual.key, expected.key, depth) &&
177179
_elementMatches(actual.value, expected.value, depth),
178-
(expectedEntry, _, count) => [
179-
...prefixFirst(
180-
'${path}has no entry to match ', describeEntry(expectedEntry)),
181-
if (count > 1) 'or ${count - 1} other entries',
182-
],
183-
(actualEntry, _, count) => [
184-
...prefixFirst(
185-
'${path}has unexpected entry ', describeEntry(actualEntry)),
186-
if (count > 1) 'and ${count - 1} other unexpected entries',
187-
],
180+
(expectedEntry, _, count) => () => [
181+
...prefixFirst(
182+
'${path}has no entry to match ', describeEntry(expectedEntry)),
183+
if (count > 1) 'or ${count - 1} other entries',
184+
],
185+
(actualEntry, _, count) => () => [
186+
...prefixFirst(
187+
'${path}has unexpected entry ', describeEntry(actualEntry)),
188+
if (count > 1) 'and ${count - 1} other unexpected entries',
189+
],
188190
);
189191
}
190192

@@ -241,12 +243,14 @@ class _Search {
241243
/// Runtime is at least `O(|actual||expected|)`, and for collections with many
242244
/// elements which compare as equal the runtime can reach
243245
/// `O((|actual| + |expected|)^2.5)`.
244-
Iterable<String>? unorderedCompare<T, E>(
246+
Iterable<String> Function()? unorderedCompare<T, E>(
245247
Iterable<T> actual,
246248
Iterable<E> expected,
247249
bool Function(T, E) elementsEqual,
248-
Iterable<String> Function(E, int index, int count) unmatchedExpected,
249-
Iterable<String> Function(T, int index, int count) unmatchedActual) {
250+
Iterable<String> Function() Function(E, int index, int count)
251+
unmatchedExpected,
252+
Iterable<String> Function() Function(T, int index, int count)
253+
unmatchedActual) {
250254
final indexedExpected = expected.toList();
251255
final indexedActual = actual.toList();
252256
final adjacency = <List<int>>[];

0 commit comments

Comments
 (0)