Skip to main content

test_better_property/
property.rs

1//! The `property!` macro: a property test written as a closure.
2//!
3//! [`property!`] is a thin syntactic wrapper over [`for_all`](crate::for_all).
4//! It takes a closure with a typed binding, infers a
5//! [`Strategy`](crate::Strategy) from that type (or takes one explicitly via a
6//! `using` clause), runs the property, and turns a
7//! [`PropertyFailure`](crate::PropertyFailure) into a [`TestError`] so the call
8//! site is an ordinary `?`-returning expression.
9//!
10//! The shrunk-failure *rendering* lives in [`render_failure`]: the matcher's
11//! own failure is kept whole, and context frames naming the case count, the
12//! original failing input, and the shrunk minimal input are wrapped around it.
13//! A golden-file test (`tests/shrink_output.rs`) pins the exact output.
14
15use std::fmt::Debug;
16
17use test_better_core::{ContextFrame, ErrorKind, TestError, TestResult};
18
19use crate::{PropertyFailure, Strategy, for_all};
20
21/// Runs a property and renders any counterexample as a [`TestError`].
22///
23/// This is the function [`property!`] expands to; it is the seam between the
24/// macro's syntax and the [`for_all`] runner. It is `#[doc(hidden)]` plumbing,
25/// not part of the curated surface: write `property!(...)`, or call [`for_all`]
26/// directly for the structured [`PropertyFailure`].
27#[doc(hidden)]
28pub fn run_property<T, S, F>(strategy: S, property: F) -> TestResult
29where
30    S: Strategy<T>,
31    T: Clone + Debug,
32    F: FnMut(T) -> TestResult,
33{
34    match for_all(strategy, property) {
35        Ok(()) => Ok(()),
36        Err(failure) => Err(render_failure(failure)),
37    }
38}
39
40/// Turns the structured [`PropertyFailure`] into a rendered [`TestError`].
41///
42/// The matcher's own failure is kept whole: its message and payload are left
43/// untouched.
44/// Three context frames are wrapped around it, outermost-first: the property
45/// summary and case count, the original failing input, and the shrunk minimal
46/// input. The kind is promoted to [`ErrorKind::Property`] so the failure reads
47/// as a property failure, not a bare assertion.
48///
49/// `#[doc(hidden)]` plumbing: [`run_property`] (and so [`property!`]) call it,
50/// and the golden-file test pins its output. Callers wanting the structured
51/// failure use [`for_all`] and read [`PropertyFailure`] directly.
52#[doc(hidden)]
53pub fn render_failure<T: Debug>(failure: PropertyFailure<T>) -> TestError {
54    let PropertyFailure {
55        original,
56        shrunk,
57        failure,
58        cases,
59    } = failure;
60    let plural = if cases == 1 { "" } else { "s" };
61    let mut error = failure;
62    error.kind = ErrorKind::Property;
63    error.push_context(ContextFrame::new(format!(
64        "checking a property; it failed after {cases} generated case{plural}"
65    )));
66    error.push_context(ContextFrame::new(format!(
67        "the original failing input was {original:?}"
68    )));
69    error.push_context(ContextFrame::new(format!(
70        "the shrunk (minimal) input is {shrunk:?}"
71    )));
72    error
73}
74
75/// Checks that a property holds for every generated input.
76///
77/// `property!` takes a closure with a typed binding and a block body that
78/// returns [`TestResult`](test_better_core::TestResult), runs it against
79/// generated values, and on failure produces a `TestError` naming the shrunk
80/// counterexample. It expands to an expression, so it is the body (or the tail)
81/// of an ordinary `#[test]` function:
82///
83/// ```
84/// use test_better_core::TestResult;
85/// use test_better_matchers::{check, lt};
86/// use test_better_property::property;
87///
88/// // In a real test this is `#[test] fn doubling_stays_in_range()`.
89/// # fn main() -> TestResult {
90/// property!(|n: u8| {
91///     check!(u16::from(n) * 2).satisfies(lt(512u16))
92/// })
93/// # }
94/// ```
95///
96/// # Inferring vs. naming the strategy
97///
98/// With only a typed binding, the strategy is inferred from the type via
99/// [`any`](crate::any) (the type must be `proptest::arbitrary::Arbitrary`). To
100/// generate from a specific strategy instead, add a trailing `using` clause:
101///
102/// ```
103/// use test_better_core::TestResult;
104/// use test_better_matchers::{check, lt};
105/// use test_better_property::property;
106///
107/// # fn main() -> TestResult {
108/// // `using` names the strategy explicitly; the binding need not be annotated.
109/// property!(|n| {
110///     check!(n).satisfies(lt(100u32))
111/// } using 0u32..100)
112/// # }
113/// ```
114#[macro_export]
115macro_rules! property {
116    // Typed binding, strategy inferred from the type.
117    (| $name:ident : $ty:ty | $body:block) => {
118        $crate::run_property($crate::any::<$ty>(), |$name: $ty| $body)
119    };
120    // Typed binding, explicit strategy via a trailing `using` clause.
121    (| $name:ident : $ty:ty | $body:block using $strategy:expr) => {
122        $crate::run_property($strategy, |$name: $ty| $body)
123    };
124    // Bare binding, explicit strategy: the type comes from the strategy.
125    (| $name:ident | $body:block using $strategy:expr) => {
126        $crate::run_property($strategy, |$name| $body)
127    };
128}
129
130#[cfg(test)]
131mod tests {
132    use test_better_core::{OrFail, TestResult};
133    use test_better_matchers::{check, eq, ge, is_true, lt};
134
135    #[test]
136    fn an_inferred_strategy_property_that_holds_passes() -> TestResult {
137        // `u8` is `Arbitrary`, so the strategy is inferred from the binding.
138        property!(|n: u8| { check!(u16::from(n) + 1).satisfies(ge(1u16)) })
139    }
140
141    #[test]
142    fn a_using_clause_names_the_strategy_explicitly() -> TestResult {
143        // The binding is bare; the type comes from the `using` strategy.
144        property!(|n| {
145            check!(n).satisfies(lt(50u64))
146        } using 0u64..50)
147    }
148
149    #[test]
150    fn a_failing_property_renders_a_property_kind_error_naming_the_shrunk_input() -> TestResult {
151        // "every u32 is below 100" is false; the macro must surface a
152        // `Property`-kind failure that names the original and shrunk
153        // counterexamples and still carries the matcher's own description.
154        let error = property!(|n: u32| {
155            check!(n).satisfies(lt(100u32))
156        } using proptest::num::u32::ANY)
157        .err()
158        .or_fail_with("a property false for most u32 must fail")?;
159        let rendered = error.to_string();
160        // The shrunk counterexample (proptest shrinks to exactly 100) is named.
161        check!(rendered.contains("the shrunk (minimal) input is 100")).satisfies(is_true())?;
162        // The original failing input is named too.
163        check!(rendered.contains("the original failing input was")).satisfies(is_true())?;
164        // The matcher's full description survives.
165        check!(rendered.contains("less than 100")).satisfies(is_true())?;
166        // And the failure reads as a property failure.
167        check!(error.kind).satisfies(eq(test_better_core::ErrorKind::Property))
168    }
169}