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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn fail(&mut self, message: impl Into<String>) {
231 self.assertion_count += 1;
232 self.record_failure(message.into());
233 }
234
235 fn record_failure(&mut self, message: String) {
237 let failure = AssertionFailure::new(message, self.failures.len());
238 self.failures.push(failure);
239 }
240
241 #[must_use]
243 pub fn failures(&self) -> &[AssertionFailure] {
244 &self.failures
245 }
246
247 #[must_use]
249 pub fn failure_count(&self) -> usize {
250 self.failures.len()
251 }
252
253 #[must_use]
255 pub const fn assertion_count(&self) -> usize {
256 self.assertion_count
257 }
258
259 #[must_use]
261 pub fn all_passed(&self) -> bool {
262 self.failures.is_empty()
263 }
264
265 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 pub fn clear(&mut self) {
280 self.failures.clear();
281 self.assertion_count = 0;
282 }
283
284 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
297pub struct AssertionSummary {
298 pub total: usize,
300 pub passed: usize,
302 pub failed: usize,
304}
305
306#[derive(Debug, Clone)]
308pub struct SoftAssertionError {
309 pub failures: Vec<String>,
311 pub count: usize,
313}
314
315impl SoftAssertionError {
316 #[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#[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}