antithesis_sdk/assert/
mod.rs

1use std::sync::atomic::AtomicU64;
2#[cfg(feature = "full")]
3use std::{collections::HashMap, sync::{atomic::Ordering, Arc, Mutex}};
4#[cfg(feature = "full")]
5use crate::internal;
6#[cfg(feature = "full")]
7use linkme::distributed_slice;
8#[cfg(feature = "full")]
9use once_cell::sync::Lazy;
10use serde::Serialize;
11use serde_json::Value;
12#[cfg(feature = "full")]
13use serde_json::json;
14
15mod macros;
16#[doc(hidden)]
17#[cfg(feature = "full")]
18pub mod guidance;
19
20/// Catalog of all antithesis assertions provided
21#[doc(hidden)]
22#[distributed_slice]
23#[cfg(feature = "full")]
24pub static ANTITHESIS_CATALOG: [AssertionCatalogInfo];
25
26/// Catalog of all antithesis guidances provided
27#[doc(hidden)]
28#[distributed_slice]
29#[cfg(feature = "full")]
30pub static ANTITHESIS_GUIDANCE_CATALOG: [self::guidance::GuidanceCatalogInfo];
31
32#[cfg(feature = "full")]
33pub(crate) static INIT_CATALOG: Lazy<()> = Lazy::new(|| {
34    for info in ANTITHESIS_CATALOG.iter() {
35        let f_name: &str = info.function.as_ref();
36        assert_impl(
37            info.assert_type,
38            info.display_type,
39            info.condition,
40            info.message,
41            info.class,
42            f_name,
43            info.file,
44            info.begin_line,
45            info.begin_column,
46            false, /* hit */
47            info.must_hit,
48            info.id,
49            &json!(null),
50            None,
51        );
52    }
53    for info in ANTITHESIS_GUIDANCE_CATALOG.iter() {
54        guidance::guidance_impl(
55            info.guidance_type,
56            info.message,
57            info.id,
58            info.class,
59            #[allow(clippy::explicit_auto_deref)]
60            *Lazy::force(info.function),
61            info.file,
62            info.begin_line,
63            info.begin_column,
64            info.maximize,
65            json!(null),
66            false,
67        )
68    }
69});
70
71pub struct TrackingInfo {
72    pub pass_count: AtomicU64,
73    pub fail_count: AtomicU64,
74}
75
76impl Default for TrackingInfo {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl TrackingInfo {
83    pub const fn new() -> Self {
84        TrackingInfo {
85            pass_count: AtomicU64::new(0),
86            fail_count: AtomicU64::new(0),
87        }
88    }
89}
90
91#[derive(Copy, Clone, PartialEq, Debug, Serialize)]
92#[serde(rename_all(serialize = "lowercase"))]
93pub enum AssertType {
94    Always,
95    Sometimes,
96    Reachability,
97}
98
99#[derive(Serialize, Debug)]
100struct AntithesisLocationInfo<'a> {
101    class: &'a str,
102    function: &'a str,
103    file: &'a str,
104    begin_line: u32,
105    begin_column: u32,
106}
107
108/// Internal representation for assertion catalog
109#[doc(hidden)]
110#[derive(Debug)]
111#[cfg(feature = "full")]
112pub struct AssertionCatalogInfo {
113    pub assert_type: AssertType,
114    pub display_type: &'static str,
115    pub condition: bool,
116    pub message: &'static str,
117    pub class: &'static str,
118    pub function: &'static Lazy<&'static str>,
119    pub file: &'static str,
120    pub begin_line: u32,
121    pub begin_column: u32,
122    pub must_hit: bool,
123    pub id: &'static str,
124}
125
126#[derive(Serialize, Debug)]
127struct AssertionInfo<'a, S: Serialize> {
128    assert_type: AssertType,
129    display_type: &'a str,
130    condition: bool,
131    message: &'a str,
132    location: AntithesisLocationInfo<'a>,
133    hit: bool,
134    must_hit: bool,
135    id: &'a str,
136    details: &'a S,
137}
138
139impl<'a, S: Serialize> AssertionInfo<'a, S> {
140    #[allow(clippy::too_many_arguments)]
141    pub fn new(
142        assert_type: AssertType,
143        display_type: &'a str,
144        condition: bool,
145        message: &'a str,
146        class: &'a str,
147        function: &'a str,
148        file: &'a str,
149        begin_line: u32,
150        begin_column: u32,
151        hit: bool,
152        must_hit: bool,
153        id: &'a str,
154        details: &'a S,
155    ) -> Self {
156        let location = AntithesisLocationInfo {
157            class,
158            function,
159            file,
160            begin_line,
161            begin_column,
162        };
163
164        AssertionInfo {
165            assert_type,
166            display_type,
167            condition,
168            message,
169            location,
170            hit,
171            must_hit,
172            id,
173            details
174        }
175    }
176} 
177
178#[cfg(feature = "full")]
179impl<S: Serialize> AssertionInfo<'_, S> {
180    // AssertionInfo::track_entry() determines if the assertion should
181    // actually be emitted:
182    //
183    // [X] If this is an assertion catalog
184    // registration (assertion.hit == false) then it is emitted.
185    //
186    // [X] if `condition` is true increment the tracker_entry.pass_count,
187    // otherwise increment the tracker_entry.fail_count.
188    //
189    // [X] if `condition` is true and tracker_entry_pass_count == 1 then
190    // actually emit the assertion.
191    //
192    // [X] if `condition` is false and tracker_entry_fail_count == 1 then
193    // actually emit the assertion.
194
195    fn track_entry(&self, info: Option<&TrackingInfo>) {
196        // Requirement: Catalog entries must always will emit()
197        if !self.hit {
198            self.emit();
199            return;
200        }
201
202        // Record the condition in the associated TrackingInfo entry,
203        // and emit the assertion when first seeing a condition
204        let emitting = match (info, self.condition) {
205            (None, _) => true,
206            (Some(info), true) => {
207                let prior_value = info.pass_count.fetch_add(1, Ordering::SeqCst);
208                prior_value == 0
209            }
210            (Some(info), false) => {
211                let prior_value = info.fail_count.fetch_add(1, Ordering::SeqCst);
212                prior_value == 0
213            }
214        };
215        if emitting {
216            Lazy::force(&INIT_CATALOG);
217            self.emit();
218        }
219    }
220
221    fn emit(&self) {
222        let json_event = json!({ "antithesis_assert": &self });
223        internal::dispatch_output(&json_event)
224    }
225}
226
227#[cfg(not(feature = "full"))]
228impl<S: Serialize> AssertionInfo<'_, S> {
229    fn track_entry(&self, _info: Option<&TrackingInfo>) {
230        return
231    }
232}
233
234
235/// This is a low-level method designed to be used by third-party frameworks.
236/// Regular users of the assert package should not call it.
237///
238/// This is primarily intended for use by adapters from other
239/// diagnostic tools that intend to output Antithesis-style
240/// assertions.
241///
242/// Be certain to provide an assertion catalog entry
243/// for each assertion issued with ``assert_raw()``.  Assertion catalog
244/// entries are also created using ``assert_raw()``, by setting the value
245/// of the ``hit`` parameter to false.
246///
247/// Please refer to the general Antithesis documentation regarding the
248/// use of the [Fallback SDK](https://antithesis.com/docs/using_antithesis/sdk/fallback/assert/)
249/// for additional information.
250///
251///
252///
253/// # Example
254///
255/// ```
256/// use serde_json::{json};
257/// use antithesis_sdk::{assert, random};
258///
259/// struct Votes {
260///     num_voters: u32,
261///     candidate_1: u32,
262///     candidate_2: u32,
263/// }
264///
265/// fn main() {
266///     establish_catalog();
267///    
268///     let mut all_votes = Votes {
269///         num_voters: 0,
270///         candidate_1: 0,
271///         candidate_2: 0,
272///     };
273///
274///     for _voter in 0..100 {
275///         tally_vote(&mut all_votes, random_bool(), random_bool());
276///     }
277/// }
278///
279/// fn random_bool() -> bool {
280///     let v1 = random::get_random() % 2;
281///     v1 == 1
282/// }
283///
284/// fn establish_catalog() {
285///     assert::assert_raw(
286///         false,                            /* condition */
287///         "Never extra votes".to_owned(),   /* message */
288///         &json!({}),                       /* details */
289///         "mycrate::stuff".to_owned(),      /* class */
290///         "mycrate::tally_vote".to_owned(), /* function */
291///         "src/voting.rs".to_owned(),       /* file */
292///         20,                               /* line */
293///         3,                                /* column */
294///         false,                            /* hit */
295///         true,                             /* must_hit */
296///         assert::AssertType::Always,       /* assert_type */
297///         "Always".to_owned(),              /* display_type */
298///         "42-1005".to_owned()              /* id */
299///     );
300/// }
301///
302/// fn tally_vote(votes: &mut Votes, candidate_1: bool, candidate_2: bool) {
303///     if candidate_1 || candidate_2 {
304///         votes.num_voters += 1;
305///     }
306///     if candidate_1 {
307///         votes.candidate_1 += 1;
308///     };
309///     if candidate_2 {
310///         votes.candidate_2 += 1;
311///     };
312///
313///     let num_votes = votes.candidate_1 + votes.candidate_2;
314///     assert::assert_raw(
315///         num_votes == votes.num_voters,    /* condition */
316///         "Never extra votes".to_owned(),   /* message */
317///         &json!({                          /* details */
318///             "votes": num_votes,
319///             "voters": votes.num_voters
320///         }),                        
321///         "mycrate::stuff".to_owned(),      /* class */
322///         "mycrate::tally_vote".to_owned(), /* function */
323///         "src/voting.rs".to_owned(),       /* file */
324///         20,                               /* line */
325///         3,                                /* column */
326///         true,                             /* hit */
327///         true,                             /* must_hit */
328///         assert::AssertType::Always,       /* assert_type */
329///         "Always".to_owned(),              /* display_type */
330///         "42-1005".to_owned()              /* id */
331///     );
332/// }
333///
334/// // Run example with output to /tmp/x7.json
335/// // ANTITHESIS_SDK_LOCAL_OUTPUT=/tmp/x7.json cargo test --doc
336/// //
337/// // Example output from /tmp/x7.json
338/// // Contents may vary due to use of random::get_random()
339/// //
340/// // {"antithesis_sdk":{"language":{"name":"Rust","version":"1.69.0"},"sdk_version":"0.1.2","protocol_version":"1.0.0"}}
341/// // {"assert_type":"always","display_type":"Always","condition":false,"message":"Never extra votes","location":{"class":"mycrate::stuff","function":"mycrate::tally_vote","file":"src/voting.rs","begin_line":20,"begin_column":3},"hit":false,"must_hit":true,"id":"42-1005","details":{}}
342/// // {"assert_type":"always","display_type":"Always","condition":true,"message":"Never extra votes","location":{"class":"mycrate::stuff","function":"mycrate::tally_vote","file":"src/voting.rs","begin_line":20,"begin_column":3},"hit":true,"must_hit":true,"id":"42-1005","details":{"voters":1,"votes":1}}
343/// // {"assert_type":"always","display_type":"Always","condition":false,"message":"Never extra votes","location":{"class":"mycrate::stuff","function":"mycrate::tally_vote","file":"src/voting.rs","begin_line":20,"begin_column":3},"hit":true,"must_hit":true,"id":"42-1005","details":{"voters":3,"votes":4}}
344/// ```
345#[allow(clippy::too_many_arguments)]
346#[cfg(feature = "full")]
347pub fn assert_raw(
348    condition: bool,
349    message: String,
350    details: &Value,
351    class: String,
352    function: String,
353    file: String,
354    begin_line: u32,
355    begin_column: u32,
356    hit: bool,
357    must_hit: bool,
358    assert_type: AssertType,
359    display_type: String,
360    id: String,
361) {
362    static ASSERT_TRACKER: Lazy<Mutex<HashMap<String, Arc<TrackingInfo>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
363
364    // Establish TrackingInfo for this trackingKey when needed
365    let info = {
366        let mut tracker = ASSERT_TRACKER.lock().unwrap();
367        if !tracker.contains_key(&id) {
368            tracker.insert(id.clone(), Arc::new(TrackingInfo::default()));
369        }
370        tracker.get(&id).unwrap().clone()
371    };
372
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#[allow(clippy::too_many_arguments)]
392#[cfg(not(feature = "full"))]
393pub fn assert_raw(
394    condition: bool,
395    message: String,
396    details: &Value,
397    class: String,
398    function: String,
399    file: String,
400    begin_line: u32,
401    begin_column: u32,
402    hit: bool,
403    must_hit: bool,
404    assert_type: AssertType,
405    display_type: String,
406    id: String,
407) {
408    assert_impl(
409        assert_type,
410        display_type.as_str(),
411        condition,
412        message.as_str(),
413        class.as_str(),
414        function.as_str(),
415        file.as_str(),
416        begin_line,
417        begin_column,
418        hit,
419        must_hit,
420        id.as_str(),
421        details,
422        None,
423    )
424}
425
426#[doc(hidden)]
427#[allow(clippy::too_many_arguments)]
428pub fn assert_impl<'a, S: Serialize>(
429    assert_type: AssertType,
430    display_type: &'a str,
431    condition: bool,
432    message: &'a str,
433    class: &'a str,
434    function: &'a str,
435    file: &'a str,
436    begin_line: u32,
437    begin_column: u32,
438    hit: bool,
439    must_hit: bool,
440    id: &'a str,
441    details: &S,
442    info: Option<&TrackingInfo>,
443) {
444    let assertion = AssertionInfo::new(
445        assert_type,
446        display_type,
447        condition,
448        message,
449        class,
450        function,
451        file,
452        begin_line,
453        begin_column,
454        hit,
455        must_hit,
456        id,
457        details,
458    );
459
460    let _ = &assertion.track_entry(info);
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    //--------------------------------------------------------------------------------
468    // Tests for TrackingInfo
469    //--------------------------------------------------------------------------------
470    #[test]
471    fn new_tracking_info() {
472        let ti = TrackingInfo::new();
473        assert_eq!(ti.pass_count.load(Ordering::SeqCst), 0);
474        assert_eq!(ti.fail_count.load(Ordering::SeqCst), 0);
475    }
476
477    #[test]
478    fn default_tracking_info() {
479        let ti: TrackingInfo = Default::default();
480        assert_eq!(ti.pass_count.load(Ordering::SeqCst), 0);
481        assert_eq!(ti.fail_count.load(Ordering::SeqCst), 0);
482    }
483
484    //--------------------------------------------------------------------------------
485    // Tests for AssertionInfo
486    //--------------------------------------------------------------------------------
487
488    #[test]
489    fn new_assertion_info_always() {
490        let this_assert_type = AssertType::Always;
491        let this_display_type = "Always";
492        let this_condition = true;
493        let this_message = "Always message";
494        let this_class = "binary::always";
495        let this_function = "binary::always::always_function";
496        let this_file = "/home/user/binary/src/always_binary.rs";
497        let this_begin_line = 10;
498        let this_begin_column = 5;
499        let this_hit = true;
500        let this_must_hit = true;
501        let this_id = "ID Always message";
502        let this_details = json!({
503            "color": "always red",
504            "extent": 15,
505        });
506
507        let ai = AssertionInfo::new(
508            this_assert_type,
509            this_display_type,
510            this_condition,
511            this_message,
512            this_class,
513            this_function,
514            this_file,
515            this_begin_line,
516            this_begin_column,
517            this_hit,
518            this_must_hit,
519            this_id,
520            &this_details,
521        );
522        assert_eq!(ai.display_type, this_display_type);
523        assert_eq!(ai.condition, this_condition);
524        assert_eq!(ai.message, this_message);
525        assert_eq!(ai.location.class, this_class);
526        assert_eq!(ai.location.function, this_function);
527        assert_eq!(ai.location.file, this_file);
528        assert_eq!(ai.location.begin_line, this_begin_line);
529        assert_eq!(ai.location.begin_column, this_begin_column);
530        assert_eq!(ai.hit, this_hit);
531        assert_eq!(ai.must_hit, this_must_hit);
532        assert_eq!(ai.id, this_id);
533        assert_eq!(ai.details, &this_details);
534    }
535
536    #[test]
537    fn new_assertion_info_sometimes() {
538        let this_assert_type = AssertType::Sometimes;
539        let this_display_type = "Sometimes";
540        let this_condition = true;
541        let this_message = "Sometimes message";
542        let this_class = "binary::sometimes";
543        let this_function = "binary::sometimes::sometimes_function";
544        let this_file = "/home/user/binary/src/sometimes_binary.rs";
545        let this_begin_line = 11;
546        let this_begin_column = 6;
547        let this_hit = true;
548        let this_must_hit = true;
549        let this_id = "ID Sometimes message";
550        let this_details = json!({
551            "color": "sometimes red",
552            "extent": 17,
553        });
554
555        let ai = AssertionInfo::new(
556            this_assert_type,
557            this_display_type,
558            this_condition,
559            this_message,
560            this_class,
561            this_function,
562            this_file,
563            this_begin_line,
564            this_begin_column,
565            this_hit,
566            this_must_hit,
567            this_id,
568            &this_details,
569        );
570        assert_eq!(ai.display_type, this_display_type);
571        assert_eq!(ai.condition, this_condition);
572        assert_eq!(ai.message, this_message);
573        assert_eq!(ai.location.class, this_class);
574        assert_eq!(ai.location.function, this_function);
575        assert_eq!(ai.location.file, this_file);
576        assert_eq!(ai.location.begin_line, this_begin_line);
577        assert_eq!(ai.location.begin_column, this_begin_column);
578        assert_eq!(ai.hit, this_hit);
579        assert_eq!(ai.must_hit, this_must_hit);
580        assert_eq!(ai.id, this_id);
581        assert_eq!(ai.details, &this_details);
582    }
583
584    #[test]
585    fn new_assertion_info_reachable() {
586        let this_assert_type = AssertType::Reachability;
587        let this_display_type = "Reachable";
588        let this_condition = true;
589        let this_message = "Reachable message";
590        let this_class = "binary::reachable";
591        let this_function = "binary::reachable::reachable_function";
592        let this_file = "/home/user/binary/src/reachable_binary.rs";
593        let this_begin_line = 12;
594        let this_begin_column = 7;
595        let this_hit = true;
596        let this_must_hit = true;
597        let this_id = "ID Reachable message";
598        let this_details = json!({
599            "color": "reachable red",
600            "extent": 19,
601        });
602
603        let ai = AssertionInfo::new(
604            this_assert_type,
605            this_display_type,
606            this_condition,
607            this_message,
608            this_class,
609            this_function,
610            this_file,
611            this_begin_line,
612            this_begin_column,
613            this_hit,
614            this_must_hit,
615            this_id,
616            &this_details,
617        );
618        assert_eq!(ai.display_type, this_display_type);
619        assert_eq!(ai.condition, this_condition);
620        assert_eq!(ai.message, this_message);
621        assert_eq!(ai.location.class, this_class);
622        assert_eq!(ai.location.function, this_function);
623        assert_eq!(ai.location.file, this_file);
624        assert_eq!(ai.location.begin_line, this_begin_line);
625        assert_eq!(ai.location.begin_column, this_begin_column);
626        assert_eq!(ai.hit, this_hit);
627        assert_eq!(ai.must_hit, this_must_hit);
628        assert_eq!(ai.id, this_id);
629        assert_eq!(ai.details, &this_details);
630    }
631
632    #[test]
633    fn assert_impl_pass() {
634        let this_assert_type = AssertType::Always;
635        let this_display_type = "Always";
636        let this_condition = true;
637        let this_message = "Always message 2";
638        let this_class = "binary::always";
639        let this_function = "binary::always::always_function";
640        let this_file = "/home/user/binary/src/always_binary.rs";
641        let this_begin_line = 10;
642        let this_begin_column = 5;
643        let this_hit = true;
644        let this_must_hit = true;
645        let this_id = "ID Always message 2";
646        let this_details = json!({
647            "color": "always red",
648            "extent": 15,
649        });
650
651        let tracker = TrackingInfo::new();
652
653        let before_tracker = clone_tracker(&tracker);
654
655        assert_impl(
656            this_assert_type,
657            this_display_type,
658            this_condition,
659            this_message,
660            this_class,
661            this_function,
662            this_file,
663            this_begin_line,
664            this_begin_column,
665            this_hit,
666            this_must_hit,
667            this_id,
668            &this_details,
669            Some(&tracker),
670        );
671
672        let after_tracker: TrackingInfo = clone_tracker(&tracker);
673
674        if this_condition {
675            assert_eq!(before_tracker.pass_count.load(Ordering::SeqCst) + 1, after_tracker.pass_count.load(Ordering::SeqCst));
676            assert_eq!(before_tracker.fail_count.load(Ordering::SeqCst), after_tracker.fail_count.load(Ordering::SeqCst));
677        } else {
678            assert_eq!(before_tracker.fail_count.load(Ordering::SeqCst) + 1, after_tracker.fail_count.load(Ordering::SeqCst));
679            assert_eq!(before_tracker.pass_count.load(Ordering::SeqCst), after_tracker.pass_count.load(Ordering::SeqCst));
680        };
681    }
682
683    #[test]
684    fn assert_impl_fail() {
685        let this_assert_type = AssertType::Always;
686        let this_display_type = "Always";
687        let this_condition = false;
688        let this_message = "Always message 3";
689        let this_class = "binary::always";
690        let this_function = "binary::always::always_function";
691        let this_file = "/home/user/binary/src/always_binary.rs";
692        let this_begin_line = 10;
693        let this_begin_column = 5;
694        let this_hit = true;
695        let this_must_hit = true;
696        let this_id = "ID Always message 3";
697        let this_details = json!({
698            "color": "always red",
699            "extent": 15,
700        });
701
702        let tracker = TrackingInfo::new();
703
704        let before_tracker = clone_tracker(&tracker);
705
706        assert_impl(
707            this_assert_type,
708            this_display_type,
709            this_condition,
710            this_message,
711            this_class,
712            this_function,
713            this_file,
714            this_begin_line,
715            this_begin_column,
716            this_hit,
717            this_must_hit,
718            this_id,
719            &this_details,
720            Some(&tracker),
721        );
722
723        let after_tracker: TrackingInfo = clone_tracker(&tracker);
724
725        if this_condition {
726            assert_eq!(before_tracker.pass_count.load(Ordering::SeqCst) + 1, after_tracker.pass_count.load(Ordering::SeqCst));
727            assert_eq!(before_tracker.fail_count.load(Ordering::SeqCst), after_tracker.fail_count.load(Ordering::SeqCst));
728        } else {
729            assert_eq!(before_tracker.fail_count.load(Ordering::SeqCst) + 1, after_tracker.fail_count.load(Ordering::SeqCst));
730            assert_eq!(before_tracker.pass_count.load(Ordering::SeqCst), after_tracker.pass_count.load(Ordering::SeqCst));
731        };
732    }
733
734    fn clone_tracker(old: &TrackingInfo) -> TrackingInfo {
735        let tracking_data = TrackingInfo::new();
736        tracking_data.pass_count.store(old.pass_count.load(Ordering::SeqCst), Ordering::SeqCst);
737        tracking_data.fail_count.store(old.fail_count.load(Ordering::SeqCst), Ordering::SeqCst);
738        tracking_data
739
740    }
741}