Skip to main content

test_better_matchers/
matcher.rs

1//! The [`Matcher`] trait and its result types, [`MatchResult`] and
2//! [`Mismatch`].
3//!
4//! A matcher is a reusable expectation: it inspects a borrowed value and
5//! reports, in structured form, whether the value met the expectation and — if
6//! not — what was expected, what was found, and an optional diff. The `check!`
7//! macro turns that structured result into a [`TestError`].
8//!
9//! [`TestError`]: test_better_core::TestError
10
11use crate::description::Description;
12
13/// A reusable expectation about a value of type `T`.
14///
15/// `T` is `?Sized` so matchers can target unsized values directly (`str`,
16/// `[u8]`) without forcing the caller to borrow through a reference type.
17pub trait Matcher<T: ?Sized> {
18    /// Checks `actual` against this matcher's expectation.
19    fn check(&self, actual: &T) -> MatchResult;
20
21    /// Describes what this matcher expects, for use in failure output and in
22    /// combinator descriptions.
23    fn description(&self) -> Description;
24}
25
26/// The structured outcome of [`Matcher::check`].
27///
28/// # Invariant
29///
30/// `matched` and `failure` always disagree: `matched == failure.is_none()`.
31/// Construct values through [`MatchResult::pass`] and [`MatchResult::fail`]
32/// rather than building the struct literal, so the invariant cannot be broken.
33#[derive(Debug, Clone)]
34pub struct MatchResult {
35    /// Whether the value met the expectation.
36    pub matched: bool,
37    /// The mismatch detail, present exactly when `matched` is `false`.
38    pub failure: Option<Mismatch>,
39}
40
41impl MatchResult {
42    /// The value met the expectation.
43    #[must_use]
44    pub fn pass() -> Self {
45        Self {
46            matched: true,
47            failure: None,
48        }
49    }
50
51    /// The value did not meet the expectation; `mismatch` explains why.
52    #[must_use]
53    pub fn fail(mismatch: Mismatch) -> Self {
54        Self {
55            matched: false,
56            failure: Some(mismatch),
57        }
58    }
59}
60
61/// Why a value failed a matcher: what was expected, what was found, and an
62/// optional diff between the two.
63#[derive(Debug, Clone)]
64pub struct Mismatch {
65    /// The matcher's expectation, as a composable [`Description`].
66    pub expected: Description,
67    /// The `Debug` rendering of the actual value.
68    pub actual: String,
69    /// An optional pre-rendered diff between expected and actual. `None` when
70    /// no diff is available.
71    pub diff: Option<String>,
72}
73
74impl Mismatch {
75    /// A mismatch with no diff.
76    #[must_use]
77    pub fn new(expected: Description, actual: impl Into<String>) -> Self {
78        Self {
79            expected,
80            actual: actual.into(),
81            diff: None,
82        }
83    }
84
85    /// Attaches a pre-rendered diff, consuming and returning `self`.
86    #[must_use]
87    pub fn with_diff(mut self, diff: impl Into<String>) -> Self {
88        self.diff = Some(diff.into());
89        self
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use test_better_core::{OrFail, TestResult};
96
97    use super::*;
98    use crate::{eq, check, is_false, is_true};
99
100    #[test]
101    fn pass_has_no_failure() -> TestResult {
102        let result = MatchResult::pass();
103        check!(result.matched).satisfies(is_true())?;
104        check!(result.failure.is_none()).satisfies(is_true())?;
105        Ok(())
106    }
107
108    #[test]
109    fn fail_carries_the_mismatch() -> TestResult {
110        let mismatch = Mismatch::new(Description::text("equal to 4"), "5");
111        let result = MatchResult::fail(mismatch);
112        check!(result.matched).satisfies(is_false())?;
113        let failure = result.failure.or_fail_with("fail() stores the mismatch")?;
114        check!(failure.expected.to_string()).satisfies(eq("equal to 4".to_string()))?;
115        check!(failure.actual).satisfies(eq("5".to_string()))?;
116        check!(failure.diff.is_none()).satisfies(is_true())?;
117        Ok(())
118    }
119
120    #[test]
121    fn mismatch_with_diff_stores_the_diff() -> TestResult {
122        let mismatch = Mismatch::new(Description::text("the file"), "other").with_diff("- a\n+ b");
123        check!(mismatch.diff.as_deref()).satisfies(eq(Some("- a\n+ b")))?;
124        Ok(())
125    }
126}