behave/soft.rs
1//! Soft assertion support for collecting multiple failures.
2//!
3//! Normal assertions with `?` stop at the first failure. Soft assertions
4//! let you check several expectations and see *all* failures at once —
5//! useful when validating multiple fields of a struct or multiple
6//! conditions that are independently meaningful.
7//!
8//! Use [`SoftErrors::check`] instead of `?` for each assertion, then call
9//! [`SoftErrors::finish`] at the end to either succeed or report every
10//! collected failure with numbered output.
11//!
12//! # When to use soft assertions
13//!
14//! - Validating multiple independent properties of a value
15//! - Form validation tests where you want to see all failing fields
16//! - Integration tests checking several response fields at once
17//!
18//! # When to use hard assertions (`?`)
19//!
20//! - When later assertions depend on earlier ones succeeding
21//! - When a single failure makes the rest meaningless
22//!
23//! # Example output on failure
24//!
25//! ```text
26//! 2 soft assertions failed:
27//!
28//! [1] expect!(name)
29//! actual: ""
30//! expected: to not be empty
31//!
32//! [2] expect!(age)
33//! actual: -1
34//! expected: to be greater than 0
35//! ```
36
37use std::fmt;
38
39use crate::MatchError;
40
41/// Collects [`MatchError`]s from soft assertions and reports them together.
42///
43/// Instead of propagating each assertion failure immediately with `?`, pass
44/// results to [`check`](Self::check). At the end of the test, call
45/// [`finish`](Self::finish) to either succeed or return all collected errors
46/// as a [`SoftMatchError`].
47///
48/// You can freely mix hard assertions (`?`) and soft assertions in the same
49/// test. Hard assertions still fail immediately; soft assertions are deferred.
50///
51/// # Examples
52///
53/// All assertions pass:
54///
55/// ```
56/// use behave::prelude::*;
57///
58/// fn demo() -> Result<(), Box<dyn std::error::Error>> {
59/// let mut errors = SoftErrors::new();
60/// errors.check(expect!(2 + 2).to_equal(4));
61/// errors.check(expect!(true).to_be_true());
62/// errors.finish()?;
63/// Ok(())
64/// }
65///
66/// assert!(demo().is_ok());
67/// ```
68///
69/// Failures are collected and reported together:
70///
71/// ```
72/// use behave::prelude::*;
73///
74/// let mut errors = SoftErrors::new();
75/// errors.check(expect!(1).to_equal(1)); // passes — not collected
76/// errors.check(expect!(2).to_equal(99)); // fails — collected
77/// errors.check(expect!(3).to_equal(88)); // fails — collected
78///
79/// let result = errors.finish();
80/// assert!(result.is_err());
81///
82/// let msg = result.unwrap_err().to_string();
83/// assert!(msg.contains("2 soft assertions failed"));
84/// assert!(msg.contains("[1]"));
85/// assert!(msg.contains("[2]"));
86/// ```
87#[derive(Debug)]
88#[non_exhaustive]
89#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
90pub struct SoftErrors {
91 collected: Vec<MatchError>,
92}
93
94impl SoftErrors {
95 /// Creates an empty soft error collector.
96 ///
97 /// # Examples
98 ///
99 /// ```
100 /// use behave::SoftErrors;
101 ///
102 /// let errors = SoftErrors::new();
103 /// assert!(errors.is_empty());
104 /// ```
105 pub const fn new() -> Self {
106 Self {
107 collected: Vec::new(),
108 }
109 }
110
111 /// Records a matcher result, collecting any error for later reporting.
112 ///
113 /// Pass the return value of any matcher method directly — no `?` needed.
114 /// Passing results silently collect failures; successes are ignored.
115 ///
116 /// # Examples
117 ///
118 /// ```
119 /// use behave::prelude::*;
120 ///
121 /// let mut errors = SoftErrors::new();
122 /// errors.check(expect!(1).to_equal(1)); // passes — not collected
123 /// errors.check(expect!(2).to_equal(99)); // fails — collected
124 /// assert_eq!(errors.len(), 1);
125 /// ```
126 pub fn check(&mut self, result: Result<(), MatchError>) {
127 if let Err(e) = result {
128 self.collected.push(e);
129 }
130 }
131
132 /// Finishes the soft assertion block and reports all collected failures.
133 ///
134 /// Returns `Ok(())` if every checked assertion passed. Returns
135 /// [`SoftMatchError`] if any assertions failed — use `?` to propagate
136 /// it as a test failure.
137 ///
138 /// This method consumes the collector. Create a new [`SoftErrors`] for
139 /// each independent validation block.
140 ///
141 /// # Errors
142 ///
143 /// Returns [`SoftMatchError`] when one or more assertions failed.
144 ///
145 /// # Examples
146 ///
147 /// ```
148 /// use behave::prelude::*;
149 ///
150 /// // All pass — finish() returns Ok
151 /// let mut errors = SoftErrors::new();
152 /// errors.check(expect!(1).to_equal(1));
153 /// assert!(errors.finish().is_ok());
154 ///
155 /// // One fails — finish() returns Err
156 /// let mut errors = SoftErrors::new();
157 /// errors.check(expect!(1).to_equal(99));
158 /// assert!(errors.finish().is_err());
159 /// ```
160 pub fn finish(self) -> Result<(), SoftMatchError> {
161 if self.collected.is_empty() {
162 return Ok(());
163 }
164 Err(SoftMatchError {
165 errors: self.collected,
166 })
167 }
168
169 /// Returns `true` if no failures have been collected.
170 ///
171 /// # Examples
172 ///
173 /// ```
174 /// use behave::SoftErrors;
175 ///
176 /// let errors = SoftErrors::new();
177 /// assert!(errors.is_empty());
178 /// ```
179 pub fn is_empty(&self) -> bool {
180 self.collected.is_empty()
181 }
182
183 /// Returns the number of collected failures.
184 ///
185 /// # Examples
186 ///
187 /// ```
188 /// use behave::prelude::*;
189 ///
190 /// let mut errors = SoftErrors::new();
191 /// errors.check(expect!(1).to_equal(99));
192 /// assert_eq!(errors.len(), 1);
193 /// ```
194 pub fn len(&self) -> usize {
195 self.collected.len()
196 }
197
198 /// Returns a slice of all collected errors.
199 ///
200 /// # Examples
201 ///
202 /// ```
203 /// use behave::prelude::*;
204 ///
205 /// let mut errors = SoftErrors::new();
206 /// errors.check(expect!(1).to_equal(99));
207 /// assert_eq!(errors.errors().len(), 1);
208 /// ```
209 pub fn errors(&self) -> &[MatchError] {
210 &self.collected
211 }
212}
213
214impl Default for SoftErrors {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220/// Error returned by [`SoftErrors::finish`] when assertions failed.
221///
222/// Contains all [`MatchError`]s that were collected during the soft
223/// assertion block. The [`Display`](fmt::Display) output numbers each
224/// failure for easy identification in test output.
225///
226/// # Examples
227///
228/// ```
229/// use behave::prelude::*;
230///
231/// let mut errors = SoftErrors::new();
232/// errors.check(expect!(1).to_equal(99));
233/// let result = errors.finish();
234/// assert!(result.is_err());
235///
236/// let msg = result.err().map(|e| e.to_string()).unwrap_or_default();
237/// assert!(msg.contains("1 soft assertion failed"));
238/// ```
239#[derive(Debug)]
240#[non_exhaustive]
241#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
242pub struct SoftMatchError {
243 /// The collected match errors.
244 pub errors: Vec<MatchError>,
245}
246
247impl fmt::Display for SoftMatchError {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 let count = self.errors.len();
250 let noun = if count == 1 {
251 "assertion"
252 } else {
253 "assertions"
254 };
255 writeln!(f, "{count} soft {noun} failed:")?;
256 for (i, err) in self.errors.iter().enumerate() {
257 write!(f, "\n[{}] {err}", i + 1)?;
258 if i + 1 < count {
259 writeln!(f)?;
260 }
261 }
262 Ok(())
263 }
264}
265
266impl std::error::Error for SoftMatchError {}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn new_is_empty() {
274 let errors = SoftErrors::new();
275 assert!(errors.is_empty());
276 assert_eq!(errors.len(), 0);
277 }
278
279 #[test]
280 fn check_collects_errors() {
281 let mut errors = SoftErrors::new();
282 errors.check(Ok(()));
283 assert!(errors.is_empty());
284
285 let err = MatchError::new("x".to_string(), "1".to_string(), "2".to_string(), false);
286 errors.check(Err(err));
287 assert_eq!(errors.len(), 1);
288 assert!(!errors.is_empty());
289 }
290
291 #[test]
292 fn finish_ok_when_empty() {
293 let errors = SoftErrors::new();
294 assert!(errors.finish().is_ok());
295 }
296
297 #[test]
298 fn finish_err_when_failures() {
299 let mut errors = SoftErrors::new();
300 let err = MatchError::new("x".to_string(), "1".to_string(), "2".to_string(), false);
301 errors.check(Err(err));
302 let result = errors.finish();
303 assert!(result.is_err());
304 }
305
306 #[test]
307 fn display_format() {
308 let mut errors = SoftErrors::new();
309 errors.check(Err(MatchError::new(
310 "name".to_string(),
311 "to not be empty".to_string(),
312 "\"\"".to_string(),
313 false,
314 )));
315 errors.check(Err(MatchError::new(
316 "age".to_string(),
317 "to be greater than 0".to_string(),
318 "-1".to_string(),
319 false,
320 )));
321 let result = errors.finish();
322 assert!(result.is_err());
323 let msg = result.err().map(|e| e.to_string()).unwrap_or_default();
324 assert!(msg.contains("2 soft assertions failed:"));
325 assert!(msg.contains("[1]"));
326 assert!(msg.contains("[2]"));
327 assert!(msg.contains("expect!(name)"));
328 assert!(msg.contains("expect!(age)"));
329 }
330
331 #[test]
332 fn errors_returns_slice() {
333 let mut errors = SoftErrors::new();
334 errors.check(Err(MatchError::new(
335 "x".to_string(),
336 "a".to_string(),
337 "b".to_string(),
338 false,
339 )));
340 assert_eq!(errors.errors().len(), 1);
341 assert_eq!(errors.errors()[0].expression, "x");
342 }
343
344 #[test]
345 fn default_is_empty() {
346 let errors = SoftErrors::default();
347 assert!(errors.is_empty());
348 }
349}