use std::collections::{BTreeSet, HashSet, VecDeque};
use std::fmt;
use crate::description::Description;
use crate::matcher::{MatchResult, Matcher, Mismatch};
pub trait Sequence {
type Item;
fn sequence_items(&self) -> Vec<&Self::Item>;
}
impl<T> Sequence for [T] {
type Item = T;
fn sequence_items(&self) -> Vec<&T> {
self.iter().collect()
}
}
impl<T, const N: usize> Sequence for [T; N] {
type Item = T;
fn sequence_items(&self) -> Vec<&T> {
self.iter().collect()
}
}
impl<T> Sequence for Vec<T> {
type Item = T;
fn sequence_items(&self) -> Vec<&T> {
self.iter().collect()
}
}
impl<T> Sequence for VecDeque<T> {
type Item = T;
fn sequence_items(&self) -> Vec<&T> {
self.iter().collect()
}
}
impl<T> Sequence for BTreeSet<T> {
type Item = T;
fn sequence_items(&self) -> Vec<&T> {
self.iter().collect()
}
}
impl<T> Sequence for HashSet<T> {
type Item = T;
fn sequence_items(&self) -> Vec<&T> {
self.iter().collect()
}
}
impl<S: Sequence + ?Sized> Sequence for &S {
type Item = S::Item;
fn sequence_items(&self) -> Vec<&S::Item> {
(**self).sequence_items()
}
}
pub struct Items<T>(Vec<T>);
impl<T> Sequence for Items<T> {
type Item = T;
fn sequence_items(&self) -> Vec<&T> {
self.0.iter().collect()
}
}
#[must_use]
pub fn items<I>(iter: I) -> Items<I::Item>
where
I: IntoIterator,
{
Items(iter.into_iter().collect())
}
struct LenMatcher {
expected: usize,
}
impl<C> Matcher<C> for LenMatcher
where
C: Sequence + ?Sized,
{
fn check(&self, actual: &C) -> MatchResult {
let len = actual.sequence_items().len();
if len == self.expected {
MatchResult::pass()
} else {
MatchResult::fail(Mismatch::new(
Description::text(format!("a sequence of length {}", self.expected)),
format!("a sequence of length {len}"),
))
}
}
fn description(&self) -> Description {
Description::text(format!("a sequence of length {}", self.expected))
}
}
#[must_use]
pub fn have_len<C>(n: usize) -> impl Matcher<C>
where
C: Sequence + ?Sized,
{
LenMatcher { expected: n }
}
struct EmptyMatcher {
want_empty: bool,
}
impl<C> Matcher<C> for EmptyMatcher
where
C: Sequence + ?Sized,
{
fn check(&self, actual: &C) -> MatchResult {
let len = actual.sequence_items().len();
if (len == 0) == self.want_empty {
MatchResult::pass()
} else if self.want_empty {
MatchResult::fail(Mismatch::new(
self.description_text(),
format!("a sequence of length {len}"),
))
} else {
MatchResult::fail(Mismatch::new(self.description_text(), "an empty sequence"))
}
}
fn description(&self) -> Description {
self.description_text()
}
}
impl EmptyMatcher {
fn description_text(&self) -> Description {
Description::text(if self.want_empty {
"an empty sequence"
} else {
"a non-empty sequence"
})
}
}
#[must_use]
pub fn is_empty<C>() -> impl Matcher<C>
where
C: Sequence + ?Sized,
{
EmptyMatcher { want_empty: true }
}
#[must_use]
pub fn is_not_empty<C>() -> impl Matcher<C>
where
C: Sequence + ?Sized,
{
EmptyMatcher { want_empty: false }
}
struct AnyItemMatcher<M> {
inner: M,
header: &'static str,
}
impl<C, M> Matcher<C> for AnyItemMatcher<M>
where
C: Sequence + ?Sized,
C::Item: fmt::Debug,
M: Matcher<C::Item>,
{
fn check(&self, actual: &C) -> MatchResult {
let items = actual.sequence_items();
if items.iter().any(|item| self.inner.check(item).matched) {
MatchResult::pass()
} else {
MatchResult::fail(Mismatch::new(
Description::labeled(self.header, self.inner.description()),
format!("{items:?}"),
))
}
}
fn description(&self) -> Description {
Description::labeled(self.header, self.inner.description())
}
}
#[must_use]
pub fn contains<C, M>(matcher: M) -> impl Matcher<C>
where
C: Sequence + ?Sized,
C::Item: fmt::Debug,
M: Matcher<C::Item>,
{
AnyItemMatcher {
inner: matcher,
header: "a sequence containing an item that is",
}
}
#[must_use]
pub fn at_least_one<C, M>(matcher: M) -> impl Matcher<C>
where
C: Sequence + ?Sized,
C::Item: fmt::Debug,
M: Matcher<C::Item>,
{
AnyItemMatcher {
inner: matcher,
header: "at least one item to satisfy",
}
}
struct EveryMatcher<M> {
inner: M,
}
impl<C, M> Matcher<C> for EveryMatcher<M>
where
C: Sequence + ?Sized,
C::Item: fmt::Debug,
M: Matcher<C::Item>,
{
fn check(&self, actual: &C) -> MatchResult {
let items = actual.sequence_items();
for (index, item) in items.iter().enumerate() {
if let Some(failure) = self.inner.check(item).failure {
return MatchResult::fail(Mismatch::new(
Matcher::<C>::description(self),
format!("item at index {index} was {}", failure.actual),
));
}
}
MatchResult::pass()
}
fn description(&self) -> Description {
Description::labeled("every item to satisfy", self.inner.description())
}
}
#[must_use]
pub fn every<C, M>(matcher: M) -> impl Matcher<C>
where
C: Sequence + ?Sized,
C::Item: fmt::Debug,
M: Matcher<C::Item>,
{
EveryMatcher { inner: matcher }
}
struct InOrderMatcher<M, const N: usize> {
matchers: [M; N],
}
impl<C, M, const N: usize> Matcher<C> for InOrderMatcher<M, N>
where
C: Sequence + ?Sized,
C::Item: fmt::Debug,
M: Matcher<C::Item>,
{
fn check(&self, actual: &C) -> MatchResult {
let items = actual.sequence_items();
let mut next = 0;
for item in &items {
if next < N && self.matchers[next].check(item).matched {
next += 1;
}
}
if next == N {
MatchResult::pass()
} else {
MatchResult::fail(Mismatch::new(
Matcher::<C>::description(self),
format!(
"a sequence matching {next} of {N} in order \
(no later item satisfied matcher at index {next}): {items:?}"
),
))
}
}
fn description(&self) -> Description {
let joined = self
.matchers
.iter()
.map(|m| m.description().to_string())
.collect::<Vec<_>>()
.join(", then ");
Description::text(format!("a sequence containing, in order: {joined}"))
}
}
#[must_use]
pub fn contains_in_order<C, M, const N: usize>(matchers: [M; N]) -> impl Matcher<C>
where
C: Sequence + ?Sized,
C::Item: fmt::Debug,
M: Matcher<C::Item>,
{
InOrderMatcher { matchers }
}
pub trait ContainsAll<Item> {
fn first_unsatisfied(&self, items: &[&Item]) -> Option<Description>;
fn describe(&self) -> Description;
}
macro_rules! impl_contains_all {
($first:ident, $($rest:ident),+) => {
#[allow(non_snake_case)]
impl<Item, $first, $($rest,)+> ContainsAll<Item> for ($first, $($rest,)+)
where
$first: Matcher<Item>,
$($rest: Matcher<Item>,)+
{
fn first_unsatisfied(&self, items: &[&Item]) -> Option<Description> {
let ($first, $($rest,)+) = self;
if !items.iter().any(|item| $first.check(item).matched) {
return Some($first.description());
}
$(
if !items.iter().any(|item| $rest.check(item).matched) {
return Some($rest.description());
}
)+
None
}
fn describe(&self) -> Description {
let ($first, $($rest,)+) = self;
let desc = $first.description();
$( let desc = desc.and($rest.description()); )+
desc
}
}
};
}
impl_contains_all!(M1, M2);
impl_contains_all!(M1, M2, M3);
impl_contains_all!(M1, M2, M3, M4);
impl_contains_all!(M1, M2, M3, M4, M5);
impl_contains_all!(M1, M2, M3, M4, M5, M6);
impl_contains_all!(M1, M2, M3, M4, M5, M6, M7);
impl_contains_all!(M1, M2, M3, M4, M5, M6, M7, M8);
struct ContainsAllMatcher<Tup> {
matchers: Tup,
}
impl<C, Tup> Matcher<C> for ContainsAllMatcher<Tup>
where
C: Sequence + ?Sized,
C::Item: fmt::Debug,
Tup: ContainsAll<C::Item>,
{
fn check(&self, actual: &C) -> MatchResult {
let items = actual.sequence_items();
match self.matchers.first_unsatisfied(&items) {
None => MatchResult::pass(),
Some(unsatisfied) => MatchResult::fail(Mismatch::new(
Description::labeled("a sequence containing an item that is", unsatisfied),
format!("{items:?}"),
)),
}
}
fn description(&self) -> Description {
Description::labeled("a sequence containing all of", self.matchers.describe())
}
}
#[must_use]
pub fn contains_all<C, Tup>(matchers: Tup) -> impl Matcher<C>
where
C: Sequence + ?Sized,
C::Item: fmt::Debug,
Tup: ContainsAll<C::Item>,
{
ContainsAllMatcher { matchers }
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeSet, HashSet, VecDeque};
use test_better_core::{OrFail, TestResult};
use super::*;
use crate::{check, eq, gt, is_false, is_true, lt};
#[test]
fn have_len_matches_the_exact_length() -> TestResult {
check!(have_len(3).check(&vec![1, 2, 3]).matched).satisfies(is_true())?;
let failure = have_len(3)
.check(&vec![1, 2])
.failure
.or_fail_with("length 2 is not 3")?;
check!(failure.expected.to_string()).satisfies(eq("a sequence of length 3".to_string()))?;
check!(failure.actual).satisfies(eq("a sequence of length 2".to_string()))?;
Ok(())
}
#[test]
fn items_collects_an_iterator_into_a_sequence() -> TestResult {
let lazy = (1..=3).map(|n| n * 10);
let collected = items(lazy);
check!(have_len(3).check(&collected).matched).satisfies(is_true())?;
check!(contains(eq(20)).check(&collected).matched).satisfies(is_true())?;
let failure = contains(eq(99))
.check(&collected)
.failure
.or_fail_with("99 is not in the iterator")?;
check!(failure.actual).satisfies(eq("[10, 20, 30]".to_string()))?;
Ok(())
}
#[test]
fn items_on_an_empty_iterator_is_empty() -> TestResult {
let empty: Items<i32> = items(std::iter::empty());
check!(is_empty().check(&empty).matched).satisfies(is_true())?;
Ok(())
}
#[test]
fn is_empty_and_is_not_empty_are_opposites() -> TestResult {
check!(is_empty().check(&Vec::<i32>::new()).matched).satisfies(is_true())?;
check!(is_empty().check(&vec![1]).matched).satisfies(is_false())?;
check!(is_not_empty().check(&vec![1]).matched).satisfies(is_true())?;
check!(is_not_empty().check(&Vec::<i32>::new()).matched).satisfies(is_false())?;
Ok(())
}
#[test]
fn contains_finds_a_matching_item() -> TestResult {
check!(contains(eq(2)).check(&vec![1, 2, 3]).matched).satisfies(is_true())?;
let failure = contains(eq(9))
.check(&vec![1, 2, 3])
.failure
.or_fail_with("9 is not in the sequence")?;
check!(failure.actual).satisfies(eq("[1, 2, 3]".to_string()))?;
Ok(())
}
#[test]
fn every_names_the_index_of_the_first_failure() -> TestResult {
check!(every(gt(0)).check(&vec![1, 2, 3]).matched).satisfies(is_true())?;
let failure = every(gt(0))
.check(&vec![1, 2, -1, 4])
.failure
.or_fail_with("-1 is not greater than 0")?;
check!(failure.actual.contains("index 2")).satisfies(is_true())?;
Ok(())
}
#[test]
fn at_least_one_matches_when_some_item_does() -> TestResult {
check!(at_least_one(gt(2)).check(&vec![1, 2, 3]).matched).satisfies(is_true())?;
check!(at_least_one(gt(9)).check(&vec![1, 2, 3]).matched).satisfies(is_false())?;
Ok(())
}
#[test]
fn contains_in_order_respects_order_but_not_adjacency() -> TestResult {
check!(
contains_in_order([eq(2), eq(4)])
.check(&vec![1, 2, 3, 4])
.matched
)
.satisfies(is_true())?;
let failure = contains_in_order([eq(4), eq(2)])
.check(&vec![1, 2, 3, 4])
.failure
.or_fail_with("2 does not come after 4")?;
check!(failure.actual.contains("matcher at index 1")).satisfies(is_true())?;
Ok(())
}
#[test]
fn contains_all_requires_every_matcher_to_be_satisfied() -> TestResult {
check!(contains_all((eq(1), gt(2))).check(&vec![1, 2, 3]).matched).satisfies(is_true())?;
let failure = contains_all((eq(1), gt(9)))
.check(&vec![1, 2, 3])
.failure
.or_fail_with("nothing is greater than 9")?;
check!(failure.expected.to_string().contains("greater than 9")).satisfies(is_true())?;
Ok(())
}
#[test]
fn collection_matchers_work_across_collection_types() -> TestResult {
let deque: VecDeque<i32> = VecDeque::from(vec![1, 2, 3]);
check!(have_len(3).check(&deque).matched).satisfies(is_true())?;
let btree: BTreeSet<i32> = BTreeSet::from([1, 2, 3]);
check!(contains(eq(2)).check(&btree).matched).satisfies(is_true())?;
let set: HashSet<i32> = HashSet::from([1, 2, 3]);
check!(every(gt(0)).check(&set).matched).satisfies(is_true())?;
let slice: &[i32] = &[10, 20, 30];
check!(contains_in_order([eq(10), eq(30)]).check(&slice).matched).satisfies(is_true())?;
let array = [1, 2, 3];
check!(every(lt(4)).check(&array).matched).satisfies(is_true())?;
Ok(())
}
}