| // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import '../../context.dart'; | |
| import '../collection_equality.dart'; | |
| import 'core.dart'; | |
| extension IterableChecks<T> on Subject<Iterable<T>> { | |
| /// A [Subject] for the number of elements in the iterable. | |
| /// | |
| /// {@example /example/iterable/iterable/length.dart} | |
| Subject<int> get length => has((l) => l.length, 'length'); | |
| /// A [Subject] for the first element of the iterable. | |
| /// | |
| /// Fails if the iterable has no elements. | |
| /// | |
| /// {@example /example/iterable/iterable/first.dart} | |
| Subject<T> get first => context.nest( | |
| () => ['has first element'], | |
| addPredicate: (predicateNoun) => 'has first element: $predicateNoun', | |
| (actual) { | |
| final iterator = actual.iterator; | |
| if (!iterator.moveNext()) { | |
| return Extracted.rejection(which: ['has no elements']); | |
| } | |
| return Extracted.value(iterator.current); | |
| }, | |
| ); | |
| /// A [Subject] for the last element of the iterable. | |
| /// | |
| /// Fails if the iterable has no elements. | |
| /// | |
| /// {@example /example/iterable/iterable/last.dart} | |
| Subject<T> get last => context.nest( | |
| () => ['has last element'], | |
| addPredicate: (predicateNoun) => 'has last element: $predicateNoun', | |
| (actual) { | |
| final iterator = actual.iterator; | |
| if (!iterator.moveNext()) { | |
| return Extracted.rejection(which: ['has no elements']); | |
| } | |
| var current = iterator.current; | |
| while (iterator.moveNext()) { | |
| current = iterator.current; | |
| } | |
| return Extracted.value(current); | |
| }, | |
| ); | |
| /// A [Subject] for the only element of the iterable. | |
| /// | |
| /// Fails if the iterable does not have exactly one element. | |
| /// | |
| /// {@example /example/iterable/iterable/single.dart} | |
| Subject<T> get single => context.nest( | |
| () => ['has single element'], | |
| addPredicate: (predicateNoun) => 'has single element: $predicateNoun', | |
| (actual) { | |
| final iterator = actual.iterator; | |
| if (!iterator.moveNext()) { | |
| 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.value(value); | |
| }, | |
| ); | |
| /// Expects that the iterable has no elements. | |
| /// | |
| /// {@example /example/iterable/iterable/is_empty.dart} | |
| void get isEmpty { | |
| context.expect( | |
| () => const ['is empty'], | |
| predicateNoun: () => 'an empty iterable', | |
| (actual) { | |
| if (actual.isEmpty) return null; | |
| return Rejection(which: ['is not empty']); | |
| }, | |
| ); | |
| } | |
| /// Expects that the iterable has at least one element. | |
| /// | |
| /// {@example /example/iterable/iterable/is_not_empty.dart} | |
| void get isNotEmpty { | |
| context.expect( | |
| () => const ['is not empty'], | |
| predicateNoun: () => 'a non-empty iterable', | |
| (actual) { | |
| if (actual.isNotEmpty) return null; | |
| return Rejection(which: ['is empty']); | |
| }, | |
| ); | |
| } | |
| /// Expects that the iterable contains [element] according to | |
| /// [Iterable.contains]. | |
| /// | |
| /// {@example /example/iterable/iterable/contains.dart} | |
| void contains(T element) { | |
| context.expect( | |
| () => prefixFirst('contains ', literal(element)), | |
| predicateNoun: () { | |
| final l = literal(element).singleOrNull; | |
| return l != null ? 'an iterable containing $l' : null; | |
| }, | |
| (actual) { | |
| if (actual.contains(element)) return null; | |
| if (actual.isEmpty) return Rejection(actual: ['an empty iterable']); | |
| return Rejection( | |
| which: prefixFirst('does not contain ', literal(element)), | |
| ); | |
| }, | |
| ); | |
| } | |
| /// Expects that the iterable contains a value matching each expected value | |
| /// from [elements] in the given order, with any extra elements between | |
| /// them. | |
| /// | |
| /// For example, the following will succeed: | |
| /// | |
| /// ```dart | |
| /// check([1, 0, 2, 0, 3]).containsInOrder([1, 2, 3]); | |
| /// ``` | |
| /// | |
| /// Values in [elements] may be a `T`, a `Condition<T>`, or a | |
| /// `Condition<Object?>`. If an expectation is a [Condition] it will be | |
| /// checked against the actual values, and any other expectations, including | |
| /// those that are not a `T` or a [Condition], will be compared with | |
| /// the equality operator. | |
| /// | |
| /// ```dart | |
| /// check([1, 0, 2, 0, 3]) | |
| /// .containsInOrder([1, Condition.it<int>()..isGreaterThan(1), 3]); | |
| /// ``` | |
| @Deprecated( | |
| 'Use `containsEqualInOrder` for expectations with values compared' | |
| ' with `==` or `containsMatchingInOrder` for other expectations', | |
| ) | |
| void containsInOrder(Iterable<Object?> elements) { | |
| context.expect( | |
| () => prefixFirst('contains, in order: ', literal(elements)), | |
| (actual) { | |
| final expected = elements.toList(); | |
| if (expected.isEmpty) { | |
| throw ArgumentError('expected may not be empty'); | |
| } | |
| var expectedIndex = 0; | |
| for (final element in actual) { | |
| final currentExpected = expected[expectedIndex]; | |
| final matches = currentExpected is Condition<T> | |
| ? currentExpected.softCheckSync(element) == null | |
| : currentExpected is Condition<dynamic> | |
| ? currentExpected.softCheckSync(element) == null | |
| : 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]), | |
| ), | |
| ], | |
| ); | |
| }, | |
| ); | |
| } | |
| /// Expects that the iterable contains a value matching each condition in | |
| /// [conditions] in the given order, with any extra elements between them. | |
| /// | |
| /// For example, the following will succeed: | |
| /// | |
| /// ```dart | |
| /// check([1, 10, 2, 10, 3]).containsMatchingInOrder([ | |
| /// .it()..isLessThan(2), | |
| /// .it()..isLessThan(3), | |
| /// .it()..isLessThan(4), | |
| /// ]); | |
| /// ``` | |
| /// | |
| /// {@example /example/iterable/iterable/contains_matching_in_order.dart} | |
| void containsMatchingInOrder(Iterable<Condition<T>> conditions) { | |
| context.expect( | |
| () => prefixFirst('contains, in order: ', literal(conditions)), | |
| (actual) { | |
| final expected = conditions.toList(); | |
| if (expected.isEmpty) { | |
| throw ArgumentError('expected may not be empty'); | |
| } | |
| var expectedIndex = 0; | |
| for (final element in actual) { | |
| final currentExpected = expected[expectedIndex]; | |
| final matches = currentExpected.softCheckSync(element) == null; | |
| 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]), | |
| ), | |
| ], | |
| ); | |
| }, | |
| ); | |
| } | |
| /// Expects that the iterable contains a value equals to each expected value | |
| /// from [elements] in the given order, with any extra elements between | |
| /// them. | |
| /// | |
| /// Values are compared with the equality operator. | |
| /// | |
| /// {@example /example/iterable/iterable/contains_equal_in_order.dart} | |
| void containsEqualInOrder(Iterable<T> elements) { | |
| context.expect( | |
| () => prefixFirst('contains, in order: ', literal(elements)), | |
| (actual) { | |
| final expected = elements.toList(); | |
| if (expected.isEmpty) { | |
| throw ArgumentError('expected may not be empty'); | |
| } | |
| var expectedIndex = 0; | |
| for (final element in actual) { | |
| final currentExpected = expected[expectedIndex]; | |
| final matches = currentExpected == element; | |
| if (matches && ++expectedIndex >= expected.length) return null; | |
| } | |
| return Rejection( | |
| which: [ | |
| ...prefixFirst( | |
| 'did not have an element equal to the expectation at index ' | |
| '$expectedIndex ', | |
| literal(expected[expectedIndex]), | |
| ), | |
| ], | |
| ); | |
| }, | |
| ); | |
| } | |
| /// Expects that the iterable contains at least on element such that | |
| /// [elementCondition] is satisfied. | |
| /// | |
| /// {@example /example/iterable/iterable/any.dart} | |
| void any(Condition<T> elementCondition) { | |
| context.expect( | |
| () { | |
| final conditionDescription = elementCondition.describeSync(); | |
| assert(conditionDescription.isNotEmpty); | |
| return ['contains a value that:', ...conditionDescription]; | |
| }, | |
| (actual) { | |
| if (actual.isEmpty) return Rejection(actual: ['an empty iterable']); | |
| for (var e in actual) { | |
| if (elementCondition.softCheckSync(e) == null) return null; | |
| } | |
| return Rejection(which: ['contains no matching element']); | |
| }, | |
| ); | |
| } | |
| /// Expects there are no elements in the iterable which fail to satisfy | |
| /// [elementCondition]. | |
| /// | |
| /// Empty iterables will pass always pass this expectation. | |
| /// | |
| /// {@example /example/iterable/iterable/every.dart} | |
| void every(Condition<T> elementCondition) { | |
| context.expect( | |
| () { | |
| final conditionDescription = elementCondition.describeSync(); | |
| assert(conditionDescription.isNotEmpty); | |
| return ['only has values that:', ...conditionDescription]; | |
| }, | |
| (actual) { | |
| final iterator = actual.iterator; | |
| for (var i = 0; iterator.moveNext(); i++) { | |
| final element = iterator.current; | |
| final failure = elementCondition.softCheckSync(element); | |
| 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, | |
| ), | |
| ], | |
| ); | |
| } | |
| return null; | |
| }, | |
| ); | |
| } | |
| /// Expects that the iterable contains elements that are deeply equal to the | |
| /// elements of [expected]. | |
| /// | |
| /// {@macro deep_collection_equals} | |
| /// | |
| /// {@example /example/iterable/iterable/deep_equals.dart} | |
| void deepEquals(Iterable<Object?> expected) => context.expect( | |
| () => prefixFirst('is deeply equal to ', literal(expected)), | |
| predicateNoun: () => literal(expected).singleOrNull, | |
| (actual) { | |
| final which = deepCollectionEquals(actual, expected); | |
| if (which == null) return null; | |
| return Rejection(which: which); | |
| }, | |
| ); | |
| /// Expects that the iterable contains elements which equal those of | |
| /// [expected] in any order. | |
| /// | |
| /// Should not be used for very large collections, runtime is O(n^2.5) in the | |
| /// worst case where the iterables contain many equal elements, and O(n^2) in | |
| /// more typical cases. | |
| /// | |
| /// {@example /example/iterable/iterable/unordered_equals.dart} | |
| void unorderedEquals(Iterable<T> expected) { | |
| context.expect(() => prefixFirst('unordered equals ', literal(expected)), ( | |
| actual, | |
| ) { | |
| final which = unorderedCompare( | |
| 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', | |
| ], | |
| ); | |
| if (which == null) return null; | |
| return Rejection(which: which); | |
| }); | |
| } | |
| /// Expects that the iterable contains elements which match all conditions of | |
| /// [expected] in any order. | |
| /// | |
| /// Should not be used for very large collections, runtime is O(n^2.5) in the | |
| /// worst case where conditions match many elements, and O(n^2) in more | |
| /// typical cases. | |
| /// | |
| /// {@example /example/iterable/iterable/unordered_matches.dart} | |
| void unorderedMatches(Iterable<Condition<T>> expected) { | |
| context.expect(() => prefixFirst('unordered matches ', literal(expected)), ( | |
| actual, | |
| ) { | |
| final which = unorderedCompare( | |
| actual, | |
| expected, | |
| (actual, expected) => expected.softCheckSync(actual) == null, | |
| (expected, index, count) => [ | |
| 'has no element matching the condition at index $index:', | |
| ...expected.describeSync(), | |
| 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); | |
| }); | |
| } | |
| /// Expects that the iterable contains elements that correspond by the | |
| /// [elementCondition] exactly to each element in [expected]. | |
| /// | |
| /// Fails if the iterable has a different length than [expected]. | |
| /// | |
| /// For each element in the iterable, calls [elementCondition] with the | |
| /// corresponding element from [expected] to get the specific condition for | |
| /// that index. | |
| /// | |
| /// [description] is used in the Expected clause. It should be a predicate | |
| /// without the object, for example with the description 'is less than' the | |
| /// full expectation will be: "pairwise is less than $expected" | |
| @Deprecated('Use `pairwiseMatches`') | |
| void pairwiseComparesTo<S>( | |
| List<S> expected, | |
| Condition<T> Function(S) elementCondition, | |
| String description, | |
| ) => pairwiseMatches(expected, elementCondition, description); | |
| /// Expects that the iterable contains elements that correspond by the | |
| /// [elementCondition] exactly to each element in [expected]. | |
| /// | |
| /// Fails if the iterable has a different length than [expected]. | |
| /// | |
| /// For each element in the iterable, calls [elementCondition] with the | |
| /// corresponding element from [expected] to get the specific condition for | |
| /// that index. | |
| /// | |
| /// [description] is used in the Expected clause. It should be a predicate | |
| /// without the object, for example with the description 'is less than' the | |
| /// full expectation will be: "pairwise is less than $expected" | |
| /// | |
| /// {@example /example/iterable/iterable/pairwise_matches.dart} | |
| void pairwiseMatches<S>( | |
| List<S> expected, | |
| Condition<T> Function(S) elementCondition, | |
| String description, | |
| ) { | |
| context.expect( | |
| () { | |
| return prefixFirst('pairwise $description ', literal(expected)); | |
| }, | |
| (actual) { | |
| final iterator = actual.iterator; | |
| 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', | |
| ], | |
| ); | |
| } | |
| final actualValue = iterator.current; | |
| final condition = elementCondition(expectedValue); | |
| final failure = condition.softCheckSync(actualValue); | |
| if (failure == null) continue; | |
| final innerDescription = condition.describeSync(); | |
| final which = failure.rejection.which; | |
| return Rejection( | |
| which: [ | |
| 'does not have an element at index $i that:', | |
| ...innerDescription, | |
| ...indent( | |
| prefixFirst( | |
| 'Actual element at index $i: ', | |
| failure.rejection.actual, | |
| ), | |
| ), | |
| if (which != null) ...indent(prefixFirst('Which: ', which)), | |
| ], | |
| ); | |
| } | |
| if (!iterator.moveNext()) return null; | |
| return Rejection( | |
| which: ['has too many elements, expected exactly ${expected.length}'], | |
| ); | |
| }, | |
| ); | |
| } | |
| } |