Skip to content

Commit 1079df0

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 1079df0

File tree

11 files changed

+305
-255
lines changed

11 files changed

+305
-255
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: 26 additions & 23 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,13 +80,13 @@ List<String>? _findIterableDifference(Object? actual,
7980
var expectedNext = expectedIterator.moveNext();
8081
if (!expectedNext && !actualNext) break;
8182
if (!expectedNext) {
82-
return [
83+
return () => [
8384
'${path}has more elements than expected',
8485
'expected an iterable with $index element(s)'
8586
];
8687
}
8788
if (!actualNext) {
88-
return [
89+
return () => [
8990
'${path}has too few elements',
9091
'expected an iterable with at least ${index + 1} element(s)'
9192
];
@@ -99,19 +100,19 @@ 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 [
103+
final which = failure.rejection.which?.call();
104+
return () => [
104105
'has an element ${path.append(index)}that:',
105106
...indent(failure.detail.actual.skip(1)),
106-
...indent(prefixFirst('Actual: ', failure.rejection.actual),
107+
...indent(prefixFirst('Actual: ', failure.rejection.actual()),
107108
failure.detail.depth + 1),
108109
if (which != null)
109110
...indent(prefixFirst('which ', which), failure.detail.depth + 1)
110111
];
111112
}
112113
} else {
113114
if (actualValue != expectedValue) {
114-
return [
115+
return () => [
115116
...prefixFirst('${path.append(index)}is ', literal(actualValue)),
116117
...prefixFirst('which does not equal ', literal(expectedValue))
117118
];
@@ -134,30 +135,30 @@ bool _elementMatches(Object? actual, Object? expected, int depth) {
134135
return expected == actual;
135136
}
136137

137-
Iterable<String>? _findSetDifference(
138+
Iterable<String> Function()? _findSetDifference(
138139
Object? actual, Set<Object?> expected, _Path path, int depth) {
139140
if (actual is! Set) {
140-
return ['${path}is not a Set'];
141+
return () => ['${path}is not a Set'];
141142
}
142143
return unorderedCompare(
143144
actual,
144145
expected,
145146
(actual, expected) => _elementMatches(actual, expected, depth),
146-
(expected, _, count) => [
147+
(expected, _, count) => () => [
147148
...prefixFirst('${path}has no element to match ', literal(expected)),
148149
if (count > 1) 'or ${count - 1} other elements',
149150
],
150-
(actual, _, count) => [
151+
(actual, _, count) => () => [
151152
...prefixFirst('${path}has an unexpected element ', literal(actual)),
152153
if (count > 1) 'and ${count - 1} other unexpected elements',
153154
],
154155
);
155156
}
156157

157-
Iterable<String>? _findMapDifference(
158+
Iterable<String> Function()? _findMapDifference(
158159
Object? actual, Map<Object?, Object?> expected, _Path path, int depth) {
159160
if (actual is! Map) {
160-
return ['${path}is not a Map'];
161+
return () => ['${path}is not a Map'];
161162
}
162163
Iterable<String> describeEntry(MapEntry<Object?, Object?> entry) {
163164
final key = literal(entry.key);
@@ -175,12 +176,12 @@ Iterable<String>? _findMapDifference(
175176
(actual, expected) =>
176177
_elementMatches(actual.key, expected.key, depth) &&
177178
_elementMatches(actual.value, expected.value, depth),
178-
(expectedEntry, _, count) => [
179+
(expectedEntry, _, count) => () => [
179180
...prefixFirst(
180181
'${path}has no entry to match ', describeEntry(expectedEntry)),
181182
if (count > 1) 'or ${count - 1} other entries',
182183
],
183-
(actualEntry, _, count) => [
184+
(actualEntry, _, count) => () => [
184185
...prefixFirst(
185186
'${path}has unexpected entry ', describeEntry(actualEntry)),
186187
if (count > 1) 'and ${count - 1} other unexpected entries',
@@ -241,12 +242,14 @@ class _Search {
241242
/// Runtime is at least `O(|actual||expected|)`, and for collections with many
242243
/// elements which compare as equal the runtime can reach
243244
/// `O((|actual| + |expected|)^2.5)`.
244-
Iterable<String>? unorderedCompare<T, E>(
245+
Iterable<String> Function()? unorderedCompare<T, E>(
245246
Iterable<T> actual,
246247
Iterable<E> expected,
247248
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) {
249+
Iterable<String> Function() Function(E, int index, int count)
250+
unmatchedExpected,
251+
Iterable<String> Function() Function(T, int index, int count)
252+
unmatchedActual) {
250253
final indexedExpected = expected.toList();
251254
final indexedActual = actual.toList();
252255
final adjacency = <List<int>>[];

0 commit comments

Comments
 (0)