test_better_matchers/predicate.rs
1//! The [`predicate`] escape hatch: a matcher built from an arbitrary
2//! Boolean-returning closure.
3//!
4//! When no standard matcher fits, `predicate` wraps a `Fn(&T) -> bool`. It
5//! takes a `name` so a failure reads `expected: even` rather than the useless
6//! `expected: <closure>`.
7
8use std::fmt;
9use std::marker::PhantomData;
10
11use crate::description::Description;
12use crate::matcher::{MatchResult, Matcher, Mismatch};
13
14/// The matcher behind [`predicate`].
15struct PredicateMatcher<T, F> {
16 name: &'static str,
17 pred: F,
18 // `T` appears only behind `&T` in `F`'s signature, not in a field, so it
19 // would otherwise be an unconstrained type parameter on the struct.
20 _marker: PhantomData<fn(&T)>,
21}
22
23impl<T, F> Matcher<T> for PredicateMatcher<T, F>
24where
25 T: fmt::Debug,
26 F: Fn(&T) -> bool,
27{
28 fn check(&self, actual: &T) -> MatchResult {
29 if (self.pred)(actual) {
30 MatchResult::pass()
31 } else {
32 MatchResult::fail(Mismatch::new(self.description(), format!("{actual:?}")))
33 }
34 }
35
36 fn description(&self) -> Description {
37 Description::text(self.name)
38 }
39}
40
41/// Matches a value for which `pred` returns `true`.
42///
43/// The escape hatch for when no standard matcher fits. `name` is what the
44/// failure reports as the expectation, so give it a readable one: a closure has
45/// no name of its own, and a failure that says `expected: <closure>` helps no
46/// one.
47///
48/// The matcher pairs naturally with [`Subject::satisfies`](crate::Subject::satisfies),
49/// reading as "x satisfies the predicate `<name>`".
50///
51/// ```
52/// use test_better_core::{OrFail, TestResult};
53/// use test_better_matchers::{Matcher, check, eq, predicate};
54///
55/// fn main() -> TestResult {
56/// check!(4).satisfies(predicate("an even number", |n: &i32| n % 2 == 0))?;
57///
58/// // The `name` is what a failure reports, not `<closure>`.
59/// let failure = predicate("an even number", |n: &i32| n % 2 == 0)
60/// .check(&3)
61/// .failure
62/// .or_fail_with("3 is not even")?;
63/// check!(failure.expected.to_string()).satisfies(eq("an even number".to_string()))?;
64/// Ok(())
65/// }
66/// ```
67#[must_use]
68pub fn predicate<T, F>(name: &'static str, pred: F) -> impl Matcher<T>
69where
70 T: fmt::Debug,
71 F: Fn(&T) -> bool,
72{
73 PredicateMatcher {
74 name,
75 pred,
76 _marker: PhantomData,
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use test_better_core::{OrFail, TestResult};
83
84 use super::*;
85 use crate::{check, eq, is_false, is_true};
86
87 #[test]
88 fn predicate_runs_the_closure() -> TestResult {
89 check!(predicate("even", |n: &i32| n % 2 == 0).check(&4).matched).satisfies(is_true())?;
90 check!(predicate("even", |n: &i32| n % 2 == 0).check(&3).matched).satisfies(is_false())?;
91 Ok(())
92 }
93
94 #[test]
95 fn predicate_failure_reports_the_name_not_the_closure() -> TestResult {
96 let failure = predicate("a positive number", |n: &i32| *n > 0)
97 .check(&-1)
98 .failure
99 .or_fail_with("-1 is not positive")?;
100 check!(failure.expected.to_string()).satisfies(eq("a positive number".to_string()))?;
101 check!(failure.actual).satisfies(eq("-1".to_string()))?;
102 Ok(())
103 }
104}