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}