Skip to content

Commit b95c060

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 b95c060

File tree

11 files changed

+301
-248
lines changed

11 files changed

+301
-248
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: 36 additions & 34 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,10 +136,10 @@ 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,
@@ -154,10 +156,10 @@ Iterable<String>? _findSetDifference(
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);
@@ -241,7 +243,7 @@ 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,
@@ -261,12 +263,12 @@ Iterable<String>? unorderedCompare<T, E>(
261263
final unpaired = _findUnpaired(adjacency, indexedActual.length);
262264
if (unpaired.first.isNotEmpty) {
263265
final firstUnmatched = indexedExpected[unpaired.first.first];
264-
return unmatchedExpected(
266+
return () => unmatchedExpected(
265267
firstUnmatched, unpaired.first.first, unpaired.first.length);
266268
}
267269
if (unpaired.last.isNotEmpty) {
268270
final firstUnmatched = indexedActual[unpaired.last.first];
269-
return unmatchedActual(
271+
return () => unmatchedActual(
270272
firstUnmatched, unpaired.last.first, unpaired.last.length);
271273
}
272274
return null;

0 commit comments

Comments
 (0)