1use std::cell::Cell;
36use std::collections::HashMap;
37
38thread_local! {
43 static ALWAYS_VIOLATION_COUNT: Cell<u64> = const { Cell::new(0) };
44 static SKIP_NEXT_ASSERTION_RESET: Cell<bool> = const { Cell::new(false) };
49}
50
51pub fn record_always_violation() {
56 ALWAYS_VIOLATION_COUNT.with(|c| c.set(c.get() + 1));
57}
58
59pub fn reset_always_violations() {
61 ALWAYS_VIOLATION_COUNT.with(|c| c.set(0));
62}
63
64#[must_use]
66pub fn has_always_violations() -> bool {
67 ALWAYS_VIOLATION_COUNT.with(|c| c.get() > 0)
68}
69
70#[derive(Debug, Clone, PartialEq)]
76pub struct AssertionStats {
77 pub total_checks: usize,
79 pub successes: usize,
81}
82
83impl AssertionStats {
84 #[must_use]
86 pub fn new() -> Self {
87 Self {
88 total_checks: 0,
89 successes: 0,
90 }
91 }
92
93 #[must_use]
97 pub fn success_rate(&self) -> f64 {
98 if self.total_checks == 0 {
99 0.0
100 } else {
101 let successes_u32 = u32::try_from(self.successes).unwrap_or(u32::MAX);
102 let total_u32 = u32::try_from(self.total_checks).unwrap_or(u32::MAX);
103 (f64::from(successes_u32) / f64::from(total_u32)) * 100.0
104 }
105 }
106
107 pub fn record(&mut self, success: bool) {
111 self.total_checks += 1;
112 if success {
113 self.successes += 1;
114 }
115 }
116}
117
118impl Default for AssertionStats {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124pub fn to_i64_saturating<T>(value: T) -> i64
139where
140 T: TryInto<i64>,
141{
142 value.try_into().unwrap_or(i64::MAX)
143}
144
145pub fn format_details(pairs: &[(&str, &dyn std::fmt::Display)]) -> String {
147 pairs
148 .iter()
149 .map(|(k, v)| format!("{k}={v}"))
150 .collect::<Vec<_>>()
151 .join(", ")
152}
153
154pub fn on_assertion_bool(
163 msg: impl AsRef<str>,
164 condition: bool,
165 kind: moonpool_assertions::AssertKind,
166 must_hit: bool,
167) {
168 moonpool_assertions::assertion_bool(kind, must_hit, condition, msg.as_ref());
169}
170
171pub fn on_assertion_numeric(
176 msg: impl AsRef<str>,
177 value: i64,
178 cmp: moonpool_assertions::AssertCmp,
179 threshold: i64,
180 kind: moonpool_assertions::AssertKind,
181 maximize: bool,
182) {
183 moonpool_assertions::assertion_numeric(kind, cmp, maximize, value, threshold, msg.as_ref());
184}
185
186pub fn on_assertion_sometimes_all(msg: impl AsRef<str>, named_bools: &[(&str, bool)]) {
190 moonpool_assertions::assertion_sometimes_all(msg.as_ref(), named_bools);
191}
192
193pub fn on_sometimes_each(msg: &str, keys: &[(&str, i64)], quality: &[(&str, i64)]) {
198 moonpool_assertions::assertion_sometimes_each(msg, keys, quality);
199}
200
201#[must_use]
210pub fn assertion_results() -> HashMap<String, AssertionStats> {
211 let slots = moonpool_assertions::assertion_read_all();
212 let mut results = HashMap::new();
213
214 for slot in &slots {
215 let total =
216 usize::try_from(slot.pass_count.saturating_add(slot.fail_count)).unwrap_or(usize::MAX);
217 if total == 0 {
218 continue;
219 }
220 results.insert(
221 slot.msg.clone(),
222 AssertionStats {
223 total_checks: total,
224 successes: usize::try_from(slot.pass_count).unwrap_or(usize::MAX),
225 },
226 );
227 }
228
229 results
230}
231
232pub fn skip_next_assertion_reset() {
239 SKIP_NEXT_ASSERTION_RESET.with(|c| c.set(true));
240}
241
242pub fn reset_assertion_results() {
248 let skip = SKIP_NEXT_ASSERTION_RESET.with(|c| {
249 let v = c.get();
250 c.set(false); v
252 });
253 if !skip {
254 moonpool_assertions::reset();
255 }
256}
257
258#[must_use]
267pub fn validate_assertion_contracts() -> (Vec<String>, Vec<String>) {
268 let mut always_violations = Vec::new();
269 let mut coverage_violations = Vec::new();
270 let slots = moonpool_assertions::assertion_read_all();
271
272 for slot in &slots {
273 let total = slot.pass_count.saturating_add(slot.fail_count);
274 let kind = moonpool_assertions::AssertKind::from_u8(slot.kind);
275
276 match kind {
277 Some(moonpool_assertions::AssertKind::Always) => {
278 if slot.fail_count > 0 {
279 always_violations.push(format!(
280 "assert_always!('{}') failed {} times out of {}",
281 slot.msg, slot.fail_count, total
282 ));
283 }
284 if slot.must_hit != 0 && total == 0 {
285 always_violations
286 .push(format!("assert_always!('{}') was never reached", slot.msg));
287 }
288 }
289 Some(moonpool_assertions::AssertKind::AlwaysOrUnreachable) => {
290 if slot.fail_count > 0 {
291 always_violations.push(format!(
292 "assert_always_or_unreachable!('{}') failed {} times out of {}",
293 slot.msg, slot.fail_count, total
294 ));
295 }
296 }
297 Some(moonpool_assertions::AssertKind::Sometimes) => {
298 if total > 0 && slot.pass_count == 0 {
299 coverage_violations.push(format!(
300 "assert_sometimes!('{}') has 0% success rate ({} checks)",
301 slot.msg, total
302 ));
303 }
304 }
305 Some(moonpool_assertions::AssertKind::Reachable) => {
306 if slot.pass_count == 0 {
307 coverage_violations.push(format!(
308 "assert_reachable!('{}') was never reached",
309 slot.msg
310 ));
311 }
312 }
313 Some(moonpool_assertions::AssertKind::Unreachable) => {
314 if slot.pass_count > 0 {
315 always_violations.push(format!(
316 "assert_unreachable!('{}') was reached {} times",
317 slot.msg, slot.pass_count
318 ));
319 }
320 }
321 Some(moonpool_assertions::AssertKind::NumericAlways) => {
322 if slot.fail_count > 0 {
323 always_violations.push(format!(
324 "numeric assert_always ('{}') failed {} times out of {}",
325 slot.msg, slot.fail_count, total
326 ));
327 }
328 }
329 Some(moonpool_assertions::AssertKind::NumericSometimes) => {
330 if total > 0 && slot.pass_count == 0 {
331 coverage_violations.push(format!(
332 "numeric assert_sometimes ('{}') has 0% success rate ({} checks)",
333 slot.msg, total
334 ));
335 }
336 }
337 Some(moonpool_assertions::AssertKind::BooleanSometimesAll) | None => {
338 }
341 }
342 }
343
344 (always_violations, coverage_violations)
345}
346
347#[macro_export]
358macro_rules! assert_always {
359 ($condition:expr, $message:expr) => {
360 let __msg = $message;
361 let cond = $condition;
362 $crate::chaos::assertions::on_assertion_bool(
363 &__msg,
364 cond,
365 $crate::chaos::assertions::_re_export::AssertKind::Always,
366 true,
367 );
368 if !cond {
369 $crate::chaos::assertions::record_always_violation();
370 }
371 };
372 ($condition:expr, $message:expr, { $($key:expr => $val:expr),+ $(,)? }) => {
373 let __msg = $message;
374 let cond = $condition;
375 $crate::chaos::assertions::on_assertion_bool(
376 &__msg,
377 cond,
378 $crate::chaos::assertions::_re_export::AssertKind::Always,
379 true,
380 );
381 if !cond {
382 $crate::chaos::assertions::record_always_violation();
383 eprintln!(
384 "[ASSERTION FAILED] {} (seed={}) | {}",
385 __msg,
386 $crate::current_sim_seed(),
387 $crate::chaos::assertions::format_details(
388 &[ $(($key, &$val as &dyn std::fmt::Display)),+ ]
389 )
390 );
391 }
392 };
393}
394
395#[macro_export]
400macro_rules! assert_always_or_unreachable {
401 ($condition:expr, $message:expr) => {
402 let __msg = $message;
403 let cond = $condition;
404 $crate::chaos::assertions::on_assertion_bool(
405 &__msg,
406 cond,
407 $crate::chaos::assertions::_re_export::AssertKind::AlwaysOrUnreachable,
408 false,
409 );
410 if !cond {
411 $crate::chaos::assertions::record_always_violation();
412 }
413 };
414 ($condition:expr, $message:expr, { $($key:expr => $val:expr),+ $(,)? }) => {
415 let __msg = $message;
416 let cond = $condition;
417 $crate::chaos::assertions::on_assertion_bool(
418 &__msg,
419 cond,
420 $crate::chaos::assertions::_re_export::AssertKind::AlwaysOrUnreachable,
421 false,
422 );
423 if !cond {
424 $crate::chaos::assertions::record_always_violation();
425 eprintln!(
426 "[ASSERTION FAILED] {} (seed={}) | {}",
427 __msg,
428 $crate::current_sim_seed(),
429 $crate::chaos::assertions::format_details(
430 &[ $(($key, &$val as &dyn std::fmt::Display)),+ ]
431 )
432 );
433 }
434 };
435}
436
437#[macro_export]
441macro_rules! assert_sometimes {
442 ($condition:expr, $message:expr) => {
443 $crate::chaos::assertions::on_assertion_bool(
444 &$message,
445 $condition,
446 $crate::chaos::assertions::_re_export::AssertKind::Sometimes,
447 true,
448 );
449 };
450}
451
452#[macro_export]
456macro_rules! assert_reachable {
457 ($message:expr) => {
458 $crate::chaos::assertions::on_assertion_bool(
459 &$message,
460 true,
461 $crate::chaos::assertions::_re_export::AssertKind::Reachable,
462 true,
463 );
464 };
465}
466
467#[macro_export]
472macro_rules! assert_unreachable {
473 ($message:expr) => {
474 let __msg = $message;
475 $crate::chaos::assertions::on_assertion_bool(
476 &__msg,
477 true,
478 $crate::chaos::assertions::_re_export::AssertKind::Unreachable,
479 false,
480 );
481 $crate::chaos::assertions::record_always_violation();
482 };
483 ($message:expr, { $($key:expr => $val:expr),+ $(,)? }) => {
484 let __msg = $message;
485 $crate::chaos::assertions::on_assertion_bool(
486 &__msg,
487 true,
488 $crate::chaos::assertions::_re_export::AssertKind::Unreachable,
489 false,
490 );
491 $crate::chaos::assertions::record_always_violation();
492 eprintln!(
493 "[ASSERTION FAILED] {} | {}",
494 __msg,
495 $crate::chaos::assertions::format_details(
496 &[ $(($key, &$val as &dyn std::fmt::Display)),+ ]
497 )
498 );
499 };
500}
501
502#[macro_export]
506macro_rules! assert_always_greater_than {
507 ($val:expr, $thresh:expr, $message:expr) => {
508 let __msg = $message;
509 let __v = $crate::chaos::assertions::to_i64_saturating($val);
510 let __t = $crate::chaos::assertions::to_i64_saturating($thresh);
511 $crate::chaos::assertions::on_assertion_numeric(
512 &__msg,
513 __v,
514 $crate::chaos::assertions::_re_export::AssertCmp::Gt,
515 __t,
516 $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
517 false,
518 );
519 if !(__v > __t) {
520 $crate::chaos::assertions::record_always_violation();
521 }
522 };
523 ($val:expr, $thresh:expr, $message:expr, { $($key:expr => $dval:expr),+ $(,)? }) => {
524 let __msg = $message;
525 let __v = $crate::chaos::assertions::to_i64_saturating($val);
526 let __t = $crate::chaos::assertions::to_i64_saturating($thresh);
527 $crate::chaos::assertions::on_assertion_numeric(
528 &__msg,
529 __v,
530 $crate::chaos::assertions::_re_export::AssertCmp::Gt,
531 __t,
532 $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
533 false,
534 );
535 if !(__v > __t) {
536 $crate::chaos::assertions::record_always_violation();
537 eprintln!(
538 "[ASSERTION FAILED] {} ({}>{} failed, seed={}) | {}",
539 __msg, __v, __t,
540 $crate::current_sim_seed(),
541 $crate::chaos::assertions::format_details(
542 &[ $(($key, &$dval as &dyn std::fmt::Display)),+ ]
543 )
544 );
545 }
546 };
547}
548
549#[macro_export]
553macro_rules! assert_always_greater_than_or_equal_to {
554 ($val:expr, $thresh:expr, $message:expr) => {
555 let __msg = $message;
556 let __v = $crate::chaos::assertions::to_i64_saturating($val);
557 let __t = $crate::chaos::assertions::to_i64_saturating($thresh);
558 $crate::chaos::assertions::on_assertion_numeric(
559 &__msg,
560 __v,
561 $crate::chaos::assertions::_re_export::AssertCmp::Ge,
562 __t,
563 $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
564 false,
565 );
566 if !(__v >= __t) {
567 $crate::chaos::assertions::record_always_violation();
568 }
569 };
570 ($val:expr, $thresh:expr, $message:expr, { $($key:expr => $dval:expr),+ $(,)? }) => {
571 let __msg = $message;
572 let __v = $crate::chaos::assertions::to_i64_saturating($val);
573 let __t = $crate::chaos::assertions::to_i64_saturating($thresh);
574 $crate::chaos::assertions::on_assertion_numeric(
575 &__msg,
576 __v,
577 $crate::chaos::assertions::_re_export::AssertCmp::Ge,
578 __t,
579 $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
580 false,
581 );
582 if !(__v >= __t) {
583 $crate::chaos::assertions::record_always_violation();
584 eprintln!(
585 "[ASSERTION FAILED] {} ({}>={} failed, seed={}) | {}",
586 __msg, __v, __t,
587 $crate::current_sim_seed(),
588 $crate::chaos::assertions::format_details(
589 &[ $(($key, &$dval as &dyn std::fmt::Display)),+ ]
590 )
591 );
592 }
593 };
594}
595
596#[macro_export]
600macro_rules! assert_always_less_than {
601 ($val:expr, $thresh:expr, $message:expr) => {
602 let __msg = $message;
603 let __v = $crate::chaos::assertions::to_i64_saturating($val);
604 let __t = $crate::chaos::assertions::to_i64_saturating($thresh);
605 $crate::chaos::assertions::on_assertion_numeric(
606 &__msg,
607 __v,
608 $crate::chaos::assertions::_re_export::AssertCmp::Lt,
609 __t,
610 $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
611 true,
612 );
613 if !(__v < __t) {
614 $crate::chaos::assertions::record_always_violation();
615 }
616 };
617 ($val:expr, $thresh:expr, $message:expr, { $($key:expr => $dval:expr),+ $(,)? }) => {
618 let __msg = $message;
619 let __v = $crate::chaos::assertions::to_i64_saturating($val);
620 let __t = $crate::chaos::assertions::to_i64_saturating($thresh);
621 $crate::chaos::assertions::on_assertion_numeric(
622 &__msg,
623 __v,
624 $crate::chaos::assertions::_re_export::AssertCmp::Lt,
625 __t,
626 $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
627 true,
628 );
629 if !(__v < __t) {
630 $crate::chaos::assertions::record_always_violation();
631 eprintln!(
632 "[ASSERTION FAILED] {} ({}<{} failed, seed={}) | {}",
633 __msg, __v, __t,
634 $crate::current_sim_seed(),
635 $crate::chaos::assertions::format_details(
636 &[ $(($key, &$dval as &dyn std::fmt::Display)),+ ]
637 )
638 );
639 }
640 };
641}
642
643#[macro_export]
647macro_rules! assert_always_less_than_or_equal_to {
648 ($val:expr, $thresh:expr, $message:expr) => {
649 let __msg = $message;
650 let __v = $crate::chaos::assertions::to_i64_saturating($val);
651 let __t = $crate::chaos::assertions::to_i64_saturating($thresh);
652 $crate::chaos::assertions::on_assertion_numeric(
653 &__msg,
654 __v,
655 $crate::chaos::assertions::_re_export::AssertCmp::Le,
656 __t,
657 $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
658 true,
659 );
660 if !(__v <= __t) {
661 $crate::chaos::assertions::record_always_violation();
662 }
663 };
664 ($val:expr, $thresh:expr, $message:expr, { $($key:expr => $dval:expr),+ $(,)? }) => {
665 let __msg = $message;
666 let __v = $crate::chaos::assertions::to_i64_saturating($val);
667 let __t = $crate::chaos::assertions::to_i64_saturating($thresh);
668 $crate::chaos::assertions::on_assertion_numeric(
669 &__msg,
670 __v,
671 $crate::chaos::assertions::_re_export::AssertCmp::Le,
672 __t,
673 $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
674 true,
675 );
676 if !(__v <= __t) {
677 $crate::chaos::assertions::record_always_violation();
678 eprintln!(
679 "[ASSERTION FAILED] {} ({}<={} failed, seed={}) | {}",
680 __msg, __v, __t,
681 $crate::current_sim_seed(),
682 $crate::chaos::assertions::format_details(
683 &[ $(($key, &$dval as &dyn std::fmt::Display)),+ ]
684 )
685 );
686 }
687 };
688}
689
690#[macro_export]
692macro_rules! assert_sometimes_greater_than {
693 ($val:expr, $thresh:expr, $message:expr) => {
694 $crate::chaos::assertions::on_assertion_numeric(
695 &$message,
696 $crate::chaos::assertions::to_i64_saturating($val),
697 $crate::chaos::assertions::_re_export::AssertCmp::Gt,
698 $crate::chaos::assertions::to_i64_saturating($thresh),
699 $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
700 true,
701 );
702 };
703}
704
705#[macro_export]
707macro_rules! assert_sometimes_greater_than_or_equal_to {
708 ($val:expr, $thresh:expr, $message:expr) => {
709 $crate::chaos::assertions::on_assertion_numeric(
710 &$message,
711 $crate::chaos::assertions::to_i64_saturating($val),
712 $crate::chaos::assertions::_re_export::AssertCmp::Ge,
713 $crate::chaos::assertions::to_i64_saturating($thresh),
714 $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
715 true,
716 );
717 };
718}
719
720#[macro_export]
722macro_rules! assert_sometimes_less_than {
723 ($val:expr, $thresh:expr, $message:expr) => {
724 $crate::chaos::assertions::on_assertion_numeric(
725 &$message,
726 $crate::chaos::assertions::to_i64_saturating($val),
727 $crate::chaos::assertions::_re_export::AssertCmp::Lt,
728 $crate::chaos::assertions::to_i64_saturating($thresh),
729 $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
730 false,
731 );
732 };
733}
734
735#[macro_export]
737macro_rules! assert_sometimes_less_than_or_equal_to {
738 ($val:expr, $thresh:expr, $message:expr) => {
739 $crate::chaos::assertions::on_assertion_numeric(
740 &$message,
741 $crate::chaos::assertions::to_i64_saturating($val),
742 $crate::chaos::assertions::_re_export::AssertCmp::Le,
743 $crate::chaos::assertions::to_i64_saturating($thresh),
744 $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
745 false,
746 );
747 };
748}
749
750#[macro_export]
765macro_rules! assert_sometimes_all {
766 ($msg:expr, [ $(($name:expr, $val:expr)),+ $(,)? ]) => {
767 $crate::chaos::assertions::on_assertion_sometimes_all($msg, &[ $(($name, $val)),+ ])
768 };
769}
770
771#[macro_export]
787macro_rules! assert_sometimes_each {
788 ($msg:expr, [ $(($name:expr, $val:expr)),+ $(,)? ]) => {
789 $crate::chaos::assertions::on_sometimes_each(
790 $msg,
791 &[ $(($name, $crate::chaos::assertions::to_i64_saturating($val))),+ ],
792 &[],
793 )
794 };
795 ($msg:expr, [ $(($name:expr, $val:expr)),+ $(,)? ], [ $(($qname:expr, $qval:expr)),+ $(,)? ]) => {
796 $crate::chaos::assertions::on_sometimes_each(
797 $msg,
798 &[ $(($name, $crate::chaos::assertions::to_i64_saturating($val))),+ ],
799 &[ $(($qname, $crate::chaos::assertions::to_i64_saturating($qval))),+ ],
800 )
801 };
802}
803
804pub mod _re_export {
806 pub use moonpool_assertions::{AssertCmp, AssertKind};
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812
813 #[test]
814 fn test_assertion_stats_new() {
815 let stats = AssertionStats::new();
816 assert_eq!(stats.total_checks, 0);
817 assert_eq!(stats.successes, 0);
818 assert!(stats.success_rate().abs() < f64::EPSILON);
819 }
820
821 #[test]
822 fn test_assertion_stats_record() {
823 let mut stats = AssertionStats::new();
824
825 stats.record(true);
826 assert_eq!(stats.total_checks, 1);
827 assert_eq!(stats.successes, 1);
828 assert!((stats.success_rate() - 100.0).abs() < f64::EPSILON);
829
830 stats.record(false);
831 assert_eq!(stats.total_checks, 2);
832 assert_eq!(stats.successes, 1);
833 assert!((stats.success_rate() - 50.0).abs() < f64::EPSILON);
834
835 stats.record(true);
836 assert_eq!(stats.total_checks, 3);
837 assert_eq!(stats.successes, 2);
838 let expected = 200.0 / 3.0;
839 assert!((stats.success_rate() - expected).abs() < 1e-10);
840 }
841
842 #[test]
843 fn test_assertion_stats_success_rate_edge_cases() {
844 let mut stats = AssertionStats::new();
845 assert!(stats.success_rate().abs() < f64::EPSILON);
846
847 stats.record(false);
848 assert!(stats.success_rate().abs() < f64::EPSILON);
849
850 stats.record(true);
851 assert!((stats.success_rate() - 50.0).abs() < f64::EPSILON);
852 }
853
854 #[test]
855 fn test_assertion_results_empty() {
856 let results = assertion_results();
859 let _ = results;
862 }
863
864 #[test]
865 fn test_validate_contracts_empty() {
866 let violations = validate_assertion_contracts();
868 let _ = violations;
870 }
871}