Skip to main content

jugar_probar/assertion/
soft.rs

1//! Soft Assertions (Feature 17)
2//!
3//! Collect multiple assertion failures without stopping test execution.
4//!
5//! ## EXTREME TDD: Tests written FIRST per spec
6//!
7//! ## Toyota Way Application:
8//! - **Jidoka**: Collect all failures for comprehensive error reporting
9//! - **Poka-Yoke**: Type-safe API prevents misuse
10
11use serde::{Deserialize, Serialize};
12use std::fmt::Debug;
13use std::time::Instant;
14
15/// A single assertion failure
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct AssertionFailure {
18    /// Message describing the failure
19    pub message: String,
20    /// Location where the assertion failed (<file:line>)
21    pub location: Option<String>,
22    /// Timestamp when the failure occurred
23    #[serde(skip)]
24    pub timestamp: Option<Instant>,
25    /// Index of this assertion in the sequence
26    pub index: usize,
27}
28
29impl AssertionFailure {
30    /// Create a new assertion failure
31    #[must_use]
32    pub fn new(message: impl Into<String>, index: usize) -> Self {
33        Self {
34            message: message.into(),
35            location: None,
36            timestamp: Some(Instant::now()),
37            index,
38        }
39    }
40
41    /// Set the location of the failure
42    #[must_use]
43    pub fn with_location(mut self, location: impl Into<String>) -> Self {
44        self.location = Some(location.into());
45        self
46    }
47}
48
49/// Mode for soft assertions behavior
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
51pub enum AssertionMode {
52    /// Collect all failures (default)
53    #[default]
54    Collect,
55    /// Stop on first failure (like hard assertions)
56    FailFast,
57}
58
59/// Soft assertions collector
60///
61/// Collects multiple assertion failures without stopping test execution.
62///
63/// ## Example
64///
65/// ```ignore
66/// let mut soft = SoftAssertions::new();
67/// soft.assert_eq(&1, &2, "values should match");
68/// soft.assert_true(false, "condition should be true");
69/// // Both failures are collected
70/// let result = soft.verify();
71/// assert!(result.is_err());
72/// ```
73#[derive(Debug, Default)]
74pub struct SoftAssertions {
75    failures: Vec<AssertionFailure>,
76    mode: AssertionMode,
77    assertion_count: usize,
78}
79
80impl SoftAssertions {
81    /// Create a new soft assertions collector
82    #[must_use]
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    /// Create with a specific mode
88    #[must_use]
89    pub fn with_mode(mode: AssertionMode) -> Self {
90        Self {
91            mode,
92            ..Self::default()
93        }
94    }
95
96    /// Set the assertion mode
97    #[must_use]
98    pub const fn mode(mut self, mode: AssertionMode) -> Self {
99        self.mode = mode;
100        self
101    }
102
103    /// Assert two values are equal
104    pub fn assert_eq<T: PartialEq + Debug>(&mut self, actual: &T, expected: &T, message: &str) {
105        contract_pre_soft_assertion_collection!();
106        self.assertion_count += 1;
107        if actual != expected {
108            let failure_msg = format!("{message}: expected {expected:?}, got {actual:?}");
109            self.record_failure(failure_msg);
110        }
111    }
112
113    /// Assert two values are not equal
114    pub fn assert_ne<T: PartialEq + Debug>(&mut self, actual: &T, expected: &T, message: &str) {
115        self.assertion_count += 1;
116        if actual == expected {
117            let failure_msg = format!("{message}: expected values to differ, both were {actual:?}");
118            self.record_failure(failure_msg);
119        }
120    }
121
122    /// Assert a condition is true
123    pub fn assert_true(&mut self, condition: bool, message: &str) {
124        self.assertion_count += 1;
125        if !condition {
126            self.record_failure(format!("{message}: expected true, got false"));
127        }
128    }
129
130    /// Assert a condition is false
131    pub fn assert_false(&mut self, condition: bool, message: &str) {
132        self.assertion_count += 1;
133        if condition {
134            self.record_failure(format!("{message}: expected false, got true"));
135        }
136    }
137
138    /// Assert a value is Some
139    pub fn assert_some<T>(&mut self, opt: &Option<T>, message: &str) {
140        self.assertion_count += 1;
141        if opt.is_none() {
142            self.record_failure(format!("{message}: expected Some, got None"));
143        }
144    }
145
146    /// Assert a value is None
147    pub fn assert_none<T>(&mut self, opt: &Option<T>, message: &str) {
148        self.assertion_count += 1;
149        if opt.is_some() {
150            self.record_failure(format!("{message}: expected None, got Some"));
151        }
152    }
153
154    /// Assert a Result is Ok
155    pub fn assert_ok<T, E>(&mut self, result: &Result<T, E>, message: &str) {
156        self.assertion_count += 1;
157        if result.is_err() {
158            self.record_failure(format!("{message}: expected Ok, got Err"));
159        }
160    }
161
162    /// Assert a Result is Err
163    pub fn assert_err<T, E>(&mut self, result: &Result<T, E>, message: &str) {
164        self.assertion_count += 1;
165        if result.is_ok() {
166            self.record_failure(format!("{message}: expected Err, got Ok"));
167        }
168    }
169
170    /// Assert a string contains a substring
171    pub fn assert_contains(&mut self, haystack: &str, needle: &str, message: &str) {
172        self.assertion_count += 1;
173        if !haystack.contains(needle) {
174            self.record_failure(format!(
175                "{message}: expected '{haystack}' to contain '{needle}'"
176            ));
177        }
178    }
179
180    /// Assert a collection has expected length
181    pub fn assert_len<T>(&mut self, collection: &[T], expected: usize, message: &str) {
182        self.assertion_count += 1;
183        if collection.len() != expected {
184            self.record_failure(format!(
185                "{message}: expected length {expected}, got {}",
186                collection.len()
187            ));
188        }
189    }
190
191    /// Assert a collection is empty
192    pub fn assert_empty<T>(&mut self, collection: &[T], message: &str) {
193        self.assertion_count += 1;
194        if !collection.is_empty() {
195            self.record_failure(format!(
196                "{message}: expected empty collection, got {} elements",
197                collection.len()
198            ));
199        }
200    }
201
202    /// Assert a collection is not empty
203    pub fn assert_not_empty<T>(&mut self, collection: &[T], message: &str) {
204        self.assertion_count += 1;
205        if collection.is_empty() {
206            self.record_failure(format!("{message}: expected non-empty collection"));
207        }
208    }
209
210    /// Assert two floats are approximately equal
211    pub fn assert_approx_eq(&mut self, actual: f64, expected: f64, epsilon: f64, message: &str) {
212        self.assertion_count += 1;
213        if (actual - expected).abs() >= epsilon {
214            self.record_failure(format!(
215                "{message}: expected {actual} ≈ {expected} (epsilon: {epsilon})"
216            ));
217        }
218    }
219
220    /// Assert a value is in a range
221    pub fn assert_in_range(&mut self, value: f64, min: f64, max: f64, message: &str) {
222        self.assertion_count += 1;
223        if value < min || value > max {
224            self.record_failure(format!(
225                "{message}: expected {value} to be in range [{min}, {max}]"
226            ));
227        }
228    }
229
230    /// Record a custom failure
231    pub fn fail(&mut self, message: impl Into<String>) {
232        self.assertion_count += 1;
233        self.record_failure(message.into());
234    }
235
236    /// Record a failure with location info
237    fn record_failure(&mut self, message: String) {
238        let failure = AssertionFailure::new(message, self.failures.len());
239        self.failures.push(failure);
240    }
241
242    /// Get all failures
243    #[must_use]
244    pub fn failures(&self) -> &[AssertionFailure] {
245        &self.failures
246    }
247
248    /// Get the number of failures
249    #[must_use]
250    pub fn failure_count(&self) -> usize {
251        self.failures.len()
252    }
253
254    /// Get the total number of assertions checked
255    #[must_use]
256    pub const fn assertion_count(&self) -> usize {
257        self.assertion_count
258    }
259
260    /// Check if all assertions passed
261    #[must_use]
262    pub fn all_passed(&self) -> bool {
263        self.failures.is_empty()
264    }
265
266    /// Verify all assertions passed, returning error if any failed
267    ///
268    /// # Errors
269    ///
270    /// Returns error containing all failure messages if any assertions failed
271    pub fn verify(&self) -> Result<(), SoftAssertionError> {
272        contract_pre_soft_assertion_collection!();
273        if self.failures.is_empty() {
274            Ok(())
275        } else {
276            Err(SoftAssertionError::new(&self.failures))
277        }
278    }
279
280    /// Clear all recorded failures
281    pub fn clear(&mut self) {
282        self.failures.clear();
283        self.assertion_count = 0;
284    }
285
286    /// Get a summary of the assertions
287    #[must_use]
288    pub fn summary(&self) -> AssertionSummary {
289        AssertionSummary {
290            total: self.assertion_count,
291            passed: self.assertion_count - self.failures.len(),
292            failed: self.failures.len(),
293        }
294    }
295}
296
297/// Summary of assertion results
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
299pub struct AssertionSummary {
300    /// Total assertions checked
301    pub total: usize,
302    /// Assertions that passed
303    pub passed: usize,
304    /// Assertions that failed
305    pub failed: usize,
306}
307
308/// Error type for soft assertion failures
309#[derive(Debug, Clone)]
310pub struct SoftAssertionError {
311    /// All failure messages
312    pub failures: Vec<String>,
313    /// Number of failed assertions
314    pub count: usize,
315}
316
317impl SoftAssertionError {
318    /// Create a new error from failures
319    #[must_use]
320    pub fn new(failures: &[AssertionFailure]) -> Self {
321        Self {
322            failures: failures.iter().map(|f| f.message.clone()).collect(),
323            count: failures.len(),
324        }
325    }
326}
327
328impl std::fmt::Display for SoftAssertionError {
329    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330        writeln!(f, "{} assertion(s) failed:", self.count)?;
331        for (i, failure) in self.failures.iter().enumerate() {
332            writeln!(f, "  {}. {failure}", i + 1)?;
333        }
334        Ok(())
335    }
336}
337
338impl std::error::Error for SoftAssertionError {}
339
340// ============================================================================
341// EXTREME TDD: Tests written FIRST per spec
342// ============================================================================
343
344#[cfg(test)]
345#[allow(clippy::unwrap_used, clippy::expect_used)]
346mod tests {
347    use super::*;
348
349    mod soft_assertions_basic {
350        use super::*;
351
352        #[test]
353        fn test_new_creates_empty() {
354            let soft = SoftAssertions::new();
355            assert!(soft.all_passed());
356            assert_eq!(soft.failure_count(), 0);
357            assert_eq!(soft.assertion_count(), 0);
358        }
359
360        #[test]
361        fn test_with_mode() {
362            let soft = SoftAssertions::with_mode(AssertionMode::FailFast);
363            assert_eq!(soft.mode, AssertionMode::FailFast);
364        }
365
366        #[test]
367        fn test_mode_builder() {
368            let soft = SoftAssertions::new().mode(AssertionMode::Collect);
369            assert_eq!(soft.mode, AssertionMode::Collect);
370        }
371    }
372
373    mod equality_assertions {
374        use super::*;
375
376        #[test]
377        fn test_assert_eq_pass() {
378            let mut soft = SoftAssertions::new();
379            soft.assert_eq(&42, &42, "values should match");
380            assert!(soft.all_passed());
381            assert_eq!(soft.assertion_count(), 1);
382        }
383
384        #[test]
385        fn test_assert_eq_fail() {
386            let mut soft = SoftAssertions::new();
387            soft.assert_eq(&1, &2, "values should match");
388            assert!(!soft.all_passed());
389            assert_eq!(soft.failure_count(), 1);
390            assert!(soft.failures()[0].message.contains("expected"));
391        }
392
393        #[test]
394        fn test_assert_ne_pass() {
395            let mut soft = SoftAssertions::new();
396            soft.assert_ne(&1, &2, "values should differ");
397            assert!(soft.all_passed());
398        }
399
400        #[test]
401        fn test_assert_ne_fail() {
402            let mut soft = SoftAssertions::new();
403            soft.assert_ne(&42, &42, "values should differ");
404            assert!(!soft.all_passed());
405        }
406    }
407
408    mod boolean_assertions {
409        use super::*;
410
411        #[test]
412        fn test_assert_true_pass() {
413            let mut soft = SoftAssertions::new();
414            soft.assert_true(true, "should be true");
415            assert!(soft.all_passed());
416        }
417
418        #[test]
419        fn test_assert_true_fail() {
420            let mut soft = SoftAssertions::new();
421            soft.assert_true(false, "should be true");
422            assert!(!soft.all_passed());
423            assert!(soft.failures()[0].message.contains("expected true"));
424        }
425
426        #[test]
427        fn test_assert_false_pass() {
428            let mut soft = SoftAssertions::new();
429            soft.assert_false(false, "should be false");
430            assert!(soft.all_passed());
431        }
432
433        #[test]
434        fn test_assert_false_fail() {
435            let mut soft = SoftAssertions::new();
436            soft.assert_false(true, "should be false");
437            assert!(!soft.all_passed());
438        }
439    }
440
441    mod option_assertions {
442        use super::*;
443
444        #[test]
445        fn test_assert_some_pass() {
446            let mut soft = SoftAssertions::new();
447            soft.assert_some(&Some(42), "should be Some");
448            assert!(soft.all_passed());
449        }
450
451        #[test]
452        fn test_assert_some_fail() {
453            let mut soft = SoftAssertions::new();
454            soft.assert_some::<i32>(&None, "should be Some");
455            assert!(!soft.all_passed());
456        }
457
458        #[test]
459        fn test_assert_none_pass() {
460            let mut soft = SoftAssertions::new();
461            soft.assert_none::<i32>(&None, "should be None");
462            assert!(soft.all_passed());
463        }
464
465        #[test]
466        fn test_assert_none_fail() {
467            let mut soft = SoftAssertions::new();
468            soft.assert_none(&Some(42), "should be None");
469            assert!(!soft.all_passed());
470        }
471    }
472
473    mod result_assertions {
474        use super::*;
475
476        #[test]
477        fn test_assert_ok_pass() {
478            let mut soft = SoftAssertions::new();
479            let result: Result<i32, &str> = Ok(42);
480            soft.assert_ok(&result, "should be Ok");
481            assert!(soft.all_passed());
482        }
483
484        #[test]
485        fn test_assert_ok_fail() {
486            let mut soft = SoftAssertions::new();
487            let result: Result<i32, &str> = Err("error");
488            soft.assert_ok(&result, "should be Ok");
489            assert!(!soft.all_passed());
490        }
491
492        #[test]
493        fn test_assert_err_pass() {
494            let mut soft = SoftAssertions::new();
495            let result: Result<i32, &str> = Err("error");
496            soft.assert_err(&result, "should be Err");
497            assert!(soft.all_passed());
498        }
499
500        #[test]
501        fn test_assert_err_fail() {
502            let mut soft = SoftAssertions::new();
503            let result: Result<i32, &str> = Ok(42);
504            soft.assert_err(&result, "should be Err");
505            assert!(!soft.all_passed());
506        }
507    }
508
509    mod string_assertions {
510        use super::*;
511
512        #[test]
513        fn test_assert_contains_pass() {
514            let mut soft = SoftAssertions::new();
515            soft.assert_contains("hello world", "world", "should contain");
516            assert!(soft.all_passed());
517        }
518
519        #[test]
520        fn test_assert_contains_fail() {
521            let mut soft = SoftAssertions::new();
522            soft.assert_contains("hello", "world", "should contain");
523            assert!(!soft.all_passed());
524        }
525    }
526
527    mod collection_assertions {
528        use super::*;
529
530        #[test]
531        fn test_assert_len_pass() {
532            let mut soft = SoftAssertions::new();
533            soft.assert_len(&[1, 2, 3], 3, "should have length 3");
534            assert!(soft.all_passed());
535        }
536
537        #[test]
538        fn test_assert_len_fail() {
539            let mut soft = SoftAssertions::new();
540            soft.assert_len(&[1, 2], 3, "should have length 3");
541            assert!(!soft.all_passed());
542        }
543
544        #[test]
545        fn test_assert_empty_pass() {
546            let mut soft = SoftAssertions::new();
547            let empty: Vec<i32> = vec![];
548            soft.assert_empty(&empty, "should be empty");
549            assert!(soft.all_passed());
550        }
551
552        #[test]
553        fn test_assert_empty_fail() {
554            let mut soft = SoftAssertions::new();
555            soft.assert_empty(&[1], "should be empty");
556            assert!(!soft.all_passed());
557        }
558
559        #[test]
560        fn test_assert_not_empty_pass() {
561            let mut soft = SoftAssertions::new();
562            soft.assert_not_empty(&[1], "should not be empty");
563            assert!(soft.all_passed());
564        }
565
566        #[test]
567        fn test_assert_not_empty_fail() {
568            let mut soft = SoftAssertions::new();
569            let empty: Vec<i32> = vec![];
570            soft.assert_not_empty(&empty, "should not be empty");
571            assert!(!soft.all_passed());
572        }
573    }
574
575    mod numeric_assertions {
576        use super::*;
577
578        #[test]
579        fn test_assert_approx_eq_pass() {
580            let mut soft = SoftAssertions::new();
581            soft.assert_approx_eq(1.001, 1.0, 0.01, "should be approximately equal");
582            assert!(soft.all_passed());
583        }
584
585        #[test]
586        fn test_assert_approx_eq_fail() {
587            let mut soft = SoftAssertions::new();
588            soft.assert_approx_eq(1.5, 1.0, 0.01, "should be approximately equal");
589            assert!(!soft.all_passed());
590        }
591
592        #[test]
593        fn test_assert_in_range_pass() {
594            let mut soft = SoftAssertions::new();
595            soft.assert_in_range(5.0, 0.0, 10.0, "should be in range");
596            assert!(soft.all_passed());
597        }
598
599        #[test]
600        fn test_assert_in_range_fail() {
601            let mut soft = SoftAssertions::new();
602            soft.assert_in_range(15.0, 0.0, 10.0, "should be in range");
603            assert!(!soft.all_passed());
604        }
605
606        #[test]
607        fn test_assert_in_range_boundaries() {
608            let mut soft = SoftAssertions::new();
609            soft.assert_in_range(0.0, 0.0, 10.0, "min boundary");
610            soft.assert_in_range(10.0, 0.0, 10.0, "max boundary");
611            assert!(soft.all_passed());
612        }
613    }
614
615    mod multiple_failures {
616        use super::*;
617
618        #[test]
619        fn test_collects_multiple_failures() {
620            let mut soft = SoftAssertions::new();
621            soft.assert_eq(&1, &2, "first check");
622            soft.assert_true(false, "second check");
623            soft.assert_contains("hello", "world", "third check");
624
625            assert_eq!(soft.failure_count(), 3);
626            assert_eq!(soft.assertion_count(), 3);
627        }
628
629        #[test]
630        fn test_mixed_pass_and_fail() {
631            let mut soft = SoftAssertions::new();
632            soft.assert_eq(&1, &1, "pass");
633            soft.assert_eq(&1, &2, "fail");
634            soft.assert_true(true, "pass");
635            soft.assert_true(false, "fail");
636
637            assert_eq!(soft.failure_count(), 2);
638            assert_eq!(soft.assertion_count(), 4);
639            assert_eq!(soft.summary().passed, 2);
640        }
641    }
642
643    mod verify {
644        use super::*;
645
646        #[test]
647        fn test_verify_pass() {
648            let mut soft = SoftAssertions::new();
649            soft.assert_eq(&1, &1, "match");
650            assert!(soft.verify().is_ok());
651        }
652
653        #[test]
654        fn test_verify_fail() {
655            let mut soft = SoftAssertions::new();
656            soft.assert_eq(&1, &2, "mismatch");
657            let err = soft.verify().unwrap_err();
658            assert_eq!(err.count, 1);
659            assert!(!err.failures.is_empty());
660        }
661
662        #[test]
663        fn test_error_display() {
664            let mut soft = SoftAssertions::new();
665            soft.assert_eq(&1, &2, "first");
666            soft.assert_true(false, "second");
667            let err = soft.verify().unwrap_err();
668            let display = format!("{err}");
669            assert!(display.contains("2 assertion(s) failed"));
670            assert!(display.contains("first"));
671            assert!(display.contains("second"));
672        }
673    }
674
675    mod summary {
676        use super::*;
677
678        #[test]
679        fn test_summary() {
680            let mut soft = SoftAssertions::new();
681            soft.assert_eq(&1, &1, "pass");
682            soft.assert_eq(&1, &2, "fail");
683            soft.assert_true(true, "pass");
684
685            let summary = soft.summary();
686            assert_eq!(summary.total, 3);
687            assert_eq!(summary.passed, 2);
688            assert_eq!(summary.failed, 1);
689        }
690    }
691
692    mod clear {
693        use super::*;
694
695        #[test]
696        fn test_clear() {
697            let mut soft = SoftAssertions::new();
698            soft.assert_eq(&1, &2, "fail");
699            assert_eq!(soft.failure_count(), 1);
700
701            soft.clear();
702            assert_eq!(soft.failure_count(), 0);
703            assert_eq!(soft.assertion_count(), 0);
704            assert!(soft.all_passed());
705        }
706    }
707
708    mod custom_failure {
709        use super::*;
710
711        #[test]
712        fn test_fail_method() {
713            let mut soft = SoftAssertions::new();
714            soft.fail("custom failure message");
715            assert!(!soft.all_passed());
716            assert_eq!(soft.failures()[0].message, "custom failure message");
717        }
718    }
719
720    mod assertion_failure {
721        use super::*;
722
723        #[test]
724        fn test_assertion_failure_new() {
725            let failure = AssertionFailure::new("test message", 0);
726            assert_eq!(failure.message, "test message");
727            assert_eq!(failure.index, 0);
728            assert!(failure.timestamp.is_some());
729            assert!(failure.location.is_none());
730        }
731
732        #[test]
733        fn test_assertion_failure_with_location() {
734            let failure = AssertionFailure::new("test", 0).with_location("test.rs:42");
735            assert_eq!(failure.location, Some("test.rs:42".to_string()));
736        }
737    }
738}