Skip to main content

test_better_matchers/
primitives.rs

1//! Primitive matchers: equality, ordering, and boolean checks.
2//!
3//! These are the leaves of the matcher library. They compare the actual value
4//! against a stored expected value (`eq`, `lt`, ...) or against a fixed truth
5//! (`is_true`, `is_false`). Combinators build on top of them.
6
7use std::fmt;
8
9use crate::description::Description;
10use crate::matcher::{MatchResult, Matcher, Mismatch};
11
12/// Generates a comparison matcher: a `struct` holding the expected value and a
13/// [`Matcher`] impl that compares with `$op` and describes itself with
14/// `$describe`.
15macro_rules! comparison_matcher {
16    ($matcher:ident, $bound:ident, $op:tt, $describe:literal) => {
17        struct $matcher<T> {
18            expected: T,
19        }
20
21        impl<T> Matcher<T> for $matcher<T>
22        where
23            T: $bound + fmt::Debug,
24        {
25            fn check(&self, actual: &T) -> MatchResult {
26                if *actual $op self.expected {
27                    MatchResult::pass()
28                } else {
29                    MatchResult::fail(Mismatch::new(self.description(), format!("{actual:?}")))
30                }
31            }
32
33            fn description(&self) -> Description {
34                Description::text(format!(concat!($describe, " {:?}"), self.expected))
35            }
36        }
37    };
38}
39
40comparison_matcher!(NeMatcher, PartialEq, !=, "not equal to");
41comparison_matcher!(LtMatcher, PartialOrd, <, "less than");
42comparison_matcher!(LeMatcher, PartialOrd, <=, "less than or equal to");
43comparison_matcher!(GtMatcher, PartialOrd, >, "greater than");
44comparison_matcher!(GeMatcher, PartialOrd, >=, "greater than or equal to");
45
46/// The matcher behind [`eq`]. Unlike the other comparison matchers it can
47/// attach a structural diff: when the expected and actual values' pretty
48/// (`{:#?}`) representations span multiple lines, a line-oriented diff is the
49/// readable way to show what changed.
50struct EqMatcher<T> {
51    expected: T,
52}
53
54impl<T> Matcher<T> for EqMatcher<T>
55where
56    T: PartialEq + fmt::Debug,
57{
58    fn check(&self, actual: &T) -> MatchResult {
59        if *actual == self.expected {
60            return MatchResult::pass();
61        }
62        let mut mismatch = Mismatch::new(self.description(), format!("{actual:?}"));
63        if let Some(diff) =
64            multi_line_diff(&format!("{:#?}", self.expected), &format!("{actual:#?}"))
65        {
66            mismatch = mismatch.with_diff(diff);
67        }
68        MatchResult::fail(mismatch)
69    }
70
71    fn description(&self) -> Description {
72        Description::text(format!("equal to {:?}", self.expected))
73    }
74}
75
76/// A line-oriented diff of two pretty-printed values, but only when at least
77/// one of them actually spans multiple lines: a diff of two single-line values
78/// is just noise next to the `expected:`/`actual:` lines.
79///
80/// With the `diff` feature off this is always `None`, so `eq` still works, it
81/// just never carries a diff.
82#[cfg(feature = "diff")]
83fn multi_line_diff(expected: &str, actual: &str) -> Option<String> {
84    if expected.contains('\n') || actual.contains('\n') {
85        Some(crate::diff::diff_lines(expected, actual))
86    } else {
87        None
88    }
89}
90
91#[cfg(not(feature = "diff"))]
92fn multi_line_diff(_expected: &str, _actual: &str) -> Option<String> {
93    None
94}
95
96/// Matches a value equal to `expected`.
97///
98/// On a mismatch where the values' pretty representations are multi-line (a
99/// struct, a collection), the failure carries a line-oriented diff.
100///
101/// ```
102/// use test_better_core::TestResult;
103/// use test_better_matchers::{eq, check};
104///
105/// fn main() -> TestResult {
106///     check!(2 + 2).satisfies(eq(4))?;
107///     check!(5).violates(eq(4))?;
108///     Ok(())
109/// }
110/// ```
111#[must_use]
112pub fn eq<T>(expected: T) -> impl Matcher<T>
113where
114    T: PartialEq + fmt::Debug,
115{
116    EqMatcher { expected }
117}
118
119/// Matches a value not equal to `expected`.
120///
121/// ```
122/// use test_better_core::TestResult;
123/// use test_better_matchers::{ne, check};
124///
125/// fn main() -> TestResult {
126///     check!(5).satisfies(ne(4))?;
127///     check!(4).violates(ne(4))?;
128///     Ok(())
129/// }
130/// ```
131#[must_use]
132pub fn ne<T>(expected: T) -> impl Matcher<T>
133where
134    T: PartialEq + fmt::Debug,
135{
136    NeMatcher { expected }
137}
138
139/// Matches a value strictly less than `expected`.
140///
141/// ```
142/// use test_better_core::TestResult;
143/// use test_better_matchers::{lt, check};
144///
145/// fn main() -> TestResult {
146///     check!(9).satisfies(lt(10))?;
147///     check!(10).violates(lt(10))?;
148///     Ok(())
149/// }
150/// ```
151#[must_use]
152pub fn lt<T>(expected: T) -> impl Matcher<T>
153where
154    T: PartialOrd + fmt::Debug,
155{
156    LtMatcher { expected }
157}
158
159/// Matches a value less than or equal to `expected`.
160///
161/// ```
162/// use test_better_core::TestResult;
163/// use test_better_matchers::{le, check};
164///
165/// fn main() -> TestResult {
166///     check!(10).satisfies(le(10))?;
167///     check!(11).violates(le(10))?;
168///     Ok(())
169/// }
170/// ```
171#[must_use]
172pub fn le<T>(expected: T) -> impl Matcher<T>
173where
174    T: PartialOrd + fmt::Debug,
175{
176    LeMatcher { expected }
177}
178
179/// Matches a value strictly greater than `expected`.
180///
181/// ```
182/// use test_better_core::TestResult;
183/// use test_better_matchers::{gt, check};
184///
185/// fn main() -> TestResult {
186///     check!(1).satisfies(gt(0))?;
187///     check!(0).violates(gt(0))?;
188///     Ok(())
189/// }
190/// ```
191#[must_use]
192pub fn gt<T>(expected: T) -> impl Matcher<T>
193where
194    T: PartialOrd + fmt::Debug,
195{
196    GtMatcher { expected }
197}
198
199/// Matches a value greater than or equal to `expected`.
200///
201/// ```
202/// use test_better_core::TestResult;
203/// use test_better_matchers::{ge, check};
204///
205/// fn main() -> TestResult {
206///     check!(0).satisfies(ge(0))?;
207///     check!(-1).violates(ge(0))?;
208///     Ok(())
209/// }
210/// ```
211#[must_use]
212pub fn ge<T>(expected: T) -> impl Matcher<T>
213where
214    T: PartialOrd + fmt::Debug,
215{
216    GeMatcher { expected }
217}
218
219/// A matcher for a fixed boolean truth, behind [`is_true`] and [`is_false`].
220struct BoolMatcher {
221    expected: bool,
222}
223
224impl Matcher<bool> for BoolMatcher {
225    fn check(&self, actual: &bool) -> MatchResult {
226        if *actual == self.expected {
227            MatchResult::pass()
228        } else {
229            MatchResult::fail(Mismatch::new(self.description(), format!("{actual:?}")))
230        }
231    }
232
233    fn description(&self) -> Description {
234        Description::text(if self.expected { "true" } else { "false" })
235    }
236}
237
238/// Matches `true`.
239///
240/// ```
241/// use test_better_core::TestResult;
242/// use test_better_matchers::{is_true, check};
243///
244/// fn main() -> TestResult {
245///     check!(1 == 1).satisfies(is_true())?;
246///     check!(false).violates(is_true())?;
247///     Ok(())
248/// }
249/// ```
250#[must_use]
251pub fn is_true() -> impl Matcher<bool> {
252    BoolMatcher { expected: true }
253}
254
255/// Matches `false`.
256///
257/// ```
258/// use test_better_core::TestResult;
259/// use test_better_matchers::{is_false, check};
260///
261/// fn main() -> TestResult {
262///     check!(1 == 2).satisfies(is_false())?;
263///     check!(true).violates(is_false())?;
264///     Ok(())
265/// }
266/// ```
267#[must_use]
268pub fn is_false() -> impl Matcher<bool> {
269    BoolMatcher { expected: false }
270}
271
272#[cfg(test)]
273mod tests {
274    use test_better_core::{OrFail, TestResult};
275
276    use super::*;
277    use crate::{check, eq, is_true};
278
279    #[test]
280    fn eq_passes_and_fails_with_rendered_mismatch() -> TestResult {
281        check!(eq(4).check(&4).matched).satisfies(is_true())?;
282        let failure = eq(4).check(&5).failure.or_fail_with("5 is not 4")?;
283        check!(failure.expected.to_string()).satisfies(eq("equal to 4".to_string()))?;
284        check!(failure.actual).satisfies(eq("5".to_string()))?;
285        Ok(())
286    }
287
288    #[test]
289    fn eq_omits_a_diff_for_single_line_values() -> TestResult {
290        let failure = eq(4).check(&5).failure.or_fail_with("5 is not 4")?;
291        check!(failure.diff.is_none()).satisfies(is_true())?;
292        Ok(())
293    }
294
295    #[cfg(feature = "diff")]
296    #[test]
297    fn eq_attaches_a_diff_when_the_pretty_repr_is_multi_line() -> TestResult {
298        let failure = eq(vec![1, 2, 3])
299            .check(&vec![1, 2, 4])
300            .failure
301            .or_fail_with("the vectors differ")?;
302        let diff = failure
303            .diff
304            .or_fail_with("multi-line pretty reprs get a diff")?;
305        check!(diff.contains("-    3,")).satisfies(is_true())?;
306        check!(diff.contains("+    4,")).satisfies(is_true())?;
307        Ok(())
308    }
309
310    #[test]
311    fn ne_passes_and_fails_with_rendered_mismatch() -> TestResult {
312        check!(ne(4).check(&5).matched).satisfies(is_true())?;
313        let failure = ne(4).check(&4).failure.or_fail_with("4 is equal to 4")?;
314        check!(failure.expected.to_string()).satisfies(eq("not equal to 4".to_string()))?;
315        check!(failure.actual).satisfies(eq("4".to_string()))?;
316        Ok(())
317    }
318
319    #[test]
320    fn lt_passes_and_fails_with_rendered_mismatch() -> TestResult {
321        check!(lt(10).check(&9).matched).satisfies(is_true())?;
322        let failure = lt(10).check(&10).failure.or_fail_with("10 is not < 10")?;
323        check!(failure.expected.to_string()).satisfies(eq("less than 10".to_string()))?;
324        check!(failure.actual).satisfies(eq("10".to_string()))?;
325        Ok(())
326    }
327
328    #[test]
329    fn le_passes_and_fails_with_rendered_mismatch() -> TestResult {
330        check!(le(10).check(&10).matched).satisfies(is_true())?;
331        let failure = le(10).check(&11).failure.or_fail_with("11 is not <= 10")?;
332        check!(failure.expected.to_string())
333            .satisfies(eq("less than or equal to 10".to_string()))?;
334        check!(failure.actual).satisfies(eq("11".to_string()))?;
335        Ok(())
336    }
337
338    #[test]
339    fn gt_passes_and_fails_with_rendered_mismatch() -> TestResult {
340        check!(gt(0).check(&1).matched).satisfies(is_true())?;
341        let failure = gt(0).check(&0).failure.or_fail_with("0 is not > 0")?;
342        check!(failure.expected.to_string()).satisfies(eq("greater than 0".to_string()))?;
343        check!(failure.actual).satisfies(eq("0".to_string()))?;
344        Ok(())
345    }
346
347    #[test]
348    fn ge_passes_and_fails_with_rendered_mismatch() -> TestResult {
349        check!(ge(0).check(&0).matched).satisfies(is_true())?;
350        let failure = ge(0).check(&-1).failure.or_fail_with("-1 is not >= 0")?;
351        check!(failure.expected.to_string())
352            .satisfies(eq("greater than or equal to 0".to_string()))?;
353        check!(failure.actual).satisfies(eq("-1".to_string()))?;
354        Ok(())
355    }
356
357    #[test]
358    fn is_true_passes_and_fails_with_rendered_mismatch() -> TestResult {
359        check!(is_true().check(&true).matched).satisfies(is_true())?;
360        let failure = is_true()
361            .check(&false)
362            .failure
363            .or_fail_with("false is not true")?;
364        check!(failure.expected.to_string()).satisfies(eq("true".to_string()))?;
365        check!(failure.actual).satisfies(eq("false".to_string()))?;
366        Ok(())
367    }
368
369    #[test]
370    fn is_false_passes_and_fails_with_rendered_mismatch() -> TestResult {
371        check!(is_false().check(&false).matched).satisfies(is_true())?;
372        let failure = is_false()
373            .check(&true)
374            .failure
375            .or_fail_with("true is not false")?;
376        check!(failure.expected.to_string()).satisfies(eq("false".to_string()))?;
377        check!(failure.actual).satisfies(eq("true".to_string()))?;
378        Ok(())
379    }
380}