diff --git a/pkgs/checks/lib/src/checks.dart b/pkgs/checks/lib/src/checks.dart index e3f195eda..bc3a0f44b 100644 --- a/pkgs/checks/lib/src/checks.dart +++ b/pkgs/checks/lib/src/checks.dart @@ -65,12 +65,12 @@ Subject check(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', @@ -282,6 +282,8 @@ abstract class Context { FutureOr> Function(T) extract); } +Iterable _empty() => const []; + /// A property extracted from a value being checked, or a rejection. class Extracted { final Rejection? rejection; @@ -293,7 +295,8 @@ class Extracted { /// 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 actual = const [], Iterable? which}) + {Iterable Function() actual = _empty, + Iterable Function()? which}) : rejection = Rejection(actual: actual, which: which), value = null; Extracted.value(T this.value) : rejection = null; @@ -306,10 +309,11 @@ class Extracted { return Extracted.value(transform(value as T)); } - Extracted _fillActual(Object? actual) => rejection == null || - rejection!.actual.isNotEmpty - ? this - : Extracted.rejection(actual: literal(actual), which: rejection!.which); + Extracted _fillActual(Object? actual) => + rejection == null || rejection!.actual != _empty + ? this + : Extracted.rejection( + actual: () => literal(actual), which: rejection!.which); } abstract class _Optional { @@ -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 actual; + final Iterable Function() actual; /// A description of the way that [actual] failed to meet the expectation. /// @@ -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? which; + final Iterable 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 implements Subject, Condition { diff --git a/pkgs/checks/lib/src/collection_equality.dart b/pkgs/checks/lib/src/collection_equality.dart index bd1194727..433926064 100644 --- a/pkgs/checks/lib/src/collection_equality.dart +++ b/pkgs/checks/lib/src/collection_equality.dart @@ -26,11 +26,12 @@ import 'package:checks/context.dart'; /// Collections may be nested to a maximum depth of 1000. Recursive collections /// are not allowed. /// {@endtemplate} -Iterable? deepCollectionEquals(Object actual, Object expected) { +Iterable 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']; } } @@ -38,7 +39,7 @@ const _maxDepth = 1000; class _ExceededDepthError extends Error {} -Iterable? _deepCollectionEquals( +Iterable Function()? _deepCollectionEquals( Object actual, Object expected, int depth) { assert(actual is Iterable || actual is Map); assert(expected is Iterable || expected is Map); @@ -50,7 +51,7 @@ Iterable? _deepCollectionEquals( final currentExpected = toCheck.expected; final path = toCheck.path; final currentDepth = toCheck.depth; - Iterable? rejectionWhich; + Iterable Function()? rejectionWhich; if (currentExpected is Set) { rejectionWhich = _findSetDifference( currentActual, currentExpected, path, currentDepth); @@ -67,10 +68,10 @@ Iterable? _deepCollectionEquals( return null; } -List? _findIterableDifference(Object? actual, +List Function()? _findIterableDifference(Object? actual, Iterable 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; @@ -79,16 +80,16 @@ List? _findIterableDifference(Object? actual, var expectedNext = expectedIterator.moveNext(); if (!expectedNext && !actualNext) break; if (!expectedNext) { - return [ - '${path}has more elements than expected', - 'expected an iterable with $index element(s)' - ]; + return () => [ + '${path}has more elements than expected', + 'expected an iterable with $index element(s)' + ]; } if (!actualNext) { - return [ - '${path}has too few elements', - 'expected an iterable with at least ${index + 1} element(s)' - ]; + return () => [ + '${path}has too few elements', + 'expected an iterable with at least ${index + 1} element(s)' + ]; } var actualValue = actualIterator.current; var expectedValue = expectedIterator.current; @@ -99,22 +100,23 @@ List? _findIterableDifference(Object? actual, } else if (expectedValue is Condition) { final failure = softCheck(actualValue, expectedValue); if (failure != null) { - final which = failure.rejection.which; - return [ - 'has an element ${path.append(index)}that:', - ...indent(failure.detail.actual.skip(1)), - ...indent(prefixFirst('Actual: ', failure.rejection.actual), - failure.detail.depth + 1), - if (which != null) - ...indent(prefixFirst('which ', which), failure.detail.depth + 1) - ]; + 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()), + failure.detail.depth + 1), + if (which != null) + ...indent( + prefixFirst('which ', which), failure.detail.depth + 1) + ]; } } else { if (actualValue != expectedValue) { - return [ - ...prefixFirst('${path.append(index)}is ', literal(actualValue)), - ...prefixFirst('which does not equal ', literal(expectedValue)) - ]; + return () => [ + ...prefixFirst('${path.append(index)}is ', literal(actualValue)), + ...prefixFirst('which does not equal ', literal(expectedValue)) + ]; } } } @@ -134,30 +136,30 @@ bool _elementMatches(Object? actual, Object? expected, int depth) { return expected == actual; } -Iterable? _findSetDifference( +Iterable Function()? _findSetDifference( Object? actual, Set 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) => [ - ...prefixFirst('${path}has no element to match ', literal(expected)), - if (count > 1) 'or ${count - 1} other elements', - ], - (actual, _, count) => [ - ...prefixFirst('${path}has an unexpected element ', literal(actual)), - if (count > 1) 'and ${count - 1} other unexpected elements', - ], + (expected, _, count) => () => [ + ...prefixFirst('${path}has no element to match ', literal(expected)), + if (count > 1) 'or ${count - 1} other elements', + ], + (actual, _, count) => () => [ + ...prefixFirst('${path}has an unexpected element ', literal(actual)), + if (count > 1) 'and ${count - 1} other unexpected elements', + ], ); } -Iterable? _findMapDifference( +Iterable Function()? _findMapDifference( Object? actual, Map expected, _Path path, int depth) { if (actual is! Map) { - return ['${path}is not a Map']; + return () => ['${path}is not a Map']; } Iterable describeEntry(MapEntry entry) { final key = literal(entry.key); @@ -175,16 +177,16 @@ Iterable? _findMapDifference( (actual, expected) => _elementMatches(actual.key, expected.key, depth) && _elementMatches(actual.value, expected.value, depth), - (expectedEntry, _, count) => [ - ...prefixFirst( - '${path}has no entry to match ', describeEntry(expectedEntry)), - if (count > 1) 'or ${count - 1} other entries', - ], - (actualEntry, _, count) => [ - ...prefixFirst( - '${path}has unexpected entry ', describeEntry(actualEntry)), - if (count > 1) 'and ${count - 1} other unexpected entries', - ], + (expectedEntry, _, count) => () => [ + ...prefixFirst( + '${path}has no entry to match ', describeEntry(expectedEntry)), + if (count > 1) 'or ${count - 1} other entries', + ], + (actualEntry, _, count) => () => [ + ...prefixFirst( + '${path}has unexpected entry ', describeEntry(actualEntry)), + if (count > 1) 'and ${count - 1} other unexpected entries', + ], ); } @@ -241,12 +243,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? unorderedCompare( +Iterable Function()? unorderedCompare( Iterable actual, Iterable expected, bool Function(T, E) elementsEqual, - Iterable Function(E, int index, int count) unmatchedExpected, - Iterable Function(T, int index, int count) unmatchedActual) { + Iterable Function() Function(E, int index, int count) + unmatchedExpected, + Iterable Function() Function(T, int index, int count) + unmatchedActual) { final indexedExpected = expected.toList(); final indexedActual = actual.toList(); final adjacency = >[]; diff --git a/pkgs/checks/lib/src/extensions/async.dart b/pkgs/checks/lib/src/extensions/async.dart index bde4ba0fb..66c6f166b 100644 --- a/pkgs/checks/lib/src/extensions/async.dart +++ b/pkgs/checks/lib/src/extensions/async.dart @@ -20,12 +20,12 @@ extension FutureChecks on Subject> { try { return Extracted.value(await actual); } catch (e, st) { - return Extracted.rejection(actual: [ - 'a future that completes as an error' - ], which: [ - ...prefixFirst('threw ', postfixLast(' at:', literal(e))), - ...(const LineSplitter()).convert(st.toString()) - ]); + return Extracted.rejection( + actual: () => ['a future that completes as an error'], + which: () => [ + ...prefixFirst('threw ', postfixLast(' at:', literal(e))), + ...(const LineSplitter()).convert(st.toString()) + ]); } }); @@ -42,14 +42,15 @@ extension FutureChecks on Subject> { context.expectUnawaited(() => ['does not complete'], (actual, reject) { unawaited(actual.then((r) { reject(Rejection( - actual: prefixFirst('a future that completed to ', literal(r)))); + actual: () => + prefixFirst('a future that completed to ', literal(r)))); }, onError: (e, st) { - reject(Rejection(actual: [ - 'a future that completed as an error:' - ], which: [ - ...prefixFirst('threw ', literal(e)), - ...(const LineSplitter()).convert(st.toString()) - ])); + reject(Rejection( + actual: () => ['a future that completed as an error:'], + which: () => [ + ...prefixFirst('threw ', literal(e)), + ...(const LineSplitter()).convert(st.toString()) + ])); })); }); } @@ -64,18 +65,19 @@ extension FutureChecks on Subject> { () => ['completes to an error${E == Object ? '' : ' of type $E'}'], (actual) async { try { + final result = await actual; return Extracted.rejection( - actual: prefixFirst('completed to ', literal(await actual)), - which: ['did not throw']); + actual: () => prefixFirst('completed to ', literal(result)), + which: () => ['did not throw']); } on E catch (e) { return Extracted.value(e); } catch (e, st) { return Extracted.rejection( - actual: prefixFirst('completed to error ', literal(e)), - which: [ - 'threw an exception that is not a $E at:', - ...(const LineSplitter()).convert(st.toString()) - ]); + actual: () => prefixFirst('completed to error ', literal(e)), + which: () => [ + 'threw an exception that is not a $E at:', + ...(const LineSplitter()).convert(st.toString()) + ]); } }); } @@ -113,19 +115,19 @@ extension StreamChecks on Subject> { context.nestAsync(() => ['emits a value'], (actual) async { if (!await actual.hasNext) { return Extracted.rejection( - actual: ['a stream'], - which: ['closed without emitting enough values']); + actual: () => ['a stream'], + which: () => ['closed without emitting enough values']); } try { await actual.peek; return Extracted.value(await actual.next); } catch (e, st) { return Extracted.rejection( - actual: prefixFirst('a stream with error ', literal(e)), - which: [ - 'emitted an error instead of a value at:', - ...(const LineSplitter()).convert(st.toString()) - ]); + actual: () => prefixFirst('a stream with error ', literal(e)), + which: () => [ + 'emitted an error instead of a value at:', + ...(const LineSplitter()).convert(st.toString()) + ]); } }); @@ -145,24 +147,25 @@ extension StreamChecks on Subject> { (actual) async { if (!await actual.hasNext) { return Extracted.rejection( - actual: ['a stream'], - which: ['closed without emitting an expected error']); + actual: () => ['a stream'], + which: () => ['closed without emitting an expected error']); } try { final value = await actual.peek; return Extracted.rejection( - actual: prefixFirst('a stream emitting value ', literal(value)), - which: ['closed without emitting an error']); + actual: () => + prefixFirst('a stream emitting value ', literal(value)), + which: () => ['closed without emitting an error']); } on E catch (e) { await actual.next.then((_) {}, onError: (_) {}); return Extracted.value(e); } catch (e, st) { return Extracted.rejection( - actual: prefixFirst('a stream with error ', literal(e)), - which: [ - 'emitted an error which is not $E at:', - ...(const LineSplitter()).convert(st.toString()) - ]); + actual: () => prefixFirst('a stream with error ', literal(e)), + which: () => [ + 'emitted an error which is not $E at:', + ...(const LineSplitter()).convert(st.toString()) + ]); } }); @@ -193,8 +196,9 @@ extension StreamChecks on Subject> { count++; } return Rejection( - actual: ['a stream'], - which: ['ended after emitting $count elements with none matching']); + actual: () => ['a stream'], + which: () => + ['ended after emitting $count elements with none matching']); }); } @@ -227,24 +231,27 @@ extension StreamChecks on Subject> { descriptions.addAll(await describeAsync(condition)); final failure = await softCheckAsync(actual, condition); if (failure != null) { - final which = failure.rejection.which; - return Rejection(actual: [ - 'a stream' - ], which: [ - if (satisfiedCount > 0) 'satisfied $satisfiedCount conditions then', - 'failed to satisfy the condition at index $satisfiedCount', - if (failure.detail.depth > 0) ...[ - 'because it:', - ...indent( - failure.detail.actual.skip(1), failure.detail.depth - 1), - ...indent(prefixFirst('Actual: ', failure.rejection.actual), - failure.detail.depth), - if (which != null) - ...indent(prefixFirst('Which: ', which), failure.detail.depth), - ] else ...[ - if (which != null) ...prefixFirst('because it ', which), - ], - ]); + final which = failure.rejection.which?.call(); + return Rejection( + actual: () => ['a stream'], + which: () => [ + if (satisfiedCount > 0) + 'satisfied $satisfiedCount conditions then', + 'failed to satisfy the condition at index $satisfiedCount', + if (failure.detail.depth > 0) ...[ + 'because it:', + ...indent(failure.detail.actual.skip(1), + failure.detail.depth - 1), + ...indent( + prefixFirst('Actual: ', failure.rejection.actual()), + failure.detail.depth), + if (which != null) + ...indent(prefixFirst('Which: ', which), + failure.detail.depth), + ] else ...[ + if (which != null) ...prefixFirst('because it ', which), + ], + ]); } satisfiedCount++; } @@ -296,11 +303,11 @@ extension StreamChecks on Subject> { } transaction.reject(); Iterable failureDetails(int index, CheckFailure? failure) { - final actual = failure!.rejection.actual; - final which = failure.rejection.which; - final detail = failure.detail; + final detail = failure!.detail; final failed = 'failed the condition at index $index'; + final which = failure.rejection.which?.call(); if (detail.depth > 0) { + final actual = failure.rejection.actual(); return [ '$failed because it:', ...indent(detail.actual.skip(1), detail.depth - 1), @@ -320,13 +327,13 @@ extension StreamChecks on Subject> { } } - return Rejection(actual: [ - 'a stream' - ], which: [ - 'failed to satisfy any condition', - for (var i = 0; i < failures.length; i++) - ...failureDetails(i, failures[i]), - ]); + return Rejection( + actual: () => ['a stream'], + which: () => [ + 'failed to satisfy any condition', + for (var i = 0; i < failures.length; i++) + ...failureDetails(i, failures[i]), + ]); }); } @@ -348,12 +355,12 @@ extension StreamChecks on Subject> { var count = 0; await for (var emitted in actual.rest) { if (softCheck(emitted, condition) == null) { - return Rejection(actual: [ - 'a stream' - ], which: [ - ...prefixFirst('emitted ', literal(emitted)), - if (count > 0) 'following $count other items' - ]); + return Rejection( + actual: () => ['a stream'], + which: () => [ + ...prefixFirst('emitted ', literal(emitted)), + if (count > 0) 'following $count other items' + ]); } count++; } @@ -420,17 +427,18 @@ extension StreamChecks on Subject> { await _expectAsync(() => ['is done'], (actual) async { if (!await actual.hasNext) return null; try { + final next = await actual.next; return Rejection( - actual: ['a stream'], - which: prefixFirst( - 'emitted an unexpected value: ', literal(await actual.next))); + actual: () => ['a stream'], + which: () => + prefixFirst('emitted an unexpected value: ', literal(next))); } catch (e, st) { - return Rejection(actual: [ - 'a stream' - ], which: [ - ...prefixFirst('emitted an unexpected error: ', literal(e)), - ...(const LineSplitter()).convert(st.toString()) - ]); + return Rejection( + actual: () => ['a stream'], + which: () => [ + ...prefixFirst('emitted an unexpected error: ', literal(e)), + ...(const LineSplitter()).convert(st.toString()) + ]); } }); } diff --git a/pkgs/checks/lib/src/extensions/core.dart b/pkgs/checks/lib/src/extensions/core.dart index 38ba4c76a..be0c9935a 100644 --- a/pkgs/checks/lib/src/extensions/core.dart +++ b/pkgs/checks/lib/src/extensions/core.dart @@ -15,7 +15,7 @@ extension CoreChecks on Subject { return Extracted.value(extract(value)); } catch (_) { return Extracted.rejection( - which: ['threw while trying to read property']); + which: () => ['threw while trying to read property']); } }); } @@ -45,7 +45,7 @@ extension CoreChecks on Subject { (actual) { if (softCheck(actual, condition) != null) return null; return Rejection( - which: ['is a value that: ', ...indent(describe(condition))], + which: () => ['is a value that: ', ...indent(describe(condition))], ); }, ); @@ -62,7 +62,7 @@ extension CoreChecks on Subject { for (final condition in conditions) { if (softCheck(actual, condition) == null) return null; } - return Rejection(which: ['did not match any condition']); + return Rejection(which: () => ['did not match any condition']); }); } @@ -72,7 +72,7 @@ extension CoreChecks on Subject { Subject isA() { return context.nest(() => ['is a $R'], (actual) { if (actual is! R) { - return Extracted.rejection(which: ['Is a ${actual.runtimeType}']); + return Extracted.rejection(which: () => ['Is a ${actual.runtimeType}']); } return Extracted.value(actual); }, atSameLevel: true); @@ -82,7 +82,7 @@ extension CoreChecks on Subject { void equals(T other) { context.expect(() => prefixFirst('equals ', literal(other)), (actual) { if (actual == other) return null; - return Rejection(which: ['are not equal']); + return Rejection(which: () => ['are not equal']); }); } @@ -91,7 +91,7 @@ extension CoreChecks on Subject { context.expect(() => prefixFirst('is identical to ', literal(other)), (actual) { if (identical(actual, other)) return null; - return Rejection(which: ['is not identical']); + return Rejection(which: () => ['is not identical']); }); } } diff --git a/pkgs/checks/lib/src/extensions/function.dart b/pkgs/checks/lib/src/extensions/function.dart index 1caa2aeb4..2bbd16d1f 100644 --- a/pkgs/checks/lib/src/extensions/function.dart +++ b/pkgs/checks/lib/src/extensions/function.dart @@ -21,14 +21,16 @@ extension ThrowsCheck on Subject { try { final result = actual(); return Extracted.rejection( - actual: prefixFirst('a function that returned ', literal(result)), - which: ['did not throw'], + actual: () => + prefixFirst('a function that returned ', literal(result)), + which: () => ['did not throw'], ); } catch (e) { if (e is E) return Extracted.value(e as E); return Extracted.rejection( - actual: prefixFirst('a function that threw error ', literal(e)), - which: ['did not throw an $E']); + actual: () => + prefixFirst('a function that threw error ', literal(e)), + which: () => ['did not throw an $E']); } }); } @@ -44,12 +46,12 @@ extension ThrowsCheck on Subject { try { return Extracted.value(actual()); } catch (e, st) { - return Extracted.rejection(actual: [ - 'a function that throws' - ], which: [ - ...prefixFirst('threw ', literal(e)), - ...st.toString().split('\n') - ]); + return Extracted.rejection( + actual: () => ['a function that throws'], + which: () => [ + ...prefixFirst('threw ', literal(e)), + ...st.toString().split('\n') + ]); } }); } diff --git a/pkgs/checks/lib/src/extensions/iterable.dart b/pkgs/checks/lib/src/extensions/iterable.dart index a48ecc321..ef73789f2 100644 --- a/pkgs/checks/lib/src/extensions/iterable.dart +++ b/pkgs/checks/lib/src/extensions/iterable.dart @@ -16,14 +16,14 @@ extension IterableChecks on Subject> { void isEmpty() { context.expect(() => const ['is empty'], (actual) { if (actual.isEmpty) return null; - return Rejection(which: ['is not empty']); + return Rejection(which: () => ['is not empty']); }); } void isNotEmpty() { context.expect(() => const ['is not empty'], (actual) { if (actual.isNotEmpty) return null; - return Rejection(which: ['is not empty']); + return Rejection(which: () => ['is not empty']); }); } @@ -33,10 +33,10 @@ extension IterableChecks on Subject> { context.expect(() { return prefixFirst('contains ', literal(element)); }, (actual) { - if (actual.isEmpty) return Rejection(actual: ['an empty iterable']); + if (actual.isEmpty) return Rejection(actual: () => ['an empty iterable']); if (actual.contains(element)) return null; return Rejection( - which: prefixFirst('does not contain ', literal(element))); + which: () => prefixFirst('does not contain ', literal(element))); }); } @@ -77,12 +77,13 @@ extension IterableChecks on Subject> { : currentExpected == element; if (matches && ++expectedIndex >= expected.length) return null; } - return Rejection(which: [ - ...prefixFirst( - 'did not have an element matching the expectation at index ' - '$expectedIndex ', - literal(expected[expectedIndex])), - ]); + return Rejection( + which: () => [ + ...prefixFirst( + 'did not have an element matching the expectation at index ' + '$expectedIndex ', + literal(expected[expectedIndex])), + ]); }); } @@ -97,11 +98,11 @@ extension IterableChecks on Subject> { ...conditionDescription, ]; }, (actual) { - if (actual.isEmpty) return Rejection(actual: ['an empty iterable']); + if (actual.isEmpty) return Rejection(actual: () => ['an empty iterable']); for (var e in actual) { if (softCheck(e, elementCondition) == null) return null; } - return Rejection(which: ['Contains no matching element']); + return Rejection(which: () => ['Contains no matching element']); }); } @@ -123,15 +124,17 @@ extension IterableChecks on Subject> { final element = iterator.current; final failure = softCheck(element, elementCondition); if (failure == null) continue; - final which = failure.rejection.which; - return Rejection(which: [ - 'has an element at index $i that:', - ...indent(failure.detail.actual.skip(1)), - ...indent(prefixFirst('Actual: ', failure.rejection.actual), - failure.detail.depth + 1), - if (which != null && which.isNotEmpty) - ...indent(prefixFirst('Which: ', which), failure.detail.depth + 1), - ]); + final which = failure.rejection.which?.call(); + return Rejection( + which: () => [ + 'has an element at index $i that:', + ...indent(failure.detail.actual.skip(1)), + ...indent(prefixFirst('Actual: ', failure.rejection.actual()), + failure.detail.depth + 1), + if (which != null && which.isNotEmpty) + ...indent(prefixFirst('Which: ', which), + failure.detail.depth + 1), + ]); } return null; }); @@ -162,18 +165,18 @@ extension IterableChecks on Subject> { actual, expected, (actual, expected) => expected == actual, - (expected, index, count) => [ - ...prefixFirst( - 'has no element equal to the expected element at index ' - '$index: ', - literal(expected)), - if (count > 1) 'or ${count - 1} other elements', - ], - (actual, index, count) => [ - ...prefixFirst( - 'has an unexpected element at index $index: ', literal(actual)), - if (count > 1) 'and ${count - 1} other unexpected elements', - ], + (expected, index, count) => () => [ + ...prefixFirst( + 'has no element equal to the expected element at index ' + '$index: ', + literal(expected)), + if (count > 1) 'or ${count - 1} other elements', + ], + (actual, index, count) => () => [ + ...prefixFirst('has an unexpected element at index $index: ', + literal(actual)), + if (count > 1) 'and ${count - 1} other unexpected elements', + ], ); if (which == null) return null; return Rejection(which: which); @@ -193,16 +196,16 @@ extension IterableChecks on Subject> { actual, expected, (actual, expected) => softCheck(actual, expected) == null, - (expected, index, count) => [ - 'has no element matching the condition at index $index:', - ...describe(expected), - if (count > 1) 'or ${count - 1} other conditions', - ], - (actual, index, count) => [ - ...prefixFirst( - 'has an unmatched element at index $index: ', literal(actual)), - if (count > 1) 'and ${count - 1} other unmatched elements', - ], + (expected, index, count) => () => [ + 'has no element matching the condition at index $index:', + ...describe(expected), + if (count > 1) 'or ${count - 1} other conditions', + ], + (actual, index, count) => () => [ + ...prefixFirst('has an unmatched element at index $index: ', + literal(actual)), + if (count > 1) 'and ${count - 1} other unmatched elements', + ], ); if (which == null) return null; return Rejection(which: which); @@ -230,27 +233,30 @@ extension IterableChecks on Subject> { for (var i = 0; i < expected.length; i++) { final expectedValue = expected[i]; if (!iterator.moveNext()) { - return Rejection(which: [ - 'has too few elements, there is no element to match at index $i' - ]); + return Rejection( + which: () => [ + 'has too few elements, ' + 'there is no element to match at index $i' + ]); } final actualValue = iterator.current; final failure = softCheck(actualValue, elementCondition(expectedValue)); if (failure == null) continue; final innerDescription = describe(elementCondition(expectedValue)); - final which = failure.rejection.which; - return Rejection(which: [ - 'does not have an element at index $i that:', - ...innerDescription, - ...prefixFirst( - 'Actual element at index $i: ', failure.rejection.actual), - if (which != null) ...prefixFirst('Which: ', which), - ]); + final which = failure.rejection.which?.call(); + return Rejection( + which: () => [ + 'does not have an element at index $i that:', + ...innerDescription, + ...prefixFirst('Actual element at index $i: ', + failure.rejection.actual()), + if (which != null) ...prefixFirst('Which: ', which), + ]); } if (!iterator.moveNext()) return null; - return Rejection(which: [ - 'has too many elements, expected exactly ${expected.length}' - ]); + return Rejection( + which: () => + ['has too many elements, expected exactly ${expected.length}']); }); } } diff --git a/pkgs/checks/lib/src/extensions/map.dart b/pkgs/checks/lib/src/extensions/map.dart index 4984b5fb2..c3dca990e 100644 --- a/pkgs/checks/lib/src/extensions/map.dart +++ b/pkgs/checks/lib/src/extensions/map.dart @@ -18,7 +18,8 @@ extension MapChecks on Subject> { () => prefixFirst('contains a value for ', literal(key)), (actual) { if (!actual.containsKey(key)) { return Extracted.rejection( - which: prefixFirst('does not contain the key ', literal(key))); + which: () => + prefixFirst('does not contain the key ', literal(key))); } return Extracted.value(actual[key] as V); }); @@ -27,14 +28,14 @@ extension MapChecks on Subject> { void isEmpty() { context.expect(() => const ['is empty'], (actual) { if (actual.isEmpty) return null; - return Rejection(which: ['is not empty']); + return Rejection(which: () => ['is not empty']); }); } void isNotEmpty() { context.expect(() => const ['is not empty'], (actual) { if (actual.isNotEmpty) return null; - return Rejection(which: ['is not empty']); + return Rejection(which: () => ['is not empty']); }); } @@ -43,7 +44,7 @@ extension MapChecks on Subject> { context.expect(() => prefixFirst('contains key ', literal(key)), (actual) { if (actual.containsKey(key)) return null; return Rejection( - which: prefixFirst('does not contain key ', literal(key))); + which: () => prefixFirst('does not contain key ', literal(key))); }); } @@ -58,11 +59,11 @@ extension MapChecks on Subject> { ...conditionDescription, ]; }, (actual) { - if (actual.isEmpty) return Rejection(actual: ['an empty map']); + if (actual.isEmpty) return Rejection(actual: () => ['an empty map']); for (var k in actual.keys) { if (softCheck(k, keyCondition) == null) return null; } - return Rejection(which: ['Contains no matching key']); + return Rejection(which: () => ['Contains no matching key']); }); } @@ -72,7 +73,7 @@ extension MapChecks on Subject> { (actual) { if (actual.containsValue(value)) return null; return Rejection( - which: prefixFirst('does not contain value ', literal(value))); + which: () => prefixFirst('does not contain value ', literal(value))); }); } @@ -87,11 +88,11 @@ extension MapChecks on Subject> { ...conditionDescription, ]; }, (actual) { - if (actual.isEmpty) return Rejection(actual: ['an empty map']); + if (actual.isEmpty) return Rejection(actual: () => ['an empty map']); for (var v in actual.values) { if (softCheck(v, valueCondition) == null) return null; } - return Rejection(which: ['Contains no matching value']); + return Rejection(which: () => ['Contains no matching value']); }); } diff --git a/pkgs/checks/lib/src/extensions/math.dart b/pkgs/checks/lib/src/extensions/math.dart index b087aef2b..3024a3cb8 100644 --- a/pkgs/checks/lib/src/extensions/math.dart +++ b/pkgs/checks/lib/src/extensions/math.dart @@ -9,7 +9,7 @@ extension NumChecks on Subject { void isGreaterThan(num other) { context.expect(() => ['is greater than <$other>'], (actual) { if (actual > other) return null; - return Rejection(which: ['is not greater than <$other>']); + return Rejection(which: () => ['is not greater than <$other>']); }); } @@ -17,7 +17,8 @@ extension NumChecks on Subject { void isGreaterOrEqual(num other) { context.expect(() => ['is greater than or equal to <$other>'], (actual) { if (actual >= other) return null; - return Rejection(which: ['is not greater than or equal to <$other>']); + return Rejection( + which: () => ['is not greater than or equal to <$other>']); }); } @@ -25,7 +26,7 @@ extension NumChecks on Subject { void isLessThan(num other) { context.expect(() => ['is less than <$other>'], (actual) { if (actual < other) return null; - return Rejection(which: ['is not less than <$other>']); + return Rejection(which: () => ['is not less than <$other>']); }); } @@ -33,7 +34,7 @@ extension NumChecks on Subject { void isLessOrEqual(num other) { context.expect(() => ['is less than or equal to <$other>'], (actual) { if (actual <= other) return null; - return Rejection(which: ['is not less than or equal to <$other>']); + return Rejection(which: () => ['is not less than or equal to <$other>']); }); } @@ -41,7 +42,7 @@ extension NumChecks on Subject { void isNaN() { context.expect(() => ['is not a number (NaN)'], (actual) { if (actual.isNaN) return null; - return Rejection(which: ['is a number']); + return Rejection(which: () => ['is a number']); }); } @@ -49,7 +50,7 @@ extension NumChecks on Subject { void isNotNaN() { context.expect(() => ['is a number (not NaN)'], (actual) { if (!actual.isNaN) return null; - return Rejection(which: ['is not a number (NaN)']); + return Rejection(which: () => ['is not a number (NaN)']); }); } @@ -57,7 +58,7 @@ extension NumChecks on Subject { void isNegative() { context.expect(() => ['is negative'], (actual) { if (actual.isNegative) return null; - return Rejection(which: ['is not negative']); + return Rejection(which: () => ['is not negative']); }); } @@ -65,7 +66,7 @@ extension NumChecks on Subject { void isNotNegative() { context.expect(() => ['is not negative'], (actual) { if (!actual.isNegative) return null; - return Rejection(which: ['is negative']); + return Rejection(which: () => ['is negative']); }); } @@ -73,7 +74,7 @@ extension NumChecks on Subject { void isFinite() { context.expect(() => ['is finite'], (actual) { if (actual.isFinite) return null; - return Rejection(which: ['is not finite']); + return Rejection(which: () => ['is not finite']); }); } @@ -84,7 +85,7 @@ extension NumChecks on Subject { void isNotFinite() { context.expect(() => ['is not finite'], (actual) { if (!actual.isFinite) return null; - return Rejection(which: ['is finite']); + return Rejection(which: () => ['is finite']); }); } @@ -94,7 +95,7 @@ extension NumChecks on Subject { void isInfinite() { context.expect(() => ['is infinite'], (actual) { if (actual.isInfinite) return null; - return Rejection(which: ['is not infinite']); + return Rejection(which: () => ['is not infinite']); }); } @@ -104,7 +105,7 @@ extension NumChecks on Subject { void isNotInfinite() { context.expect(() => ['is not infinite'], (actual) { if (!actual.isInfinite) return null; - return Rejection(which: ['is infinite']); + return Rejection(which: () => ['is infinite']); }); } @@ -114,7 +115,7 @@ extension NumChecks on Subject { context.expect(() => ['is within <$delta> of <$other>'], (actual) { final difference = (other - actual).abs(); if (difference <= delta) return null; - return Rejection(which: ['differs by <$difference>']); + return Rejection(which: () => ['differs by <$difference>']); }); } } diff --git a/pkgs/checks/lib/src/extensions/string.dart b/pkgs/checks/lib/src/extensions/string.dart index d57e8403b..5f31f8bea 100644 --- a/pkgs/checks/lib/src/extensions/string.dart +++ b/pkgs/checks/lib/src/extensions/string.dart @@ -14,7 +14,7 @@ extension StringChecks on Subject { context.expect(() => prefixFirst('contains ', literal(pattern)), (actual) { if (actual.contains(pattern)) return null; return Rejection( - which: prefixFirst('Does not contain ', literal(pattern)), + which: () => prefixFirst('Does not contain ', literal(pattern)), ); }); } @@ -24,14 +24,14 @@ extension StringChecks on Subject { void isEmpty() { context.expect(() => const ['is empty'], (actual) { if (actual.isEmpty) return null; - return Rejection(which: ['is not empty']); + return Rejection(which: () => ['is not empty']); }); } void isNotEmpty() { context.expect(() => const ['is not empty'], (actual) { if (actual.isNotEmpty) return null; - return Rejection(which: ['is empty']); + return Rejection(which: () => ['is empty']); }); } @@ -41,7 +41,7 @@ extension StringChecks on Subject { (actual) { if (actual.startsWith(other)) return null; return Rejection( - which: prefixFirst('does not start with ', literal(other)), + which: () => prefixFirst('does not start with ', literal(other)), ); }, ); @@ -53,7 +53,7 @@ extension StringChecks on Subject { (actual) { if (actual.endsWith(other)) return null; return Rejection( - which: prefixFirst('does not end with ', literal(other)), + which: () => prefixFirst('does not end with ', literal(other)), ); }, ); @@ -64,7 +64,7 @@ extension StringChecks on Subject { context.expect(() => prefixFirst('matches ', literal(expected)), (actual) { if (expected.hasMatch(actual)) return null; return Rejection( - which: prefixFirst('does not match ', literal(expected))); + which: () => prefixFirst('does not match ', literal(expected))); }); } @@ -81,12 +81,13 @@ extension StringChecks on Subject { for (var s in expected) { var index = actual.indexOf(s, fromIndex); if (index < 0) { - return Rejection(which: [ - ...prefixFirst( - 'does not have a match for the substring ', literal(s)), - if (fromIndex != 0) - 'following the other matches up to character $fromIndex' - ]); + return Rejection( + which: () => [ + ...prefixFirst( + 'does not have a match for the substring ', literal(s)), + if (fromIndex != 0) + 'following the other matches up to character $fromIndex' + ]); } fromIndex = index + s.length; } @@ -155,36 +156,39 @@ Rejection? _findDifference(String actual, String expected, if (i == minLength) { if (escapedExpected.length < escapedActual.length) { if (expected.isEmpty) { - return Rejection(which: ['is not the empty string']); + return Rejection(which: () => ['is not the empty string']); } - return Rejection(which: [ - 'is too long with unexpected trailing characters:', - _trailing(escapedActualDisplay, i) - ]); + return Rejection( + which: () => [ + 'is too long with unexpected trailing characters:', + _trailing(escapedActualDisplay, i) + ]); } else { if (actual.isEmpty) { - return Rejection(actual: [ - 'an empty string' - ], which: [ - 'is missing all expected characters:', - _trailing(escapedExpectedDisplay, 0) - ]); + return Rejection( + actual: () => ['an empty string'], + which: () => [ + 'is missing all expected characters:', + _trailing(escapedExpectedDisplay, 0) + ]); } - return Rejection(which: [ - 'is too short with missing trailing characters:', - _trailing(escapedExpectedDisplay, i) - ]); + return Rejection( + which: () => [ + 'is too short with missing trailing characters:', + _trailing(escapedExpectedDisplay, i) + ]); } } else { final indentation = ' ' * (i > 10 ? 14 : i); - return Rejection(which: [ - 'differs at offset $i:', - '${_leading(escapedExpectedDisplay, i)}' - '${_trailing(escapedExpectedDisplay, i)}', - '${_leading(escapedActualDisplay, i)}' - '${_trailing(escapedActualDisplay, i)}', - '$indentation^' - ]); + return Rejection( + which: () => [ + 'differs at offset $i:', + '${_leading(escapedExpectedDisplay, i)}' + '${_trailing(escapedExpectedDisplay, i)}', + '${_leading(escapedActualDisplay, i)}' + '${_trailing(escapedActualDisplay, i)}', + '$indentation^' + ]); } } diff --git a/pkgs/checks/test/extensions/collection_equality_test.dart b/pkgs/checks/test/extensions/collection_equality_test.dart index 64eb5bfb0..5e0c49002 100644 --- a/pkgs/checks/test/extensions/collection_equality_test.dart +++ b/pkgs/checks/test/extensions/collection_equality_test.dart @@ -82,18 +82,24 @@ void main() { ['a'] ], [ {'a'} - ])).isNotNull().deepEquals(['at [<0>] is not a Set']); + ])).isNotNull().returnsNormally().deepEquals(['at [<0>] is not a Set']); }); test('reports long iterables', () { - check(deepCollectionEquals([0], [])).isNotNull().deepEquals([ + check(deepCollectionEquals([0], [])) + .isNotNull() + .returnsNormally() + .deepEquals([ 'has more elements than expected', 'expected an iterable with 0 element(s)' ]); }); test('reports short iterables', () { - check(deepCollectionEquals([], [0])).isNotNull().deepEquals([ + check(deepCollectionEquals([], [0])) + .isNotNull() + .returnsNormally() + .deepEquals([ 'has too few elements', 'expected an iterable with at least 1 element(s)' ]); @@ -102,12 +108,14 @@ void main() { test('reports unequal elements in iterables', () { check(deepCollectionEquals([0], [1])) .isNotNull() + .returnsNormally() .deepEquals(['at [<0>] is <0>', 'which does not equal <1>']); }); test('reports unmet conditions in iterables', () { check(deepCollectionEquals([0], [it()..isA().isGreaterThan(0)])) .isNotNull() + .returnsNormally() .deepEquals([ 'has an element at [<0>] that:', ' Actual: <0>', @@ -119,6 +127,7 @@ void main() { check(deepCollectionEquals( {'a': 'b'}, {'a': it()..isA().startsWith('a')})) .isNotNull() + .returnsNormally() .deepEquals([ "has no entry to match 'a': ().startsWith('a'): 'a'})) .isNotNull() + .returnsNormally() .deepEquals([ 'has no entry to match on Subject { didRunCallback = true; final failure = softCheck(value, condition); if (failure == null) { - return Extracted.rejection(which: [ - 'was accepted by the condition checking:', - ...describe(condition) - ]); + return Extracted.rejection( + which: () => [ + 'was accepted by the condition checking:', + ...describe(condition) + ]); } return Extracted.value(failure.rejection); }); if (didRunCallback) { rejection .has((r) => r.actual, 'actual') + .returnsNormally() .deepEquals(actual ?? literal(actualValue)); } else { rejection @@ -36,7 +38,11 @@ extension RejectionChecks on Subject { if (which == null) { rejection.has((r) => r.which, 'which').isNull(); } else { - rejection.has((r) => r.which, 'which').isNotNull().deepEquals(which); + rejection + .has((r) => r.which, 'which') + .isNotNull() + .returnsNormally() + .deepEquals(which); } } @@ -51,16 +57,17 @@ extension RejectionChecks on Subject { didRunCallback = true; final failure = await softCheckAsync(value, condition); if (failure == null) { - return Extracted.rejection(which: [ - 'was accepted by the condition checking:', - ...await describeAsync(condition) - ]); + final description = await describeAsync(condition); + return Extracted.rejection( + which: () => + ['was accepted by the condition checking:', ...description]); } return Extracted.value(failure.rejection); })); if (didRunCallback) { rejection .has((r) => r.actual, 'actual') + .returnsNormally() .deepEquals(actual ?? literal(actualValue)); } else { rejection @@ -71,7 +78,11 @@ extension RejectionChecks on Subject { if (which == null) { rejection.has((r) => r.which, 'which').isNull(); } else { - rejection.has((r) => r.which, 'which').isNotNull().deepEquals(which); + rejection + .has((r) => r.which, 'which') + .isNotNull() + .returnsNormally() + .deepEquals(which); } } }