1use std::sync::atomic::AtomicU64;
2#[cfg(feature = "full")]
3use std::{collections::HashMap, sync::{atomic::Ordering, Arc, RwLock}};
4use crate::details::Details;
5#[cfg(feature = "full")]
6use crate::internal;
7#[cfg(feature = "full")]
8use linkme::distributed_slice;
9#[cfg(feature = "full")]
10use once_cell::sync::Lazy;
11use serde::Serialize;
12use serde_json::Value;
13#[cfg(feature = "full")]
14use serde_json::json;
15
16mod macros;
17#[doc(hidden)]
18#[cfg(feature = "full")]
19pub mod guidance;
20
21#[doc(hidden)]
23#[distributed_slice]
24#[cfg(feature = "full")]
25pub static ANTITHESIS_CATALOG: [AssertionCatalogInfo];
26
27#[doc(hidden)]
29#[distributed_slice]
30#[cfg(feature = "full")]
31pub static ANTITHESIS_GUIDANCE_CATALOG: [self::guidance::GuidanceCatalogInfo];
32
33#[cfg(feature = "full")]
34pub(crate) static INIT_CATALOG: Lazy<()> = Lazy::new(|| {
35 for event in crate::catalog::registrations() {
36 internal::dispatch_output(&event);
37 }
38});
39
40#[cfg(feature = "full")]
43pub(crate) fn catalog_event(info: &AssertionCatalogInfo) -> Value {
44 let f_name: &str = info.function.as_ref();
45 let details = json!(null);
46 let assertion = AssertionInfo::new(
47 info.assert_type,
48 info.display_type,
49 info.condition,
50 info.message,
51 info.class,
52 f_name,
53 info.file,
54 info.begin_line,
55 info.begin_column,
56 false, info.must_hit,
58 info.id,
59 &details,
60 );
61 json!({ "antithesis_assert": &assertion })
62}
63
64pub struct TrackingInfo {
65 pub pass_count: AtomicU64,
66 pub fail_count: AtomicU64,
67}
68
69impl Default for TrackingInfo {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75impl TrackingInfo {
76 pub const fn new() -> Self {
77 TrackingInfo {
78 pass_count: AtomicU64::new(0),
79 fail_count: AtomicU64::new(0),
80 }
81 }
82}
83
84#[derive(Copy, Clone, PartialEq, Debug, Serialize)]
85#[serde(rename_all(serialize = "lowercase"))]
86pub enum AssertType {
87 Always,
88 Sometimes,
89 Reachability,
90}
91
92#[derive(Serialize, Debug)]
93struct AntithesisLocationInfo<'a> {
94 class: &'a str,
95 function: &'a str,
96 file: &'a str,
97 begin_line: u32,
98 begin_column: u32,
99}
100
101#[doc(hidden)]
103#[derive(Debug)]
104#[cfg(feature = "full")]
105pub struct AssertionCatalogInfo {
106 pub assert_type: AssertType,
107 pub display_type: &'static str,
108 pub condition: bool,
109 pub message: &'static str,
110 pub class: &'static str,
111 pub function: &'static Lazy<&'static str>,
112 pub file: &'static str,
113 pub begin_line: u32,
114 pub begin_column: u32,
115 pub must_hit: bool,
116 pub id: &'static str,
117}
118
119#[derive(Serialize)]
120struct AssertionInfo<'a> {
121 assert_type: AssertType,
122 display_type: &'a str,
123 condition: bool,
124 message: &'a str,
125 location: AntithesisLocationInfo<'a>,
126 hit: bool,
127 must_hit: bool,
128 id: &'a str,
129 #[serde(skip)]
131 details: &'a dyn Details,
132}
133
134impl<'a> AssertionInfo<'a> {
135 #[allow(clippy::too_many_arguments)]
136 pub fn new(
137 assert_type: AssertType,
138 display_type: &'a str,
139 condition: bool,
140 message: &'a str,
141 class: &'a str,
142 function: &'a str,
143 file: &'a str,
144 begin_line: u32,
145 begin_column: u32,
146 hit: bool,
147 must_hit: bool,
148 id: &'a str,
149 details: &'a dyn Details,
150 ) -> Self {
151 let location = AntithesisLocationInfo {
152 class,
153 function,
154 file,
155 begin_line,
156 begin_column,
157 };
158
159 AssertionInfo {
160 assert_type,
161 display_type,
162 condition,
163 message,
164 location,
165 hit,
166 must_hit,
167 id,
168 details
169 }
170 }
171}
172
173#[cfg(feature = "full")]
174impl AssertionInfo<'_> {
175 fn track_entry(&self, info: Option<&TrackingInfo>) {
191 if !self.hit {
193 self.emit();
194 return;
195 }
196
197 let emitting = match (info, self.condition) {
200 (None, _) => true,
201 (Some(info), true) => {
202 let prior_value = info.pass_count.fetch_add(1, Ordering::SeqCst);
203 prior_value == 0
204 }
205 (Some(info), false) => {
206 let prior_value = info.fail_count.fetch_add(1, Ordering::SeqCst);
207 prior_value == 0
208 }
209 };
210 if emitting {
211 Lazy::force(&INIT_CATALOG);
212 self.emit();
213 }
214 }
215
216 fn emit(&self) {
217 let mut assertion = serde_json::to_value(self).unwrap();
218 if self.hit {
219 if let Some(details) = self.details.object(self.id) {
220 assertion["details"] = Value::Object(details);
221 }
222 }
223 let json_event = json!({ "antithesis_assert": assertion });
224 internal::dispatch_output(&json_event)
225 }
226}
227
228#[cfg(not(feature = "full"))]
229impl AssertionInfo<'_> {
230 fn track_entry(&self, _info: Option<&TrackingInfo>) {
231 return
232 }
233}
234
235
236#[allow(clippy::too_many_arguments)]
353#[cfg(feature = "full")]
354pub fn assert_raw<T: Serialize>(
355 condition: bool,
356 message: String,
357 details: &T,
358 class: String,
359 function: String,
360 file: String,
361 begin_line: u32,
362 begin_column: u32,
363 hit: bool,
364 must_hit: bool,
365 assert_type: AssertType,
366 display_type: String,
367 id: String,
368) {
369 let info = raw_tracker_entry(&id);
373 assert_impl(
374 assert_type,
375 display_type.as_str(),
376 condition,
377 message.as_str(),
378 class.as_str(),
379 function.as_str(),
380 file.as_str(),
381 begin_line,
382 begin_column,
383 hit,
384 must_hit,
385 id.as_str(),
386 details,
387 Some(&*info),
388 )
389}
390
391#[cfg(feature = "full")]
394fn raw_tracker_entry(id: &str) -> Arc<TrackingInfo> {
395 static ASSERT_TRACKER: Lazy<RwLock<HashMap<String, Arc<TrackingInfo>>>> =
396 Lazy::new(|| RwLock::new(HashMap::new()));
397
398 let existing = ASSERT_TRACKER.read().unwrap().get(id).cloned();
400 match existing {
401 Some(info) => info,
402 None => ASSERT_TRACKER
403 .write()
404 .unwrap()
405 .entry(id.to_owned())
406 .or_default()
407 .clone(),
408 }
409}
410
411#[allow(clippy::too_many_arguments)]
412#[cfg(not(feature = "full"))]
413pub fn assert_raw<T: Serialize>(
414 condition: bool,
415 message: String,
416 details: &T,
417 class: String,
418 function: String,
419 file: String,
420 begin_line: u32,
421 begin_column: u32,
422 hit: bool,
423 must_hit: bool,
424 assert_type: AssertType,
425 display_type: String,
426 id: String,
427) {
428 assert_impl(
429 assert_type,
430 display_type.as_str(),
431 condition,
432 message.as_str(),
433 class.as_str(),
434 function.as_str(),
435 file.as_str(),
436 begin_line,
437 begin_column,
438 hit,
439 must_hit,
440 id.as_str(),
441 details,
442 None,
443 )
444}
445
446#[doc(hidden)]
447#[allow(clippy::too_many_arguments)]
448pub fn assert_impl<'a>(
449 assert_type: AssertType,
450 display_type: &'a str,
451 condition: bool,
452 message: &'a str,
453 class: &'a str,
454 function: &'a str,
455 file: &'a str,
456 begin_line: u32,
457 begin_column: u32,
458 hit: bool,
459 must_hit: bool,
460 id: &'a str,
461 details: &dyn Details,
462 info: Option<&TrackingInfo>,
463) {
464 let assertion = AssertionInfo::new(
465 assert_type,
466 display_type,
467 condition,
468 message,
469 class,
470 function,
471 file,
472 begin_line,
473 begin_column,
474 hit,
475 must_hit,
476 id,
477 details,
478 );
479
480 let _ = &assertion.track_entry(info);
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486 #[cfg(not(feature = "full"))] use serde_json::json;
487 #[cfg(not(feature = "full"))] use std::sync::atomic::Ordering;
488
489 #[test]
493 fn new_tracking_info() {
494 let ti = TrackingInfo::new();
495 assert_eq!(ti.pass_count.load(Ordering::SeqCst), 0);
496 assert_eq!(ti.fail_count.load(Ordering::SeqCst), 0);
497 }
498
499 #[test]
500 fn default_tracking_info() {
501 let ti: TrackingInfo = Default::default();
502 assert_eq!(ti.pass_count.load(Ordering::SeqCst), 0);
503 assert_eq!(ti.fail_count.load(Ordering::SeqCst), 0);
504 }
505
506 #[test]
511 fn new_assertion_info_always() {
512 let this_assert_type = AssertType::Always;
513 let this_display_type = "Always";
514 let this_condition = true;
515 let this_message = "Always message";
516 let this_class = "binary::always";
517 let this_function = "binary::always::always_function";
518 let this_file = "/home/user/binary/src/always_binary.rs";
519 let this_begin_line = 10;
520 let this_begin_column = 5;
521 let this_hit = true;
522 let this_must_hit = true;
523 let this_id = "ID Always message";
524 let this_details = json!({
525 "color": "always red",
526 "extent": 15,
527 });
528
529 let ai = AssertionInfo::new(
530 this_assert_type,
531 this_display_type,
532 this_condition,
533 this_message,
534 this_class,
535 this_function,
536 this_file,
537 this_begin_line,
538 this_begin_column,
539 this_hit,
540 this_must_hit,
541 this_id,
542 &this_details,
543 );
544 assert_eq!(ai.display_type, this_display_type);
545 assert_eq!(ai.condition, this_condition);
546 assert_eq!(ai.message, this_message);
547 assert_eq!(ai.location.class, this_class);
548 assert_eq!(ai.location.function, this_function);
549 assert_eq!(ai.location.file, this_file);
550 assert_eq!(ai.location.begin_line, this_begin_line);
551 assert_eq!(ai.location.begin_column, this_begin_column);
552 assert_eq!(ai.hit, this_hit);
553 assert_eq!(ai.must_hit, this_must_hit);
554 assert_eq!(ai.id, this_id);
555 #[cfg(feature = "full")]
556 assert_eq!(ai.details.object(ai.id), this_details.as_object().cloned());
557 }
558
559 #[test]
560 fn new_assertion_info_sometimes() {
561 let this_assert_type = AssertType::Sometimes;
562 let this_display_type = "Sometimes";
563 let this_condition = true;
564 let this_message = "Sometimes message";
565 let this_class = "binary::sometimes";
566 let this_function = "binary::sometimes::sometimes_function";
567 let this_file = "/home/user/binary/src/sometimes_binary.rs";
568 let this_begin_line = 11;
569 let this_begin_column = 6;
570 let this_hit = true;
571 let this_must_hit = true;
572 let this_id = "ID Sometimes message";
573 let this_details = json!({
574 "color": "sometimes red",
575 "extent": 17,
576 });
577
578 let ai = AssertionInfo::new(
579 this_assert_type,
580 this_display_type,
581 this_condition,
582 this_message,
583 this_class,
584 this_function,
585 this_file,
586 this_begin_line,
587 this_begin_column,
588 this_hit,
589 this_must_hit,
590 this_id,
591 &this_details,
592 );
593 assert_eq!(ai.display_type, this_display_type);
594 assert_eq!(ai.condition, this_condition);
595 assert_eq!(ai.message, this_message);
596 assert_eq!(ai.location.class, this_class);
597 assert_eq!(ai.location.function, this_function);
598 assert_eq!(ai.location.file, this_file);
599 assert_eq!(ai.location.begin_line, this_begin_line);
600 assert_eq!(ai.location.begin_column, this_begin_column);
601 assert_eq!(ai.hit, this_hit);
602 assert_eq!(ai.must_hit, this_must_hit);
603 assert_eq!(ai.id, this_id);
604 #[cfg(feature = "full")]
605 assert_eq!(ai.details.object(ai.id), this_details.as_object().cloned());
606 }
607
608 #[test]
609 fn new_assertion_info_reachable() {
610 let this_assert_type = AssertType::Reachability;
611 let this_display_type = "Reachable";
612 let this_condition = true;
613 let this_message = "Reachable message";
614 let this_class = "binary::reachable";
615 let this_function = "binary::reachable::reachable_function";
616 let this_file = "/home/user/binary/src/reachable_binary.rs";
617 let this_begin_line = 12;
618 let this_begin_column = 7;
619 let this_hit = true;
620 let this_must_hit = true;
621 let this_id = "ID Reachable message";
622 let this_details = json!({
623 "color": "reachable red",
624 "extent": 19,
625 });
626
627 let ai = AssertionInfo::new(
628 this_assert_type,
629 this_display_type,
630 this_condition,
631 this_message,
632 this_class,
633 this_function,
634 this_file,
635 this_begin_line,
636 this_begin_column,
637 this_hit,
638 this_must_hit,
639 this_id,
640 &this_details,
641 );
642 assert_eq!(ai.display_type, this_display_type);
643 assert_eq!(ai.condition, this_condition);
644 assert_eq!(ai.message, this_message);
645 assert_eq!(ai.location.class, this_class);
646 assert_eq!(ai.location.function, this_function);
647 assert_eq!(ai.location.file, this_file);
648 assert_eq!(ai.location.begin_line, this_begin_line);
649 assert_eq!(ai.location.begin_column, this_begin_column);
650 assert_eq!(ai.hit, this_hit);
651 assert_eq!(ai.must_hit, this_must_hit);
652 assert_eq!(ai.id, this_id);
653 #[cfg(feature = "full")]
654 assert_eq!(ai.details.object(ai.id), this_details.as_object().cloned());
655 }
656
657 #[test]
658 fn assert_impl_pass() {
659 let this_assert_type = AssertType::Always;
660 let this_display_type = "Always";
661 let this_condition = true;
662 let this_message = "Always message 2";
663 let this_class = "binary::always";
664 let this_function = "binary::always::always_function";
665 let this_file = "/home/user/binary/src/always_binary.rs";
666 let this_begin_line = 10;
667 let this_begin_column = 5;
668 let this_hit = true;
669 let this_must_hit = true;
670 let this_id = "ID Always message 2";
671 let this_details = json!({
672 "color": "always red",
673 "extent": 15,
674 });
675
676 let tracker = TrackingInfo::new();
677
678 let before_tracker = clone_tracker(&tracker);
679
680 assert_impl(
681 this_assert_type,
682 this_display_type,
683 this_condition,
684 this_message,
685 this_class,
686 this_function,
687 this_file,
688 this_begin_line,
689 this_begin_column,
690 this_hit,
691 this_must_hit,
692 this_id,
693 &this_details,
694 Some(&tracker),
695 );
696
697 let after_tracker: TrackingInfo = clone_tracker(&tracker);
698
699 if this_condition {
700 assert_eq!(before_tracker.pass_count.load(Ordering::SeqCst) + 1, after_tracker.pass_count.load(Ordering::SeqCst));
701 assert_eq!(before_tracker.fail_count.load(Ordering::SeqCst), after_tracker.fail_count.load(Ordering::SeqCst));
702 } else {
703 assert_eq!(before_tracker.fail_count.load(Ordering::SeqCst) + 1, after_tracker.fail_count.load(Ordering::SeqCst));
704 assert_eq!(before_tracker.pass_count.load(Ordering::SeqCst), after_tracker.pass_count.load(Ordering::SeqCst));
705 };
706 }
707
708 #[test]
709 fn assert_impl_fail() {
710 let this_assert_type = AssertType::Always;
711 let this_display_type = "Always";
712 let this_condition = false;
713 let this_message = "Always message 3";
714 let this_class = "binary::always";
715 let this_function = "binary::always::always_function";
716 let this_file = "/home/user/binary/src/always_binary.rs";
717 let this_begin_line = 10;
718 let this_begin_column = 5;
719 let this_hit = true;
720 let this_must_hit = true;
721 let this_id = "ID Always message 3";
722 let this_details = json!({
723 "color": "always red",
724 "extent": 15,
725 });
726
727 let tracker = TrackingInfo::new();
728
729 let before_tracker = clone_tracker(&tracker);
730
731 assert_impl(
732 this_assert_type,
733 this_display_type,
734 this_condition,
735 this_message,
736 this_class,
737 this_function,
738 this_file,
739 this_begin_line,
740 this_begin_column,
741 this_hit,
742 this_must_hit,
743 this_id,
744 &this_details,
745 Some(&tracker),
746 );
747
748 let after_tracker: TrackingInfo = clone_tracker(&tracker);
749
750 if this_condition {
751 assert_eq!(before_tracker.pass_count.load(Ordering::SeqCst) + 1, after_tracker.pass_count.load(Ordering::SeqCst));
752 assert_eq!(before_tracker.fail_count.load(Ordering::SeqCst), after_tracker.fail_count.load(Ordering::SeqCst));
753 } else {
754 assert_eq!(before_tracker.fail_count.load(Ordering::SeqCst) + 1, after_tracker.fail_count.load(Ordering::SeqCst));
755 assert_eq!(before_tracker.pass_count.load(Ordering::SeqCst), after_tracker.pass_count.load(Ordering::SeqCst));
756 };
757 }
758
759 fn clone_tracker(old: &TrackingInfo) -> TrackingInfo {
760 let tracking_data = TrackingInfo::new();
761 tracking_data.pass_count.store(old.pass_count.load(Ordering::SeqCst), Ordering::SeqCst);
762 tracking_data.fail_count.store(old.fail_count.load(Ordering::SeqCst), Ordering::SeqCst);
763 tracking_data
764
765 }
766}