Skip to main content

antithesis_sdk/assert/
mod.rs

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/// Catalog of all antithesis assertions provided
22#[doc(hidden)]
23#[distributed_slice]
24#[cfg(feature = "full")]
25pub static ANTITHESIS_CATALOG: [AssertionCatalogInfo];
26
27/// Catalog of all antithesis guidances provided
28#[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/// The catalog-registration event for one assertion declaration, emitted by
41/// both `antithesis_init()` and `catalog::write`.
42#[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, /* hit */
57        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/// Internal representation for assertion catalog
102#[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    // Serialized separately so declarations never evaluate the details serializer.
130    #[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    // AssertionInfo::track_entry() determines if the assertion should
176    // actually be emitted:
177    //
178    // [X] If this is an assertion catalog
179    // registration (assertion.hit == false) then it is emitted.
180    //
181    // [X] if `condition` is true increment the tracker_entry.pass_count,
182    // otherwise increment the tracker_entry.fail_count.
183    //
184    // [X] if `condition` is true and tracker_entry_pass_count == 1 then
185    // actually emit the assertion.
186    //
187    // [X] if `condition` is false and tracker_entry_fail_count == 1 then
188    // actually emit the assertion.
189
190    fn track_entry(&self, info: Option<&TrackingInfo>) {
191        // Requirement: Catalog entries must always will emit()
192        if !self.hit {
193            self.emit();
194            return;
195        }
196
197        // Record the condition in the associated TrackingInfo entry,
198        // and emit the assertion when first seeing a condition
199        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/// This is a low-level method designed to be used by third-party frameworks.
237/// Regular users of the assert package should not call it.
238///
239/// This is primarily intended for use by adapters from other diagnostic tools
240/// that intend to output Antithesis-style assertions.
241///
242/// Be certain to provide an assertion catalog entry for each assertion issued
243/// with ``assert_raw()``.  Assertion catalog entries are also created using
244/// ``assert_raw()``, by setting the value of the ``hit`` parameter to false.
245///
246/// Details may be any borrowed value implementing `Serialize`, handled as
247/// the assertion macros handle theirs: objects are preserved, other non-null
248/// values are wrapped under `"value"`, `&Value::Null` means no details, and
249/// a serialization error emits an `antithesis_error` naming the assertion
250/// `id` while the assertion itself is still emitted without details.
251/// Catalog entries (`hit == false`) never serialize details.
252///
253/// Please refer to the general Antithesis documentation regarding the use of
254/// the
255/// [Fallback SDK](https://antithesis.com/docs/using_antithesis/sdk/fallback/assert/)
256/// for additional information.
257///
258///
259///
260/// # Example
261///
262/// ```
263/// use serde_json::{json};
264/// use antithesis_sdk::{assert, random};
265///
266/// struct Votes {
267///     num_voters: u32,
268///     candidate_1: u32,
269///     candidate_2: u32,
270/// }
271///
272/// fn main() {
273///     establish_catalog();
274///    
275///     let mut all_votes = Votes {
276///         num_voters: 0,
277///         candidate_1: 0,
278///         candidate_2: 0,
279///     };
280///
281///     for _voter in 0..100 {
282///         tally_vote(&mut all_votes, random_bool(), random_bool());
283///     }
284/// }
285///
286/// fn random_bool() -> bool {
287///     let v1 = random::get_random() % 2;
288///     v1 == 1
289/// }
290///
291/// fn establish_catalog() {
292///     assert::assert_raw(
293///         false,                            /* condition */
294///         "Never extra votes".to_owned(),   /* message */
295///         &json!({}),                       /* details */
296///         "mycrate::stuff".to_owned(),      /* class */
297///         "mycrate::tally_vote".to_owned(), /* function */
298///         "src/voting.rs".to_owned(),       /* file */
299///         20,                               /* line */
300///         3,                                /* column */
301///         false,                            /* hit */
302///         true,                             /* must_hit */
303///         assert::AssertType::Always,       /* assert_type */
304///         "Always".to_owned(),              /* display_type */
305///         "42-1005".to_owned()              /* id */
306///     );
307/// }
308///
309/// fn tally_vote(votes: &mut Votes, candidate_1: bool, candidate_2: bool) {
310///     if candidate_1 || candidate_2 {
311///         votes.num_voters += 1;
312///     }
313///     if candidate_1 {
314///         votes.candidate_1 += 1;
315///     };
316///     if candidate_2 {
317///         votes.candidate_2 += 1;
318///     };
319///
320///     let num_votes = votes.candidate_1 + votes.candidate_2;
321///     assert::assert_raw(
322///         num_votes == votes.num_voters,    /* condition */
323///         "Never extra votes".to_owned(),   /* message */
324///         &json!({                          /* details */
325///             "votes": num_votes,
326///             "voters": votes.num_voters
327///         }),                        
328///         "mycrate::stuff".to_owned(),      /* class */
329///         "mycrate::tally_vote".to_owned(), /* function */
330///         "src/voting.rs".to_owned(),       /* file */
331///         20,                               /* line */
332///         3,                                /* column */
333///         true,                             /* hit */
334///         true,                             /* must_hit */
335///         assert::AssertType::Always,       /* assert_type */
336///         "Always".to_owned(),              /* display_type */
337///         "42-1005".to_owned()              /* id */
338///     );
339/// }
340///
341/// // Run example with output to /tmp/x7.json
342/// // ANTITHESIS_SDK_LOCAL_OUTPUT=/tmp/x7.json cargo test --doc
343/// //
344/// // Example output from /tmp/x7.json
345/// // Contents may vary due to use of random::get_random()
346/// //
347/// // {"antithesis_sdk":{"language":{"name":"Rust","version":"1.69.0"},"sdk_version":"0.1.2","protocol_version":"1.0.0"}}
348/// // {"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"}
349/// // {"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}}
350/// // {"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}}
351/// ```
352#[allow(clippy::too_many_arguments)]
353#[cfg(feature = "full")]
354pub fn assert_raw<T: Details>(
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    // Only this thin shim is generic (and so compiled into the caller's
370    // crate); the tracker lookup and everything downstream are erased
371    // behind `dyn Details` and compiled once, here.
372    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// The per-id tracking entries of raw assertions (the macros hold theirs in
392// a static per call site).
393#[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    // The read guard must drop before the write lock is taken.
399    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: Details>(
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}