Skip to content
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
6 changes: 5 additions & 1 deletion pkgs/checks/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## 0.3.2-wip
## 0.4.0-wip

- **Breaking changes in `package:checks/context.dart`** Require callbacks for
the `actual` and `which` arguments to `Rejection`. Allows skipping the
formatting of these strings in some cases where the expectation is used in a
soft check.
- Require Dart 3.7
- Improve speed of pretty printing for large collections.
- Improve formatting for failures involving unexpected exceptions.
Expand Down
27 changes: 16 additions & 11 deletions pkgs/checks/lib/src/checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ Subject<T> check<T>(T value, {String? because}) => Subject._(
// 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),
prefixFirst('Actual: ', f.rejection.actual()),
f.detail.depth,
),
if (which != null && which.isNotEmpty)
Expand Down Expand Up @@ -578,6 +578,8 @@ abstract final class Context<T> {
);
}

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

/// A property extracted from a value being checked, or a rejection.
final class Extracted<T> {
final Rejection? _rejection;
Expand All @@ -589,8 +591,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 @@ -604,9 +606,12 @@ final class Extracted<T> {
}

Extracted<T> _fillActual(Object? actual) =>
_rejection == null || _rejection.actual.isNotEmpty
_rejection == null || _rejection.actual != _empty
? this
: Extracted.rejection(actual: literal(actual), which: _rejection.which);
: Extracted.rejection(
actual: () => literal(actual),
which: _rejection.which,
);
}

abstract interface class _Optional<T> {
Expand Down Expand Up @@ -1045,7 +1050,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 @@ -1059,13 +1064,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});
}

class AsyncConditionDisallowed implements Exception {
Expand Down
51 changes: 27 additions & 24 deletions pkgs/checks/lib/src/collection_equality.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,22 @@ import '../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,
Expand All @@ -74,7 +77,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,
Expand Down Expand Up @@ -105,15 +108,15 @@ Iterable<String>? _deepCollectionEquals(
return null;
}

List<String>? _findIterableDifference(
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 @@ -122,13 +125,13 @@ List<String>? _findIterableDifference(
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 @@ -146,7 +149,7 @@ List<String>? _findIterableDifference(
return null;
}

List<String>? _compareValue(
List<String> Function()? _compareValue(
Object? actualValue,
Object? expectedValue,
_Path path,
Expand All @@ -162,12 +165,12 @@ List<String>? _compareValue(
} 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(pathAppend)}that:',
...indent(failure.detail.actual.skip(1)),
...indent(
prefixFirst('Actual: ', failure.rejection.actual),
prefixFirst('Actual: ', failure.rejection.actual()),
failure.detail.depth + 1,
),
if (which != null)
Expand All @@ -176,7 +179,7 @@ List<String>? _compareValue(
}
} else {
if (actualValue != expectedValue) {
return [
return () => [
...prefixFirst('${path.append(pathAppend)}is ', literal(actualValue)),
...prefixFirst('which does not equal ', literal(expectedValue)),
];
Expand All @@ -198,14 +201,14 @@ 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 @@ -222,15 +225,15 @@ Iterable<String>? _findSetDifference(
);
}

Iterable<String>? _findMapDifference(
Iterable<String> Function()? _findMapDifference(
Object? actual,
Map<Object?, Object?> expected,
_Path path,
Queue<_Search> queue,
int depth,
) {
if (actual is! Map) {
return ['${path}is not a Map'];
return () => ['${path}is not a Map'];
}
if (expected.keys.any(
(key) => key is Condition || key is Iterable || key is Map,
Expand All @@ -254,7 +257,7 @@ Iterable<String> _describeEntry(MapEntry<Object?, Object?> entry) {
/// Returns a description of a difference found between [actual] and [expected]
/// when [expected] has only direct key values and there is a 1:1 mapping
/// between an expected value and a checked value in the map.
Iterable<String>? _findUnambiguousMapDifference(
Iterable<String> Function()? _findUnambiguousMapDifference(
Map<Object?, Object?> actual,
Map<Object?, Object?> expected,
_Path path,
Expand All @@ -266,7 +269,7 @@ Iterable<String>? _findUnambiguousMapDifference(
assert(entry.key is! Iterable);
assert(entry.key is! Map);
if (!actual.containsKey(entry.key)) {
return prefixFirst(
return () => prefixFirst(
'${path}has no key matching expected entry ',
_describeEntry(entry),
);
Expand All @@ -283,7 +286,7 @@ Iterable<String>? _findUnambiguousMapDifference(
}
for (final entry in actual.entries) {
if (!expected.containsKey(entry.key)) {
return prefixFirst(
return () => prefixFirst(
'${path}has an unexpected key for entry ',
_describeEntry(entry),
);
Expand All @@ -292,7 +295,7 @@ Iterable<String>? _findUnambiguousMapDifference(
return null;
}

Iterable<String>? _findAmbiguousMapDifference(
Iterable<String> Function()? _findAmbiguousMapDifference(
Map<Object?, Object?> actual,
Map<Object?, Object?> expected,
_Path path,
Expand Down Expand Up @@ -367,7 +370,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 @@ -388,15 +391,15 @@ 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,
Expand Down
Loading
Loading