Skip to main content

test_better_matchers/
collections.rs

1//! Collection matchers and the [`Sequence`] trait they are generic over.
2//!
3//! [`Sequence`] is the crate's abstraction over an ordered run of items: it is
4//! implemented for slices, arrays, `Vec`, `VecDeque`, `BTreeSet`, `HashSet`,
5//! and `&S` for any `Sequence` `S`. The matchers in this module
6//! ([`have_len`], [`is_empty`], [`is_not_empty`], [`contains`],
7//! [`contains_all`], [`contains_in_order`], [`every`], [`at_least_one`]) work
8//! for every one of those.
9//!
10//! Failures name the index of the first item (or, for sets, the offending
11//! value) that broke the expectation.
12
13use std::collections::{BTreeSet, HashSet, VecDeque};
14use std::fmt;
15
16use crate::description::Description;
17use crate::matcher::{MatchResult, Matcher, Mismatch};
18
19/// An ordered run of items a collection matcher can inspect.
20///
21/// Implemented for `[T]`, `[T; N]`, `Vec<T>`, `VecDeque<T>`, `BTreeSet<T>`,
22/// `HashSet<T>`, and `&S` for any `Sequence` `S`. Items are borrowed, not
23/// cloned. A lazy iterator is not a `Sequence` (it cannot be inspected through
24/// a shared borrow); collect it into a `Vec` first.
25pub trait Sequence {
26    /// The element type.
27    type Item;
28
29    /// Borrows every item, in order.
30    fn sequence_items(&self) -> Vec<&Self::Item>;
31}
32
33impl<T> Sequence for [T] {
34    type Item = T;
35
36    fn sequence_items(&self) -> Vec<&T> {
37        self.iter().collect()
38    }
39}
40
41impl<T, const N: usize> Sequence for [T; N] {
42    type Item = T;
43
44    fn sequence_items(&self) -> Vec<&T> {
45        self.iter().collect()
46    }
47}
48
49impl<T> Sequence for Vec<T> {
50    type Item = T;
51
52    fn sequence_items(&self) -> Vec<&T> {
53        self.iter().collect()
54    }
55}
56
57impl<T> Sequence for VecDeque<T> {
58    type Item = T;
59
60    fn sequence_items(&self) -> Vec<&T> {
61        self.iter().collect()
62    }
63}
64
65impl<T> Sequence for BTreeSet<T> {
66    type Item = T;
67
68    fn sequence_items(&self) -> Vec<&T> {
69        self.iter().collect()
70    }
71}
72
73impl<T> Sequence for HashSet<T> {
74    type Item = T;
75
76    fn sequence_items(&self) -> Vec<&T> {
77        self.iter().collect()
78    }
79}
80
81impl<S: Sequence + ?Sized> Sequence for &S {
82    type Item = S::Item;
83
84    fn sequence_items(&self) -> Vec<&S::Item> {
85        (**self).sequence_items()
86    }
87}
88
89/// An eager [`Sequence`] wrapper around an iterator's collected items.
90///
91/// A lazy iterator cannot implement [`Sequence`] directly: the trait borrows
92/// its items through `&self`, and a blanket `impl<I: Iterator>` would collide
93/// with the concrete collection impls under coherence. [`Items`] sidesteps both
94/// problems by collecting the iterator into a `Vec<T>` up front, which means
95/// the assertion sees a full, indexable view of the produced values.
96///
97/// Construct one with [`items`]. The wrapper is just a `Vec<T>` underneath, so
98/// it carries no surprises around iteration order or repeated reads.
99pub struct Items<T>(Vec<T>);
100
101impl<T> Sequence for Items<T> {
102    type Item = T;
103
104    fn sequence_items(&self) -> Vec<&T> {
105        self.0.iter().collect()
106    }
107}
108
109/// Collects an iterator into an [`Items`] wrapper that implements [`Sequence`].
110///
111/// ```
112/// use test_better_core::TestResult;
113/// use test_better_matchers::{contains, eq, check, items};
114///
115/// fn main() -> TestResult {
116///     let doubled = (1..=3).map(|n| n * 2);
117///     check!(items(doubled)).satisfies(contains(eq(4)))?;
118///     Ok(())
119/// }
120/// ```
121#[must_use]
122pub fn items<I>(iter: I) -> Items<I::Item>
123where
124    I: IntoIterator,
125{
126    Items(iter.into_iter().collect())
127}
128
129/// The matcher behind [`have_len`].
130struct LenMatcher {
131    expected: usize,
132}
133
134impl<C> Matcher<C> for LenMatcher
135where
136    C: Sequence + ?Sized,
137{
138    fn check(&self, actual: &C) -> MatchResult {
139        let len = actual.sequence_items().len();
140        if len == self.expected {
141            MatchResult::pass()
142        } else {
143            MatchResult::fail(Mismatch::new(
144                Description::text(format!("a sequence of length {}", self.expected)),
145                format!("a sequence of length {len}"),
146            ))
147        }
148    }
149
150    fn description(&self) -> Description {
151        Description::text(format!("a sequence of length {}", self.expected))
152    }
153}
154
155/// Matches a sequence with exactly `n` items.
156///
157/// ```
158/// use test_better_core::TestResult;
159/// use test_better_matchers::{check, have_len};
160///
161/// fn main() -> TestResult {
162///     check!(vec![1, 2, 3]).satisfies(have_len(3))?;
163///     Ok(())
164/// }
165/// ```
166#[must_use]
167pub fn have_len<C>(n: usize) -> impl Matcher<C>
168where
169    C: Sequence + ?Sized,
170{
171    LenMatcher { expected: n }
172}
173
174/// The matcher behind [`is_empty`] and [`is_not_empty`].
175struct EmptyMatcher {
176    want_empty: bool,
177}
178
179impl<C> Matcher<C> for EmptyMatcher
180where
181    C: Sequence + ?Sized,
182{
183    fn check(&self, actual: &C) -> MatchResult {
184        let len = actual.sequence_items().len();
185        if (len == 0) == self.want_empty {
186            MatchResult::pass()
187        } else if self.want_empty {
188            MatchResult::fail(Mismatch::new(
189                self.description_text(),
190                format!("a sequence of length {len}"),
191            ))
192        } else {
193            MatchResult::fail(Mismatch::new(self.description_text(), "an empty sequence"))
194        }
195    }
196
197    fn description(&self) -> Description {
198        self.description_text()
199    }
200}
201
202impl EmptyMatcher {
203    fn description_text(&self) -> Description {
204        Description::text(if self.want_empty {
205            "an empty sequence"
206        } else {
207            "a non-empty sequence"
208        })
209    }
210}
211
212/// Matches a sequence with no items.
213///
214/// ```
215/// use test_better_core::TestResult;
216/// use test_better_matchers::{check, is_empty};
217///
218/// fn main() -> TestResult {
219///     check!(Vec::<i32>::new()).satisfies(is_empty())?;
220///     Ok(())
221/// }
222/// ```
223#[must_use]
224pub fn is_empty<C>() -> impl Matcher<C>
225where
226    C: Sequence + ?Sized,
227{
228    EmptyMatcher { want_empty: true }
229}
230
231/// Matches a sequence with at least one item.
232///
233/// ```
234/// use test_better_core::TestResult;
235/// use test_better_matchers::{check, is_not_empty};
236///
237/// fn main() -> TestResult {
238///     check!(vec![1]).satisfies(is_not_empty())?;
239///     Ok(())
240/// }
241/// ```
242#[must_use]
243pub fn is_not_empty<C>() -> impl Matcher<C>
244where
245    C: Sequence + ?Sized,
246{
247    EmptyMatcher { want_empty: false }
248}
249
250/// The matcher behind [`contains`] and [`at_least_one`]: at least one item
251/// satisfies the inner matcher.
252struct AnyItemMatcher<M> {
253    inner: M,
254    /// The phrase that heads the expected description (`contains` and
255    /// `at_least_one` read differently even though they check the same thing).
256    header: &'static str,
257}
258
259impl<C, M> Matcher<C> for AnyItemMatcher<M>
260where
261    C: Sequence + ?Sized,
262    C::Item: fmt::Debug,
263    M: Matcher<C::Item>,
264{
265    fn check(&self, actual: &C) -> MatchResult {
266        let items = actual.sequence_items();
267        if items.iter().any(|item| self.inner.check(item).matched) {
268            MatchResult::pass()
269        } else {
270            MatchResult::fail(Mismatch::new(
271                Description::labeled(self.header, self.inner.description()),
272                format!("{items:?}"),
273            ))
274        }
275    }
276
277    fn description(&self) -> Description {
278        Description::labeled(self.header, self.inner.description())
279    }
280}
281
282/// Matches a sequence that contains at least one item satisfying `matcher`.
283///
284/// ```
285/// use test_better_core::TestResult;
286/// use test_better_matchers::{contains, eq, check};
287///
288/// fn main() -> TestResult {
289///     check!(vec![1, 2, 3]).satisfies(contains(eq(2)))?;
290///     Ok(())
291/// }
292/// ```
293#[must_use]
294pub fn contains<C, M>(matcher: M) -> impl Matcher<C>
295where
296    C: Sequence + ?Sized,
297    C::Item: fmt::Debug,
298    M: Matcher<C::Item>,
299{
300    AnyItemMatcher {
301        inner: matcher,
302        header: "a sequence containing an item that is",
303    }
304}
305
306/// Matches a sequence in which at least one item satisfies `matcher`.
307///
308/// The check is the same as [`contains`]; the two exist because they read
309/// differently at the call site.
310///
311/// ```
312/// use test_better_core::TestResult;
313/// use test_better_matchers::{at_least_one, check, gt};
314///
315/// fn main() -> TestResult {
316///     check!(vec![1, 2, 3]).satisfies(at_least_one(gt(2)))?;
317///     Ok(())
318/// }
319/// ```
320#[must_use]
321pub fn at_least_one<C, M>(matcher: M) -> impl Matcher<C>
322where
323    C: Sequence + ?Sized,
324    C::Item: fmt::Debug,
325    M: Matcher<C::Item>,
326{
327    AnyItemMatcher {
328        inner: matcher,
329        header: "at least one item to satisfy",
330    }
331}
332
333/// The matcher behind [`every`]: every item satisfies the inner matcher.
334struct EveryMatcher<M> {
335    inner: M,
336}
337
338impl<C, M> Matcher<C> for EveryMatcher<M>
339where
340    C: Sequence + ?Sized,
341    C::Item: fmt::Debug,
342    M: Matcher<C::Item>,
343{
344    fn check(&self, actual: &C) -> MatchResult {
345        let items = actual.sequence_items();
346        for (index, item) in items.iter().enumerate() {
347            if let Some(failure) = self.inner.check(item).failure {
348                return MatchResult::fail(Mismatch::new(
349                    // `EveryMatcher<M>` implements `Matcher<C>` for a family of
350                    // `C`, so `description` is spelled out to stay unambiguous.
351                    Matcher::<C>::description(self),
352                    format!("item at index {index} was {}", failure.actual),
353                ));
354            }
355        }
356        MatchResult::pass()
357    }
358
359    fn description(&self) -> Description {
360        Description::labeled("every item to satisfy", self.inner.description())
361    }
362}
363
364/// Matches a sequence in which *every* item satisfies `matcher`.
365///
366/// On failure the error names the index of the first item that did not match.
367///
368/// ```
369/// use test_better_core::TestResult;
370/// use test_better_matchers::{every, check, gt};
371///
372/// fn main() -> TestResult {
373///     check!(vec![1, 2, 3]).satisfies(every(gt(0)))?;
374///     Ok(())
375/// }
376/// ```
377#[must_use]
378pub fn every<C, M>(matcher: M) -> impl Matcher<C>
379where
380    C: Sequence + ?Sized,
381    C::Item: fmt::Debug,
382    M: Matcher<C::Item>,
383{
384    EveryMatcher { inner: matcher }
385}
386
387/// The matcher behind [`contains_in_order`].
388struct InOrderMatcher<M, const N: usize> {
389    matchers: [M; N],
390}
391
392impl<C, M, const N: usize> Matcher<C> for InOrderMatcher<M, N>
393where
394    C: Sequence + ?Sized,
395    C::Item: fmt::Debug,
396    M: Matcher<C::Item>,
397{
398    fn check(&self, actual: &C) -> MatchResult {
399        let items = actual.sequence_items();
400        let mut next = 0;
401        for item in &items {
402            if next < N && self.matchers[next].check(item).matched {
403                next += 1;
404            }
405        }
406        if next == N {
407            MatchResult::pass()
408        } else {
409            MatchResult::fail(Mismatch::new(
410                Matcher::<C>::description(self),
411                format!(
412                    "a sequence matching {next} of {N} in order \
413                     (no later item satisfied matcher at index {next}): {items:?}"
414                ),
415            ))
416        }
417    }
418
419    fn description(&self) -> Description {
420        let joined = self
421            .matchers
422            .iter()
423            .map(|m| m.description().to_string())
424            .collect::<Vec<_>>()
425            .join(", then ");
426        Description::text(format!("a sequence containing, in order: {joined}"))
427    }
428}
429
430/// Matches a sequence that contains items satisfying `matchers` in order, not
431/// necessarily contiguously.
432///
433/// On failure the error names the index of the first matcher that no remaining
434/// item could satisfy.
435///
436/// ```
437/// use test_better_core::TestResult;
438/// use test_better_matchers::{contains_in_order, eq, check};
439///
440/// fn main() -> TestResult {
441///     check!(vec![1, 2, 3, 4]).satisfies(contains_in_order([eq(2), eq(4)]))?;
442///     Ok(())
443/// }
444/// ```
445#[must_use]
446pub fn contains_in_order<C, M, const N: usize>(matchers: [M; N]) -> impl Matcher<C>
447where
448    C: Sequence + ?Sized,
449    C::Item: fmt::Debug,
450    M: Matcher<C::Item>,
451{
452    InOrderMatcher { matchers }
453}
454
455/// A tuple of matchers, all over the same `Item`, for [`contains_all`].
456///
457/// Implemented for tuples of arity 2 through 8 by a macro in this module; you
458/// do not implement it yourself.
459pub trait ContainsAll<Item> {
460    /// The description of the first matcher that no item in `items` satisfies,
461    /// or `None` if every matcher is satisfied.
462    fn first_unsatisfied(&self, items: &[&Item]) -> Option<Description>;
463
464    /// The conjunction (`a and b and ...`) of the tuple's descriptions.
465    fn describe(&self) -> Description;
466}
467
468/// Implements [`ContainsAll`] for one tuple arity. The first type parameter is
469/// split out so the description fold has a guaranteed first element.
470macro_rules! impl_contains_all {
471    ($first:ident, $($rest:ident),+) => {
472        #[allow(non_snake_case)]
473        impl<Item, $first, $($rest,)+> ContainsAll<Item> for ($first, $($rest,)+)
474        where
475            $first: Matcher<Item>,
476            $($rest: Matcher<Item>,)+
477        {
478            fn first_unsatisfied(&self, items: &[&Item]) -> Option<Description> {
479                let ($first, $($rest,)+) = self;
480                if !items.iter().any(|item| $first.check(item).matched) {
481                    return Some($first.description());
482                }
483                $(
484                    if !items.iter().any(|item| $rest.check(item).matched) {
485                        return Some($rest.description());
486                    }
487                )+
488                None
489            }
490
491            fn describe(&self) -> Description {
492                let ($first, $($rest,)+) = self;
493                let desc = $first.description();
494                $( let desc = desc.and($rest.description()); )+
495                desc
496            }
497        }
498    };
499}
500
501impl_contains_all!(M1, M2);
502impl_contains_all!(M1, M2, M3);
503impl_contains_all!(M1, M2, M3, M4);
504impl_contains_all!(M1, M2, M3, M4, M5);
505impl_contains_all!(M1, M2, M3, M4, M5, M6);
506impl_contains_all!(M1, M2, M3, M4, M5, M6, M7);
507impl_contains_all!(M1, M2, M3, M4, M5, M6, M7, M8);
508
509/// The matcher behind [`contains_all`].
510struct ContainsAllMatcher<Tup> {
511    matchers: Tup,
512}
513
514impl<C, Tup> Matcher<C> for ContainsAllMatcher<Tup>
515where
516    C: Sequence + ?Sized,
517    C::Item: fmt::Debug,
518    Tup: ContainsAll<C::Item>,
519{
520    fn check(&self, actual: &C) -> MatchResult {
521        let items = actual.sequence_items();
522        match self.matchers.first_unsatisfied(&items) {
523            None => MatchResult::pass(),
524            Some(unsatisfied) => MatchResult::fail(Mismatch::new(
525                Description::labeled("a sequence containing an item that is", unsatisfied),
526                format!("{items:?}"),
527            )),
528        }
529    }
530
531    fn description(&self) -> Description {
532        Description::labeled("a sequence containing all of", self.matchers.describe())
533    }
534}
535
536/// Matches a sequence in which every matcher in the tuple is satisfied by some
537/// item (each matcher independently; one item may satisfy several).
538///
539/// On failure the error names the first matcher that no item satisfied.
540///
541/// ```
542/// use test_better_core::TestResult;
543/// use test_better_matchers::{contains_all, eq, check, gt};
544///
545/// fn main() -> TestResult {
546///     check!(vec![1, 2, 3]).satisfies(contains_all((eq(1), gt(2))))?;
547///     Ok(())
548/// }
549/// ```
550#[must_use]
551pub fn contains_all<C, Tup>(matchers: Tup) -> impl Matcher<C>
552where
553    C: Sequence + ?Sized,
554    C::Item: fmt::Debug,
555    Tup: ContainsAll<C::Item>,
556{
557    ContainsAllMatcher { matchers }
558}
559
560#[cfg(test)]
561mod tests {
562    use std::collections::{BTreeSet, HashSet, VecDeque};
563
564    use test_better_core::{OrFail, TestResult};
565
566    use super::*;
567    use crate::{check, eq, gt, is_false, is_true, lt};
568
569    #[test]
570    fn have_len_matches_the_exact_length() -> TestResult {
571        check!(have_len(3).check(&vec![1, 2, 3]).matched).satisfies(is_true())?;
572        let failure = have_len(3)
573            .check(&vec![1, 2])
574            .failure
575            .or_fail_with("length 2 is not 3")?;
576        check!(failure.expected.to_string()).satisfies(eq("a sequence of length 3".to_string()))?;
577        check!(failure.actual).satisfies(eq("a sequence of length 2".to_string()))?;
578        Ok(())
579    }
580
581    #[test]
582    fn items_collects_an_iterator_into_a_sequence() -> TestResult {
583        // A lazy iterator wrapped by `items` behaves like a `Vec` under every
584        // collection matcher: it has a length, it indexes, and `contains` /
585        // `contains_in_order` produce the same failure shape they do on a
586        // hand-collected `Vec<T>`.
587        let lazy = (1..=3).map(|n| n * 10);
588        let collected = items(lazy);
589        check!(have_len(3).check(&collected).matched).satisfies(is_true())?;
590        check!(contains(eq(20)).check(&collected).matched).satisfies(is_true())?;
591        let failure = contains(eq(99))
592            .check(&collected)
593            .failure
594            .or_fail_with("99 is not in the iterator")?;
595        check!(failure.actual).satisfies(eq("[10, 20, 30]".to_string()))?;
596        Ok(())
597    }
598
599    #[test]
600    fn items_on_an_empty_iterator_is_empty() -> TestResult {
601        let empty: Items<i32> = items(std::iter::empty());
602        check!(is_empty().check(&empty).matched).satisfies(is_true())?;
603        Ok(())
604    }
605
606    #[test]
607    fn is_empty_and_is_not_empty_are_opposites() -> TestResult {
608        check!(is_empty().check(&Vec::<i32>::new()).matched).satisfies(is_true())?;
609        check!(is_empty().check(&vec![1]).matched).satisfies(is_false())?;
610        check!(is_not_empty().check(&vec![1]).matched).satisfies(is_true())?;
611        check!(is_not_empty().check(&Vec::<i32>::new()).matched).satisfies(is_false())?;
612        Ok(())
613    }
614
615    #[test]
616    fn contains_finds_a_matching_item() -> TestResult {
617        check!(contains(eq(2)).check(&vec![1, 2, 3]).matched).satisfies(is_true())?;
618        let failure = contains(eq(9))
619            .check(&vec![1, 2, 3])
620            .failure
621            .or_fail_with("9 is not in the sequence")?;
622        check!(failure.actual).satisfies(eq("[1, 2, 3]".to_string()))?;
623        Ok(())
624    }
625
626    #[test]
627    fn every_names_the_index_of_the_first_failure() -> TestResult {
628        check!(every(gt(0)).check(&vec![1, 2, 3]).matched).satisfies(is_true())?;
629        let failure = every(gt(0))
630            .check(&vec![1, 2, -1, 4])
631            .failure
632            .or_fail_with("-1 is not greater than 0")?;
633        check!(failure.actual.contains("index 2")).satisfies(is_true())?;
634        Ok(())
635    }
636
637    #[test]
638    fn at_least_one_matches_when_some_item_does() -> TestResult {
639        check!(at_least_one(gt(2)).check(&vec![1, 2, 3]).matched).satisfies(is_true())?;
640        check!(at_least_one(gt(9)).check(&vec![1, 2, 3]).matched).satisfies(is_false())?;
641        Ok(())
642    }
643
644    #[test]
645    fn contains_in_order_respects_order_but_not_adjacency() -> TestResult {
646        check!(
647            contains_in_order([eq(2), eq(4)])
648                .check(&vec![1, 2, 3, 4])
649                .matched
650        )
651        .satisfies(is_true())?;
652        let failure = contains_in_order([eq(4), eq(2)])
653            .check(&vec![1, 2, 3, 4])
654            .failure
655            .or_fail_with("2 does not come after 4")?;
656        check!(failure.actual.contains("matcher at index 1")).satisfies(is_true())?;
657        Ok(())
658    }
659
660    #[test]
661    fn contains_all_requires_every_matcher_to_be_satisfied() -> TestResult {
662        check!(contains_all((eq(1), gt(2))).check(&vec![1, 2, 3]).matched).satisfies(is_true())?;
663        let failure = contains_all((eq(1), gt(9)))
664            .check(&vec![1, 2, 3])
665            .failure
666            .or_fail_with("nothing is greater than 9")?;
667        check!(failure.expected.to_string().contains("greater than 9")).satisfies(is_true())?;
668        Ok(())
669    }
670
671    #[test]
672    fn collection_matchers_work_across_collection_types() -> TestResult {
673        let deque: VecDeque<i32> = VecDeque::from(vec![1, 2, 3]);
674        check!(have_len(3).check(&deque).matched).satisfies(is_true())?;
675
676        let btree: BTreeSet<i32> = BTreeSet::from([1, 2, 3]);
677        check!(contains(eq(2)).check(&btree).matched).satisfies(is_true())?;
678
679        let set: HashSet<i32> = HashSet::from([1, 2, 3]);
680        check!(every(gt(0)).check(&set).matched).satisfies(is_true())?;
681
682        let slice: &[i32] = &[10, 20, 30];
683        check!(contains_in_order([eq(10), eq(30)]).check(&slice).matched).satisfies(is_true())?;
684
685        let array = [1, 2, 3];
686        check!(every(lt(4)).check(&array).matched).satisfies(is_true())?;
687        Ok(())
688    }
689}