1use serde::{Deserialize, Serialize};
12use std::fmt::Debug;
13use std::time::Instant;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct AssertionFailure {
18 pub message: String,
20 pub location: Option<String>,
22 #[serde(skip)]
24 pub timestamp: Option<Instant>,
25 pub index: usize,
27}
28
29impl AssertionFailure {
30 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
51pub enum AssertionMode {
52 #[default]
54 Collect,
55 FailFast,
57}
58
59#[derive(Debug, Default)]
74pub struct SoftAssertions {
75 failures: Vec<AssertionFailure>,
76 mode: AssertionMode,
77 assertion_count: usize,
78}
79
80impl SoftAssertions {
81 #[must_use]
83 pub fn new() -> Self {
84 Self::default()
85 }
86
87 #[must_use]
89 pub fn with_mode(mode: AssertionMode) -> Self {
90 Self {
91 mode,
92 ..Self::default()
93 }
94 }
95
96 #[must_use]
98 pub const fn mode(mut self, mode: AssertionMode) -> Self {
99 self.mode = mode;
100 self
101 }
102
103 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn fail(&mut self, message: impl Into<String>) {
232 self.assertion_count += 1;
233 self.record_failure(message.into());
234 }
235
236 fn record_failure(&mut self, message: String) {
238 let failure = AssertionFailure::new(message, self.failures.len());
239 self.failures.push(failure);
240 }
241
242 #[must_use]
244 pub fn failures(&self) -> &[AssertionFailure] {
245 &self.failures
246 }
247
248 #[must_use]
250 pub fn failure_count(&self) -> usize {
251 self.failures.len()
252 }
253
254 #[must_use]
256 pub const fn assertion_count(&self) -> usize {
257 self.assertion_count
258 }
259
260 #[must_use]
262 pub fn all_passed(&self) -> bool {
263 self.failures.is_empty()
264 }
265
266 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 pub fn clear(&mut self) {
282 self.failures.clear();
283 self.assertion_count = 0;
284 }
285
286 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
299pub struct AssertionSummary {
300 pub total: usize,
302 pub passed: usize,
304 pub failed: usize,
306}
307
308#[derive(Debug, Clone)]
310pub struct SoftAssertionError {
311 pub failures: Vec<String>,
313 pub count: usize,
315}
316
317impl SoftAssertionError {
318 #[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#[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}