From 8ab184be2f26861f13be1d62f8acc635e65a9da6 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Fri, 3 Feb 2023 02:00:47 +0000 Subject: [PATCH 1/3] Make `label` arguments take callbacks The main benefit this brings is it brings more alignment with the `clause` arguments for `expect` calls. The docs will be able to focus on the difference in how the value is use (preceding "that" in the case of labels, standing on its own in a list in the case of clauses) and can use a consistent description for how it is passed. A secondary benefit is that it allows multiline labels and avoid workaround like joining with `r'\n'`. A final benefit is that it saves some unnecessary String formatting since the callback isn't called if no expectations fail on the Subject, or when used as a soft check where the failure details are ignored. - Make the `label` arguments to `nest` and `nestAsync`, and the _label field in `_TestContext` an `Iterable Function()`. - Wrap strings that had been passed to `String` arguments with callbacks that return the string in a list. - When writing the label in a failure, write all lines, and use a postfix " that:". - Update some `Map` expectations which had manually joined with literal slash-n to keep the label or clause to a single line to take advantage of the multiline allowance. Split tests for the changed implementations and add tests for the descriptions with multiline examples. Some of these could have used multiline clauses before. --- pkgs/checks/lib/src/checks.dart | 48 ++++++------ pkgs/checks/lib/src/extensions/async.dart | 12 +-- pkgs/checks/lib/src/extensions/core.dart | 6 +- pkgs/checks/lib/src/extensions/function.dart | 4 +- pkgs/checks/lib/src/extensions/map.dart | 19 ++--- pkgs/checks/test/extensions/map_test.dart | 80 +++++++++++++++----- pkgs/checks/test/test_shared.dart | 9 ++- 7 files changed, 115 insertions(+), 63 deletions(-) diff --git a/pkgs/checks/lib/src/checks.dart b/pkgs/checks/lib/src/checks.dart index 2a09dea3a..e3f195eda 100644 --- a/pkgs/checks/lib/src/checks.dart +++ b/pkgs/checks/lib/src/checks.dart @@ -63,7 +63,7 @@ extension Skip on Subject { Subject check(T value, {String? because}) => Subject._(_TestContext._root( value: _Present(value), // TODO - switch between "a" and "an" - label: 'a $T', + label: () => ['a $T'], fail: (f) { final which = f.rejection.which; throw TestFailure([ @@ -261,7 +261,8 @@ abstract class Context { /// context. The [label] will be used as if it were a single line "clause" /// passed to [expect]. If the label is empty, the clause will be omitted. The /// label should only be left empty if the value extraction cannot fail. - Subject nest(String label, Extracted Function(T) extract, + Subject nest( + Iterable Function() label, Extracted Function(T) extract, {bool atSameLevel = false}); /// Extract an asynchronous property from the value for further checking. @@ -277,8 +278,8 @@ abstract class Context { /// Some context may disallow asynchronous expectations, for instance in /// [softCheck] which must synchronously check the value. In those contexts /// this method will throw. - Future> nestAsync( - String label, FutureOr> Function(T) extract); + Future> nestAsync(Iterable Function() label, + FutureOr> Function(T) extract); } /// A property extracted from a value being checked, or a rejection. @@ -363,7 +364,7 @@ class _TestContext implements Context, _ClauseDescription { final List<_TestContext> _aliases; // The "a value" in "a value that:". - final String _label; + final Iterable Function() _label; final void Function(CheckFailure) _fail; @@ -375,9 +376,9 @@ class _TestContext implements Context, _ClauseDescription { required void Function(CheckFailure) fail, required bool allowAsync, required bool allowUnawaited, - String? label, + Iterable Function()? label, }) : _value = value, - _label = label ?? '', + _label = label ?? (() => ['']), _fail = fail, _allowAsync = allowAsync, _allowUnawaited = allowUnawaited, @@ -394,7 +395,7 @@ class _TestContext implements Context, _ClauseDescription { _allowUnawaited = original._allowUnawaited, // Never read from an aliased context because they are never present in // `_clauses`. - _label = ''; + _label = (() => ['']); _TestContext._child(this._value, this._label, _TestContext parent) : _parent = parent, @@ -444,12 +445,13 @@ class _TestContext implements Context, _ClauseDescription { } @override - Subject nest(String label, Extracted Function(T) extract, + Subject nest( + Iterable Function() label, Extracted Function(T) extract, {bool atSameLevel = false}) { final result = _value.map((actual) => extract(actual)._fillActual(actual)); final rejection = result.rejection; if (rejection != null) { - _clauses.add(_StringClause(() => [label])); + _clauses.add(_StringClause(label)); _fail(_failure(rejection)); } final value = result.value ?? _Absent(); @@ -457,7 +459,7 @@ class _TestContext implements Context, _ClauseDescription { if (atSameLevel) { context = _TestContext._alias(this, value); _aliases.add(context); - if (label.isNotEmpty) _clauses.add(_StringClause(() => [label])); + _clauses.add(_StringClause(label)); } else { context = _TestContext._child(value, label, this); _clauses.add(context); @@ -466,8 +468,8 @@ class _TestContext implements Context, _ClauseDescription { } @override - Future> nestAsync( - String label, FutureOr> Function(T) extract) async { + Future> nestAsync(Iterable Function() label, + FutureOr> Function(T) extract) async { if (!_allowAsync) { throw StateError( 'Async expectations cannot be used on a synchronous subject'); @@ -478,7 +480,7 @@ class _TestContext implements Context, _ClauseDescription { outstandingWork.complete(); final rejection = result.rejection; if (rejection != null) { - _clauses.add(_StringClause(() => [label])); + _clauses.add(_StringClause(label)); _fail(_failure(rejection)); } final value = result.value ?? _Absent(); @@ -507,9 +509,9 @@ class _TestContext implements Context, _ClauseDescription { var successfulOverlap = 0; final expected = []; if (_clauses.isEmpty) { - expected.add(_label); + expected.addAll(_label()); } else { - expected.add('$_label that:'); + expected.addAll(postfixLast(' that:', _label())); for (var clause in _clauses) { final details = clause.detail(failingContext); expected.addAll(indent(details.expected)); @@ -550,14 +552,15 @@ class _SkippedContext implements Context { } @override - Subject nest(String label, Extracted Function(T p1) extract, + Subject nest( + Iterable Function() label, Extracted Function(T p1) extract, {bool atSameLevel = false}) { return Subject._(_SkippedContext()); } @override - Future> nestAsync( - String label, FutureOr> Function(T p1) extract) async { + Future> nestAsync(Iterable Function() label, + FutureOr> Function(T p1) extract) async { return Subject._(_SkippedContext()); } } @@ -766,7 +769,8 @@ class _ReplayContext implements Context, Condition { } @override - Subject nest(String label, Extracted Function(T p1) extract, + Subject nest( + Iterable Function() label, Extracted Function(T p1) extract, {bool atSameLevel = false}) { final nestedContext = _ReplayContext(); _interactions.add((c) { @@ -777,8 +781,8 @@ class _ReplayContext implements Context, Condition { } @override - Future> nestAsync( - String label, FutureOr> Function(T) extract) async { + Future> nestAsync(Iterable Function() label, + FutureOr> Function(T) extract) async { final nestedContext = _ReplayContext(); _interactions.add((c) async { var result = await c.nestAsync(label, extract); diff --git a/pkgs/checks/lib/src/extensions/async.dart b/pkgs/checks/lib/src/extensions/async.dart index 2633af3bf..bde4ba0fb 100644 --- a/pkgs/checks/lib/src/extensions/async.dart +++ b/pkgs/checks/lib/src/extensions/async.dart @@ -16,7 +16,7 @@ extension FutureChecks on Subject> { /// /// Fails if the future completes as an error. Future> completes() => - context.nestAsync('completes to a value', (actual) async { + context.nestAsync(() => ['completes to a value'], (actual) async { try { return Extracted.value(await actual); } catch (e, st) { @@ -61,7 +61,7 @@ extension FutureChecks on Subject> { /// /// Fails if the future completes to a value. Future> throws() => context.nestAsync( - 'completes to an error${E == Object ? '' : ' of type $E'}', + () => ['completes to an error${E == Object ? '' : ' of type $E'}'], (actual) async { try { return Extracted.rejection( @@ -110,7 +110,7 @@ extension StreamChecks on Subject> { /// Fails if the stream emits an error instead of a value, or closes without /// emitting a value. Future> emits() => - context.nestAsync('emits a value', (actual) async { + context.nestAsync(() => ['emits a value'], (actual) async { if (!await actual.hasNext) { return Extracted.rejection( actual: ['a stream'], @@ -140,8 +140,8 @@ extension StreamChecks on Subject> { /// If this expectation fails, the source queue will be left in it's original /// state. /// If this expectation succeeds, consumes the error event. - Future> emitsError() => - context.nestAsync('emits an error${E == Object ? '' : ' of type $E'}', + Future> emitsError() => context.nestAsync( + () => ['emits an error${E == Object ? '' : ' of type $E'}'], (actual) async { if (!await actual.hasNext) { return Extracted.rejection( @@ -462,6 +462,6 @@ extension StreamQueueWrap on Subject> { /// so that they can support conditional expectations and check multiple /// possibilities from the same point in the stream. Subject> get withQueue => - context.nest('', (actual) => Extracted.value(StreamQueue(actual)), + context.nest(() => [], (actual) => Extracted.value(StreamQueue(actual)), atSameLevel: true); } diff --git a/pkgs/checks/lib/src/extensions/core.dart b/pkgs/checks/lib/src/extensions/core.dart index 1d32b8894..38ba4c76a 100644 --- a/pkgs/checks/lib/src/extensions/core.dart +++ b/pkgs/checks/lib/src/extensions/core.dart @@ -10,7 +10,7 @@ extension CoreChecks on Subject { /// Sets up a clause that the value "has [name] that:" followed by any /// expectations applied to the returned [Subject]. Subject has(R Function(T) extract, String name) { - return context.nest('has $name', (T value) { + return context.nest(() => ['has $name'], (T value) { try { return Extracted.value(extract(value)); } catch (_) { @@ -70,7 +70,7 @@ extension CoreChecks on Subject { /// /// If the value is a [T], returns a [Subject] for further expectations. Subject isA() { - return context.nest('is a $R', (actual) { + return context.nest(() => ['is a $R'], (actual) { if (actual is! R) { return Extracted.rejection(which: ['Is a ${actual.runtimeType}']); } @@ -118,7 +118,7 @@ extension BoolChecks on Subject { extension NullabilityChecks on Subject { Subject isNotNull() { - return context.nest('is not null', (actual) { + return context.nest(() => ['is not null'], (actual) { if (actual == null) return Extracted.rejection(); return Extracted.value(actual); }, atSameLevel: true); diff --git a/pkgs/checks/lib/src/extensions/function.dart b/pkgs/checks/lib/src/extensions/function.dart index 7e20aca61..1caa2aeb4 100644 --- a/pkgs/checks/lib/src/extensions/function.dart +++ b/pkgs/checks/lib/src/extensions/function.dart @@ -17,7 +17,7 @@ extension ThrowsCheck on Subject { /// fail. Instead invoke the function and check the expectation on the /// returned [Future]. Subject throws() { - return context.nest('throws an error of type $E', (actual) { + return context.nest(() => ['throws an error of type $E'], (actual) { try { final result = actual(); return Extracted.rejection( @@ -40,7 +40,7 @@ extension ThrowsCheck on Subject { /// /// If the function throws synchronously, this expectation will fail. Subject returnsNormally() { - return context.nest('returns a value', (actual) { + return context.nest(() => ['returns a value'], (actual) { try { return Extracted.value(actual()); } catch (e, st) { diff --git a/pkgs/checks/lib/src/extensions/map.dart b/pkgs/checks/lib/src/extensions/map.dart index b836f1ee5..4984b5fb2 100644 --- a/pkgs/checks/lib/src/extensions/map.dart +++ b/pkgs/checks/lib/src/extensions/map.dart @@ -14,11 +14,11 @@ extension MapChecks on Subject> { Subject> get values => has((m) => m.values, 'values'); Subject get length => has((m) => m.length, 'length'); Subject operator [](K key) { - final keyString = literal(key).join(r'\n'); - return context.nest('contains a value for $keyString', (actual) { + return context.nest( + () => prefixFirst('contains a value for ', literal(key)), (actual) { if (!actual.containsKey(key)) { return Extracted.rejection( - which: ['does not contain the key $keyString']); + which: prefixFirst('does not contain the key ', literal(key))); } return Extracted.value(actual[key] as V); }); @@ -40,10 +40,10 @@ extension MapChecks on Subject> { /// Expects that the map contains [key] according to [Map.containsKey]. void containsKey(K key) { - final keyString = literal(key).join(r'\n'); - context.expect(() => ['contains key $keyString'], (actual) { + context.expect(() => prefixFirst('contains key ', literal(key)), (actual) { if (actual.containsKey(key)) return null; - return Rejection(which: ['does not contain key $keyString']); + return Rejection( + which: prefixFirst('does not contain key ', literal(key))); }); } @@ -68,10 +68,11 @@ extension MapChecks on Subject> { /// Expects that the map contains [value] according to [Map.containsValue]. void containsValue(V value) { - final valueString = literal(value).join(r'\n'); - context.expect(() => ['contains value $valueString'], (actual) { + context.expect(() => prefixFirst('contains value ', literal(value)), + (actual) { if (actual.containsValue(value)) return null; - return Rejection(which: ['does not contain value $valueString']); + return Rejection( + which: prefixFirst('does not contain value ', literal(value))); }); } diff --git a/pkgs/checks/test/extensions/map_test.dart b/pkgs/checks/test/extensions/map_test.dart index 8cbee0e06..8eb0eb291 100644 --- a/pkgs/checks/test/extensions/map_test.dart +++ b/pkgs/checks/test/extensions/map_test.dart @@ -30,10 +30,31 @@ void main() { check(_testMap).values.contains(1); }); - test('operator []', () async { - check(_testMap)['a'].equals(1); - check(_testMap) - .isRejectedBy(it()..['z'], which: ['does not contain the key \'z\'']); + group('operator []', () { + test('succeeds for a key that exists', () { + check(_testMap)['a'].equals(1); + }); + test('fails for a missing key', () { + check(_testMap) + .isRejectedBy(it()..['z'], which: ['does not contain the key \'z\'']); + }); + test('can be described', () { + check(it>()..['some\nlong\nkey']) + .description + .deepEquals([ + " contains a value for 'some", + ' long', + " key'", + ]); + check(it>()..['some\nlong\nkey'].equals(1)) + .description + .deepEquals([ + " contains a value for 'some", + ' long', + " key' that:", + ' equals <1>', + ]); + }); }); test('isEmpty', () { check({}).isEmpty(); @@ -43,13 +64,25 @@ void main() { check(_testMap).isNotEmpty(); check({}).isRejectedBy(it()..isNotEmpty(), which: ['is not empty']); }); - test('containsKey', () { - check(_testMap).containsKey('a'); - - check(_testMap).isRejectedBy( - it()..containsKey('c'), - which: ["does not contain key 'c'"], - ); + group('containsKey', () { + test('succeeds for a key that exists', () { + check(_testMap).containsKey('a'); + }); + test('fails for a missing key', () { + check(_testMap).isRejectedBy( + it()..containsKey('c'), + which: ["does not contain key 'c'"], + ); + }); + test('can be described', () { + check(it>()..containsKey('some\nlong\nkey')) + .description + .deepEquals([ + " contains key 'some", + ' long', + " key'", + ]); + }); }); test('containsKeyThat', () { check(_testMap).containsKeyThat(it()..equals('a')); @@ -58,12 +91,25 @@ void main() { which: ['Contains no matching key'], ); }); - test('containsValue', () { - check(_testMap).containsValue(1); - check(_testMap).isRejectedBy( - it()..containsValue(3), - which: ['does not contain value <3>'], - ); + group('containsValue', () { + test('succeeds for happy case', () { + check(_testMap).containsValue(1); + }); + test('fails for missing value', () { + check(_testMap).isRejectedBy( + it()..containsValue(3), + which: ['does not contain value <3>'], + ); + }); + test('can be described', () { + check(it>()..containsValue('some\nlong\nkey')) + .description + .deepEquals([ + " contains value 'some", + ' long', + " key'", + ]); + }); }); test('containsValueThat', () { check(_testMap).containsValueThat(it()..equals(1)); diff --git a/pkgs/checks/test/test_shared.dart b/pkgs/checks/test/test_shared.dart index 325c63279..1a30eb55a 100644 --- a/pkgs/checks/test/test_shared.dart +++ b/pkgs/checks/test/test_shared.dart @@ -10,8 +10,8 @@ extension RejectionChecks on Subject { {Iterable? actual, Iterable? which}) { late T actualValue; var didRunCallback = false; - final rejection = context - .nest('does not meet a condition with a Rejection', (value) { + final rejection = context.nest( + () => ['does not meet a condition with a Rejection'], (value) { actualValue = value; didRunCallback = true; final failure = softCheck(value, condition); @@ -45,7 +45,8 @@ extension RejectionChecks on Subject { late T actualValue; var didRunCallback = false; final rejection = (await context.nestAsync( - 'does not meet an async condition with a Rejection', (value) async { + () => ['does not meet an async condition with a Rejection'], + (value) async { actualValue = value; didRunCallback = true; final failure = await softCheckAsync(value, condition); @@ -80,7 +81,7 @@ extension ConditionChecks on Subject> { has((c) => describe(c), 'description'); Future>> get asyncDescription async => context.nestAsync( - 'has description', + () => ['has description'], (condition) async => Extracted.value(await describeAsync(condition))); } From b95c06081ec48a5e63f6cc1055374b39093e2cfe Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Fri, 3 Feb 2023 03:12:00 +0000 Subject: [PATCH 2/3] 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` with `Iterable Function()` and invoke them at the moment the strings are needed. --- pkgs/checks/lib/src/checks.dart | 28 +-- pkgs/checks/lib/src/collection_equality.dart | 70 ++++---- pkgs/checks/lib/src/extensions/async.dart | 170 +++++++++--------- pkgs/checks/lib/src/extensions/core.dart | 12 +- pkgs/checks/lib/src/extensions/function.dart | 22 +-- pkgs/checks/lib/src/extensions/iterable.dart | 76 ++++---- pkgs/checks/lib/src/extensions/map.dart | 19 +- pkgs/checks/lib/src/extensions/math.dart | 27 +-- pkgs/checks/lib/src/extensions/string.dart | 74 ++++---- .../extensions/collection_equality_test.dart | 20 ++- pkgs/checks/test/test_shared.dart | 31 ++-- 11 files changed, 301 insertions(+), 248 deletions(-) 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..88aa9a6c0 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,10 +136,10 @@ 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, @@ -154,10 +156,10 @@ Iterable? _findSetDifference( ); } -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); @@ -241,7 +243,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? unorderedCompare( +Iterable Function()? unorderedCompare( Iterable actual, Iterable expected, bool Function(T, E) elementsEqual, @@ -261,12 +263,12 @@ Iterable? unorderedCompare( 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); } return null; 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..5cf65bac1 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; }); @@ -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); } } } From 974907747ce795cf7444150b063747786c321206 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Thu, 8 Jun 2023 19:28:15 +0000 Subject: [PATCH 3/3] Fix `which` arguments for new APIs since last merge --- pkgs/checks/lib/src/extensions/core.dart | 10 ++++++---- pkgs/checks/lib/src/extensions/iterable.dart | 9 +++++---- pkgs/checks/test/context_test.dart | 14 ++++++++------ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/pkgs/checks/lib/src/extensions/core.dart b/pkgs/checks/lib/src/extensions/core.dart index 61ca996de..1f40b883a 100644 --- a/pkgs/checks/lib/src/extensions/core.dart +++ b/pkgs/checks/lib/src/extensions/core.dart @@ -147,7 +147,7 @@ extension ComparableChecks on Subject> { (actual) { if (actual.compareTo(other) > 0) return null; return Rejection( - which: prefixFirst('is not greater than ', literal(other))); + which: () => prefixFirst('is not greater than ', literal(other))); }); } @@ -158,7 +158,7 @@ extension ComparableChecks on Subject> { (actual) { if (actual.compareTo(other) >= 0) return null; return Rejection( - which: + which: () => prefixFirst('is not greater than or equal to ', literal(other))); }); } @@ -168,7 +168,8 @@ extension ComparableChecks on Subject> { context.expect(() => prefixFirst('is less than ', literal(other)), (actual) { if (actual.compareTo(other) < 0) return null; - return Rejection(which: prefixFirst('is not less than ', literal(other))); + return Rejection( + which: () => prefixFirst('is not less than ', literal(other))); }); } @@ -179,7 +180,8 @@ extension ComparableChecks on Subject> { (actual) { if (actual.compareTo(other) <= 0) return null; return Rejection( - which: prefixFirst('is not less than or equal to ', literal(other))); + which: () => + prefixFirst('is not less than or equal to ', literal(other))); }); } } diff --git a/pkgs/checks/lib/src/extensions/iterable.dart b/pkgs/checks/lib/src/extensions/iterable.dart index 984db896c..63bc75f67 100644 --- a/pkgs/checks/lib/src/extensions/iterable.dart +++ b/pkgs/checks/lib/src/extensions/iterable.dart @@ -13,7 +13,7 @@ extension IterableChecks on Subject> { Subject get first => context.nest(() => ['has first element'], (actual) { final iterator = actual.iterator; if (!iterator.moveNext()) { - return Extracted.rejection(which: ['has no elements']); + return Extracted.rejection(which: () => ['has no elements']); } return Extracted.value(iterator.current); }); @@ -21,7 +21,7 @@ extension IterableChecks on Subject> { Subject get last => context.nest(() => ['has last element'], (actual) { final iterator = actual.iterator; if (!iterator.moveNext()) { - return Extracted.rejection(which: ['has no elements']); + return Extracted.rejection(which: () => ['has no elements']); } var current = iterator.current; while (iterator.moveNext()) { @@ -33,11 +33,12 @@ extension IterableChecks on Subject> { Subject get single => context.nest(() => ['has single element'], (actual) { final iterator = actual.iterator; if (!iterator.moveNext()) { - return Extracted.rejection(which: ['has no elements']); + return Extracted.rejection(which: () => ['has no elements']); } final value = iterator.current; if (iterator.moveNext()) { - return Extracted.rejection(which: ['has more than one element']); + return Extracted.rejection( + which: () => ['has more than one element']); } return Extracted.value(value); }); diff --git a/pkgs/checks/test/context_test.dart b/pkgs/checks/test/context_test.dart index 3fd92ac5a..2fccf962f 100644 --- a/pkgs/checks/test/context_test.dart +++ b/pkgs/checks/test/context_test.dart @@ -112,7 +112,7 @@ void main() { check(null).context.expectUnawaited(() => [''], (actual, reject) { final completer = Completer() ..future.then((_) { - reject(Rejection(which: ['foo'])); + reject(Rejection(which: () => ['foo'])); }); callback = completer.complete; }); @@ -165,11 +165,13 @@ extension _MonitorChecks on Subject { onError.context.expectUnawaited(() => ['emits no further errors'], (actual, reject) async { await for (var error in actual.rest) { - reject(Rejection(which: [ - ...prefixFirst('threw late error', literal(error.error)), - ...(const LineSplitter().convert( - TestHandle.current.formatStackTrace(error.stackTrace).toString())) - ])); + reject(Rejection( + which: () => [ + ...prefixFirst('threw late error', literal(error.error)), + ...(const LineSplitter().convert(TestHandle.current + .formatStackTrace(error.stackTrace) + .toString())) + ])); } }); }