Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add containsMatchingInOrder containsEqualInOrder #2284

Merged
merged 5 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pkgs/checks/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
for equality. This maintains the path into a nested collection for typical
cases of checking for equality against a purely value collection.
- Always wrap Condition descriptions in angle brackets.
- Add `containsMatchingInOrder` and `containsEqualInOrder` to replace the
combined functionality in `containsInOrder`.
- Replace `pairwiseComparesTo` with `pairwiseMatches`.

## 0.3.0

Expand Down
7 changes: 6 additions & 1 deletion pkgs/checks/doc/migrating_from_matcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,14 @@ check(because: 'some explanation', actual).expectation();
- `containsPair(key, value)` -> Use `Subject<Map>[key].equals(value)`
- `hasLength(expected)` -> `length.equals(expected)`
- `isNot(Matcher)` -> `not(conditionCallback)`
- `pairwiseCompare` -> `pairwiseComparesTo`
- `pairwiseCompare` -> `pairwiseMatches`
- `same` -> `identicalTo`
- `stringContainsInOrder` -> `Subject<String>.containsInOrder`
- `containsAllInOrder(iterable)` ->
`Subject<Iterable>.containsMatchingInOrder(iterable)` to compare with
conditions other than equals,
`Subject<Iterable>.containsEqualInOrder(iterable)` to compare each index
with the equality operator (`==`).

### Members from `package:test/expect.dart` without a direct replacement

Expand Down
2 changes: 1 addition & 1 deletion pkgs/checks/lib/src/describe.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Iterable<String> _prettyPrint(
.map((line) => line.replaceAll("'", r"\'"))
.toList();
return prefixFirst("'", postfixLast("'", escaped));
} else if (object is Condition) {
} else if (object is Condition<Never>) {
return ['<A value that:', ...postfixLast('>', describe(object))];
} else {
final value = const LineSplitter().convert(object.toString());
Expand Down
87 changes: 87 additions & 0 deletions pkgs/checks/lib/src/extensions/iterable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ extension IterableChecks<T> on Subject<Iterable<T>> {
/// check([1, 0, 2, 0, 3])
/// .containsInOrder([1, (Subject<int> v) => v.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) {
Expand All @@ -115,6 +117,74 @@ extension IterableChecks<T> on Subject<Iterable<T>> {
});
}

/// 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, 0, 2, 0, 3]).containsMatchingInOrder([
/// (it) => it.isLessThan(2),
/// (it) => it.isLessThan(3),
/// (it) => it.isLessThan(4),
/// ]);
/// ```
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 = softCheck(element, currentExpected) == 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.
///
/// For example, the following will succeed:
///
/// ```dart
/// check([1, 0, 2, 0, 3]).containsInOrder([1, 2, 3]);
/// ```
///
/// Values, will be compared with the equality operator.
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.
void any(Condition<T> elementCondition) {
Expand Down Expand Up @@ -250,7 +320,24 @@ extension IterableChecks<T> on Subject<Iterable<T>> {
/// [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"
void pairwiseMatches<S>(List<S> expected,
Condition<T> Function(S) elementCondition, String description) {
context.expect(() {
return prefixFirst('pairwise $description ', literal(expected));
Expand Down
73 changes: 68 additions & 5 deletions pkgs/checks/test/extensions/iterable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,69 @@ void main() {
]);
});
});

group('containsMatchingInOrder', () {
test('succeeds for happy case', () {
check([0, 1, 0, 2, 0, 3]).containsMatchingInOrder([
(it) => it.isLessThan(2),
(it) => it.isLessThan(3),
(it) => it.isLessThan(4),
]);
});
test('fails for not found elements', () async {
check([0]).isRejectedBy(
(it) => it.containsMatchingInOrder([(it) => it.isGreaterThan(0)]),
which: [
'did not have an element matching the expectation at index 0 '
'<A value that:',
' is greater than <0>>'
]);
});
test('can be described', () {
check((Subject<Iterable<int>> it) => it.containsMatchingInOrder([
(it) => it.isLessThan(2),
(it) => it.isLessThan(3),
(it) => it.isLessThan(4),
])).description.deepEquals([
' contains, in order: [<A value that:',
' is less than <2>>,',
' <A value that:',
' is less than <3>>,',
' <A value that:',
' is less than <4>>]',
]);
check((Subject<Iterable<int>> it) => it.containsMatchingInOrder(
[(it) => it.equals(1), (it) => it.equals(2)]))
.description
.deepEquals([
' contains, in order: [<A value that:',
' equals <1>>,',
' <A value that:',
' equals <2>>]'
]);
});
});

group('containsEqualInOrder', () {
test('succeeds for happy case', () {
check([0, 1, 0, 2, 0, 3]).containsEqualInOrder([1, 2, 3]);
});
test('fails for not found elements', () async {
check([0]).isRejectedBy((it) => it.containsEqualInOrder([1]), which: [
'did not have an element equal to the expectation at index 0 <1>'
]);
});
test('can be described', () {
check((Subject<Iterable<int>> it) => it.containsEqualInOrder([1, 2, 3]))
.description
.deepEquals([' contains, in order: [1, 2, 3]']);
check((Subject<Iterable<int>> it) => it.containsEqualInOrder([1, 2]))
.description
.deepEquals([
' contains, in order: [1, 2]',
]);
});
});
group('every', () {
test('succeeds for the happy path', () {
check(_testIterable).every((it) => it.isGreaterOrEqual(-1));
Expand Down Expand Up @@ -178,14 +241,14 @@ void main() {
});
});

group('pairwiseComparesTo', () {
group('pairwiseMatches', () {
test('succeeds for the happy path', () {
check(_testIterable).pairwiseComparesTo([1, 2],
check(_testIterable).pairwiseMatches([1, 2],
(expected) => (it) => it.isLessThan(expected), 'is less than');
});
test('fails for mismatched element', () async {
check(_testIterable).isRejectedBy(
(it) => it.pairwiseComparesTo([1, 1],
(it) => it.pairwiseMatches([1, 1],
(expected) => (it) => it.isLessThan(expected), 'is less than'),
which: [
'does not have an element at index 1 that:',
Expand All @@ -196,15 +259,15 @@ void main() {
});
test('fails for too few elements', () {
check(_testIterable).isRejectedBy(
(it) => it.pairwiseComparesTo([1, 2, 3],
(it) => it.pairwiseMatches([1, 2, 3],
(expected) => (it) => it.isLessThan(expected), 'is less than'),
which: [
'has too few elements, there is no element to match at index 2'
]);
});
test('fails for too many elements', () {
check(_testIterable).isRejectedBy(
(it) => it.pairwiseComparesTo([1],
(it) => it.pairwiseMatches([1],
(expected) => (it) => it.isLessThan(expected), 'is less than'),
which: ['has too many elements, expected exactly 1']);
});
Expand Down