blob: e868f16600e0b79133e180a68546ded3ffa3d154 [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 '../../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 => context.nest(() => ['has first element'], (actual) {
final iterator = actual.iterator;
if (!iterator.moveNext()) {
return Extracted.rejection(which: ['has no elements']);
}
return Extracted.value(iterator.current);
});
Subject<T> get last => context.nest(() => ['has last element'], (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);
});
Subject<T> get single => context.nest(() => ['has single element'], (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);
});
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 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 [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 callback it will be
/// checked against the actual values, and any other expectations, including
/// those that are not a `T` or a condition callback, will be compared with
/// the equality operator.
///
/// ```dart
/// 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) {
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 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) => 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) {
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"
@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));
},
(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}'],
);
},
);
}
}