antithesis_sdk/assert/
mod.rs

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