blob: 9cf67bac837346c28b845969190aa5a10f9343ef [file] [log] [blame]
// 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 'package:checks/context.dart';
import '../collection_equality.dart';
import 'core.dart';
extension IterableChecks<T> on Subject<Iterable<T>> {
Subject<int> get length => has((l) => l.length, 'length');
Subject<T> get first => has((l) => l.first, 'first element');
Subject<T> get last => has((l) => l.last, 'last element');
Subject<T> get single => has((l) => l.single, 'single element');
void isEmpty() {
context.expect(() => const ['is empty'], (actual) {
if (actual.isEmpty) return null;
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']);
});
}
/// Expects that the iterable contains [element] according to
/// [Iterable.contains].
void contains(T element) {
context.expect(() {
return prefixFirst('contains ', literal(element));
}, (actual) {
if (actual.isEmpty) return Rejection(actual: ['an empty iterable']);
if (actual.contains(element)) return null;
return Rejection(
which: prefixFirst('does not contain ', literal(element)));
});
}
/// Expects that the iterable contains a value matching each expected value
/// from [elelements] in the given order, with any extra elements between
/// them.
///
/// For example, the following will succeed:
///
/// ```dart
/// checkThat([1, 0, 2, 0, 3]).containsInOrder([1, 2, 3]);
/// ```
///
/// Values in [elements] may be a `T`, a `Condition<T>`, or a
/// `Condition<dynamic>`. 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
/// checkThat([1, 0, 2, 0, 3])
/// .containsInOrder([1, it<int>()..isGreaterThan(1), 3]);
/// ```
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>
? softCheck(element, currentExpected) == null
: currentExpected is Condition<dynamic>
? softCheck(element, currentExpected) == 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 at least on element such that
/// [elementCondition] is satisfied.
void any(Condition<T> elementCondition) {
context.expect(() {
final conditionDescription = describe(elementCondition);
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 (softCheck(e, elementCondition) == 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.
void every(Condition<T> elementCondition) {
context.expect(() {
final conditionDescription = describe(elementCondition);
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 = 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),
]);
}
return null;
});
}
/// Expects that the iterable contains elements that are deeply equal to the
/// elements of [expected].
///
/// {@macro deep_collection_equals}
void deepEquals(Iterable<Object?> expected) => context
.expect(() => prefixFirst('is deeply equal to ', literal(expected)),
(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.
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.
void unorderedMatches(Iterable<Condition<T>> expected) {
context.expect(() => prefixFirst('unordered matches ', literal(expected)),
(actual) {
final which = unorderedCompare(
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',
],
);
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"
void pairwiseComparesTo<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 failure = softCheck(actualValue, elementCondition(expectedValue));
if (failure == null) continue;
final innerDescription = describe<T>(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),
]);
}
if (!iterator.moveNext()) return null;
return Rejection(which: [
'has too many elements, expected exactly ${expected.length}'
]);
});
}
}