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 `expect!`
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, expect, is_false, is_true};
99
100 #[test]
101 fn pass_has_no_failure() -> TestResult {
102 let result = MatchResult::pass();
103 expect!(result.matched).to(is_true())?;
104 expect!(result.failure.is_none()).to(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 expect!(result.matched).to(is_false())?;
113 let failure = result.failure.or_fail_with("fail() stores the mismatch")?;
114 expect!(failure.expected.to_string()).to(eq("equal to 4".to_string()))?;
115 expect!(failure.actual).to(eq("5".to_string()))?;
116 expect!(failure.diff.is_none()).to(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 expect!(mismatch.diff.as_deref()).to(eq(Some("- a\n+ b")))?;
124 Ok(())
125 }
126}