Skip to main content

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}