Skip to main content

test_better_matchers/
combinators.rs

1//! Logical combinators: [`not`], [`all_of`], and [`any_of`].
2//!
3//! These take other matchers and build a compound matcher out of them. `not`
4//! inverts a single matcher; `all_of` and `any_of` take a *tuple* of matchers
5//! (arities 2 through 8) and require, respectively, that every one or at least
6//! one of them holds. Each combinator's [`Description`] is built from its
7//! children's, through the `!`/`and`/`or` combinators on [`Description`].
8
9use std::fmt;
10
11use crate::description::Description;
12use crate::matcher::{MatchResult, Matcher, Mismatch};
13
14/// The matcher behind [`not`]: inverts the wrapped matcher.
15struct NotMatcher<M> {
16    inner: M,
17}
18
19impl<T, M> Matcher<T> for NotMatcher<M>
20where
21    T: ?Sized + fmt::Debug,
22    M: Matcher<T>,
23{
24    fn check(&self, actual: &T) -> MatchResult {
25        if self.inner.check(actual).matched {
26            // The inner matcher matched, so `not` fails. The inner pass
27            // carried no `Mismatch`, hence no rendered actual; render it here,
28            // which is why `not` needs `T: Debug`.
29            MatchResult::fail(Mismatch::new(self.description(), format!("{actual:?}")))
30        } else {
31            MatchResult::pass()
32        }
33    }
34
35    fn description(&self) -> Description {
36        !self.inner.description()
37    }
38}
39
40/// Matches when `matcher` does *not* match.
41///
42/// Negating a matcher is the composable alternative to
43/// [`violates`](crate::Subject::violates): `not` is itself a matcher, so it nests
44/// inside other combinators (`all_of((not(eq(0)), lt(100)))`).
45///
46/// ```
47/// use test_better_core::TestResult;
48/// use test_better_matchers::{eq, check, not};
49///
50/// fn main() -> TestResult {
51///     check!(5).satisfies(not(eq(4)))?;
52///     check!(4).violates(not(eq(4)))?;
53///     Ok(())
54/// }
55/// ```
56#[must_use]
57pub fn not<T, M>(matcher: M) -> impl Matcher<T>
58where
59    T: ?Sized + fmt::Debug,
60    M: Matcher<T>,
61{
62    NotMatcher { inner: matcher }
63}
64
65/// A tuple of matchers, all targeting the same type `T`.
66///
67/// Implemented for tuples of arity 2 through 8 by a macro in this module; you
68/// do not implement it yourself. It is the input to [`all_of`] and [`any_of`],
69/// which interpret the tuple under conjunction and disjunction respectively.
70pub trait MatcherTuple<T: ?Sized> {
71    /// Every matcher in the tuple must match. Returns the first sub-matcher's
72    /// failure, so the error pinpoints which expectation broke.
73    fn check_all(&self, actual: &T) -> MatchResult;
74
75    /// At least one matcher in the tuple must match. When none do, the failure
76    /// describes the whole disjunction.
77    fn check_any(&self, actual: &T) -> MatchResult;
78
79    /// The conjunction (`a and b and ...`) of the tuple's descriptions.
80    fn describe_all(&self) -> Description;
81
82    /// The disjunction (`a or b or ...`) of the tuple's descriptions.
83    fn describe_any(&self) -> Description;
84}
85
86/// Implements [`MatcherTuple`] for one tuple arity. The first type parameter is
87/// split out from the rest so the description fold and the `check_any` actual
88/// capture have a guaranteed first element without an `unwrap`.
89macro_rules! impl_matcher_tuple {
90    ($first:ident, $($rest:ident),+) => {
91        #[allow(non_snake_case)]
92        impl<T, $first, $($rest,)+> MatcherTuple<T> for ($first, $($rest,)+)
93        where
94            T: ?Sized,
95            $first: Matcher<T>,
96            $($rest: Matcher<T>,)+
97        {
98            fn check_all(&self, actual: &T) -> MatchResult {
99                let ($first, $($rest,)+) = self;
100                if let Some(mismatch) = $first.check(actual).failure {
101                    return MatchResult::fail(mismatch);
102                }
103                $(
104                    if let Some(mismatch) = $rest.check(actual).failure {
105                        return MatchResult::fail(mismatch);
106                    }
107                )+
108                MatchResult::pass()
109            }
110
111            fn check_any(&self, actual: &T) -> MatchResult {
112                let ($first, $($rest,)+) = self;
113                let first_actual = match $first.check(actual).failure {
114                    None => return MatchResult::pass(),
115                    Some(mismatch) => mismatch.actual,
116                };
117                $(
118                    if $rest.check(actual).matched {
119                        return MatchResult::pass();
120                    }
121                )+
122                MatchResult::fail(Mismatch::new(self.describe_any(), first_actual))
123            }
124
125            fn describe_all(&self) -> Description {
126                let ($first, $($rest,)+) = self;
127                let desc = $first.description();
128                $( let desc = desc.and($rest.description()); )+
129                desc
130            }
131
132            fn describe_any(&self) -> Description {
133                let ($first, $($rest,)+) = self;
134                let desc = $first.description();
135                $( let desc = desc.or($rest.description()); )+
136                desc
137            }
138        }
139    };
140}
141
142impl_matcher_tuple!(M1, M2);
143impl_matcher_tuple!(M1, M2, M3);
144impl_matcher_tuple!(M1, M2, M3, M4);
145impl_matcher_tuple!(M1, M2, M3, M4, M5);
146impl_matcher_tuple!(M1, M2, M3, M4, M5, M6);
147impl_matcher_tuple!(M1, M2, M3, M4, M5, M6, M7);
148impl_matcher_tuple!(M1, M2, M3, M4, M5, M6, M7, M8);
149
150/// The matcher behind [`all_of`]: conjunction over a tuple of matchers.
151struct AllOfMatcher<Tup> {
152    matchers: Tup,
153}
154
155impl<T, Tup> Matcher<T> for AllOfMatcher<Tup>
156where
157    T: ?Sized,
158    Tup: MatcherTuple<T>,
159{
160    fn check(&self, actual: &T) -> MatchResult {
161        self.matchers.check_all(actual)
162    }
163
164    fn description(&self) -> Description {
165        self.matchers.describe_all()
166    }
167}
168
169/// Matches when *every* matcher in the tuple matches.
170///
171/// On failure the error is the first sub-matcher's, so it names the specific
172/// expectation that broke rather than the whole conjunction.
173///
174/// ```
175/// use test_better_core::TestResult;
176/// use test_better_matchers::{all_of, check, gt, lt};
177///
178/// fn main() -> TestResult {
179///     check!(50).satisfies(all_of((gt(0), lt(100))))?;
180///     Ok(())
181/// }
182/// ```
183#[must_use]
184pub fn all_of<T, Tup>(matchers: Tup) -> impl Matcher<T>
185where
186    T: ?Sized,
187    Tup: MatcherTuple<T>,
188{
189    AllOfMatcher { matchers }
190}
191
192/// The matcher behind [`any_of`]: disjunction over a tuple of matchers.
193struct AnyOfMatcher<Tup> {
194    matchers: Tup,
195}
196
197impl<T, Tup> Matcher<T> for AnyOfMatcher<Tup>
198where
199    T: ?Sized,
200    Tup: MatcherTuple<T>,
201{
202    fn check(&self, actual: &T) -> MatchResult {
203        self.matchers.check_any(actual)
204    }
205
206    fn description(&self) -> Description {
207        self.matchers.describe_any()
208    }
209}
210
211/// Matches when *at least one* matcher in the tuple matches.
212///
213/// When none match, the failure describes the whole disjunction (`a or b or
214/// ...`).
215///
216/// ```
217/// use test_better_core::TestResult;
218/// use test_better_matchers::{any_of, eq, check};
219///
220/// fn main() -> TestResult {
221///     check!(7).satisfies(any_of((eq(7), eq(8), eq(9))))?;
222///     Ok(())
223/// }
224/// ```
225#[must_use]
226pub fn any_of<T, Tup>(matchers: Tup) -> impl Matcher<T>
227where
228    T: ?Sized,
229    Tup: MatcherTuple<T>,
230{
231    AnyOfMatcher { matchers }
232}
233
234#[cfg(test)]
235mod tests {
236    use test_better_core::{OrFail, TestResult};
237
238    use super::*;
239    use crate::{check, eq, gt, is_false, is_true, lt};
240
241    #[test]
242    fn not_inverts_the_inner_matcher() -> TestResult {
243        check!(not(eq(4)).check(&5).matched).satisfies(is_true())?;
244        check!(not(eq(4)).check(&4).matched).satisfies(is_false())?;
245        Ok(())
246    }
247
248    #[test]
249    fn not_failure_negates_the_description_and_renders_the_actual() -> TestResult {
250        let failure = not(eq(4))
251            .check(&4)
252            .failure
253            .or_fail_with("4 does match eq(4), so not(eq(4)) fails")?;
254        check!(failure.expected.to_string()).satisfies(eq("not equal to 4".to_string()))?;
255        check!(failure.actual).satisfies(eq("4".to_string()))?;
256        Ok(())
257    }
258
259    #[test]
260    fn all_of_passes_when_every_matcher_matches() -> TestResult {
261        check!(all_of((gt(0), lt(100))).check(&50).matched).satisfies(is_true())?;
262        Ok(())
263    }
264
265    #[test]
266    fn all_of_fails_with_the_first_failing_sub_matcher() -> TestResult {
267        let failure = all_of((gt(0), lt(100)))
268            .check(&150)
269            .failure
270            .or_fail_with("150 is not less than 100")?;
271        check!(failure.expected.to_string()).satisfies(eq("less than 100".to_string()))?;
272        check!(failure.actual).satisfies(eq("150".to_string()))?;
273        Ok(())
274    }
275
276    #[test]
277    fn all_of_describes_itself_as_a_conjunction() -> TestResult {
278        let description = all_of((gt(0), lt(100))).description();
279        check!(description.to_string())
280            .satisfies(eq("greater than 0 and less than 100".to_string()))?;
281        Ok(())
282    }
283
284    #[test]
285    fn any_of_passes_when_at_least_one_matcher_matches() -> TestResult {
286        check!(any_of((eq(7), eq(8), eq(9))).check(&8).matched).satisfies(is_true())?;
287        Ok(())
288    }
289
290    #[test]
291    fn any_of_fails_when_no_matcher_matches() -> TestResult {
292        let failure = any_of((eq(7), eq(8), eq(9)))
293            .check(&1)
294            .failure
295            .or_fail_with("1 is none of 7, 8, 9")?;
296        check!(failure.expected.to_string())
297            .satisfies(eq("equal to 7 or equal to 8 or equal to 9".to_string()))?;
298        check!(failure.actual).satisfies(eq("1".to_string()))?;
299        Ok(())
300    }
301
302    #[test]
303    fn combinators_nest() -> TestResult {
304        // `not` is itself a matcher, so it composes inside `all_of`.
305        check!(all_of((not(eq(0)), lt(100))).check(&50).matched).satisfies(is_true())?;
306        check!(all_of((not(eq(0)), lt(100))).check(&0).matched).satisfies(is_false())?;
307        Ok(())
308    }
309
310    #[test]
311    fn all_of_supports_arity_eight() -> TestResult {
312        let matcher = all_of((gt(0), lt(100), gt(1), lt(99), gt(2), lt(98), gt(3), lt(97)));
313        check!(matcher.check(&50).matched).satisfies(is_true())?;
314        Ok(())
315    }
316}