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