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