antithesis_sdk/catalog.rs
1use std::io;
2
3#[cfg(feature = "full")]
4use crate::assert::{self, guidance};
5#[cfg(feature = "full")]
6use once_cell::sync::Lazy;
7#[cfg(feature = "full")]
8use serde_json::Value;
9
10/// Which assertion macro a declaration came from.
11///
12/// This is the typed form of the `display_type` field in the serialized
13/// event; [`display_type()`](Self::display_type) gives that string back. It
14/// is distinct from [`AssertType`](crate::assert::AssertType), the
15/// three-way wire-level enum, which cannot tell [`assert_always!`] from
16/// [`assert_always_or_unreachable!`] or [`assert_reachable!`] from
17/// [`assert_unreachable!`] without also consulting `must_hit`.
18///
19/// The numeric and boolean macros (`assert_always_greater_than!`,
20/// `assert_sometimes_all!`, and so on) are ordinary `Always` or `Sometimes`
21/// assertions with guidance attached, and report as such.
22///
23/// [`assert_always!`]: crate::assert_always
24/// [`assert_always_or_unreachable!`]: crate::assert_always_or_unreachable
25/// [`assert_reachable!`]: crate::assert_reachable
26/// [`assert_unreachable!`]: crate::assert_unreachable
27#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
28#[non_exhaustive]
29pub enum AssertionKind {
30 /// [`assert_always!`](crate::assert_always): the condition must hold on
31 /// every evaluation, and the assertion must be reached.
32 Always,
33 /// [`assert_always_or_unreachable!`](crate::assert_always_or_unreachable):
34 /// the condition must hold on every evaluation, but the assertion need
35 /// never be reached.
36 AlwaysOrUnreachable,
37 /// [`assert_sometimes!`](crate::assert_sometimes): the condition must
38 /// hold on at least one evaluation.
39 Sometimes,
40 /// [`assert_reachable!`](crate::assert_reachable): the assertion must be
41 /// reached at least once.
42 Reachable,
43 /// [`assert_unreachable!`](crate::assert_unreachable): the assertion must
44 /// never be reached.
45 Unreachable,
46}
47
48impl AssertionKind {
49 /// The `display_type` string that serialized events carry for this kind,
50 /// which is also how the triage report labels it. Use it to match catalog
51 /// entries against events read back from a run.
52 pub const fn display_type(self) -> &'static str {
53 match self {
54 AssertionKind::Always => "Always",
55 AssertionKind::AlwaysOrUnreachable => "AlwaysOrUnreachable",
56 AssertionKind::Sometimes => "Sometimes",
57 AssertionKind::Reachable => "Reachable",
58 AssertionKind::Unreachable => "Unreachable",
59 }
60 }
61}
62
63/// Where an assertion was declared, as captured by the macro at compile time.
64///
65/// `file` is whatever `file!()` produced when the crate was compiled, so its
66/// form (relative or absolute, remapped or not) depends on how the build was
67/// invoked. `class` is the enclosing module path and `function` the path of
68/// the enclosing function.
69///
70/// Locations order by file, line, and column, then module and function.
71#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
72#[non_exhaustive]
73pub struct SourceLocation {
74 pub file: &'static str,
75 pub begin_line: u32,
76 pub begin_column: u32,
77 pub class: &'static str,
78 pub function: &'static str,
79}
80
81/// One assertion declaration compiled into this binary.
82///
83/// There is one per assertion macro call site linked into the program,
84/// whether or not that code ever runs: the catalog is a static property of
85/// the binary, not of any particular execution. The numeric and boolean
86/// macros each contribute one assertion. The guidance they additionally
87/// register is not an assertion: [`antithesis_init()`](crate::antithesis_init)
88/// registers it with the platform, but this module does not list it.
89///
90/// Catalog entries registered at runtime through
91/// [`assert_raw`](crate::assert::assert_raw) are not part of the compiled-in
92/// catalog and do not appear.
93#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
94#[non_exhaustive]
95pub struct Assertion {
96 /// The key under which Antithesis aggregates this assertion's evaluations
97 /// into one test property. Currently equal to `message`; treat it as
98 /// opaque.
99 pub id: &'static str,
100 /// The name of the test property, as shown in the triage report.
101 pub message: &'static str,
102 pub kind: AssertionKind,
103 pub location: SourceLocation,
104}
105
106/// Every assertion declaration compiled into this binary.
107///
108/// These are the assertions that [`antithesis_init()`](crate::antithesis_init)
109/// registers, read without emitting anything and without init having run.
110/// The order is the linker's, the same order init registers them in: fixed
111/// for a given binary, but free to change between builds. Sort by
112/// [`Assertion::location`] if you need an order that survives rebuilds.
113///
114/// Without the `full` feature the catalog is always empty.
115///
116/// ```
117/// use antithesis_sdk::{assert_sometimes, catalog};
118///
119/// // Never called, yet its assertion is still in the catalog.
120/// #[allow(dead_code)]
121/// fn never_called(x: u64) {
122/// assert_sometimes!(x > 3, "x exceeds three");
123/// }
124///
125/// for assertion in catalog::assertions() {
126/// println!(
127/// "{:?} {:?} at {}:{}",
128/// assertion.kind, assertion.message,
129/// assertion.location.file, assertion.location.begin_line
130/// );
131/// }
132/// ```
133pub fn assertions() -> impl Iterator<Item = Assertion> {
134 #[cfg(feature = "full")]
135 {
136 assert::ANTITHESIS_CATALOG.iter().map(assertion_of)
137 }
138 #[cfg(not(feature = "full"))]
139 {
140 std::iter::empty()
141 }
142}
143
144/// Writes the assertions of [`assertions()`] to `out` as JSON lines.
145///
146/// Each line is the catalog-registration event that
147/// [`antithesis_init()`](crate::antithesis_init) emits for that assertion,
148/// produced by the same serialization path and in the same order. Init
149/// additionally writes the `antithesis_sdk` version header first and the
150/// guidance registrations of the numeric and boolean macros last; neither
151/// describes an assertion, so they are left out here.
152///
153/// Nothing is sent to the Antithesis environment or to the local output file,
154/// and init need not have run. Without the `full` feature nothing is written.
155///
156/// For a checked-in golden file, sort the lines first: their order is the
157/// linker's and can change between builds. Two further caveats: `file` is
158/// whatever `file!()` produced at compile time, which depends on how the
159/// build was invoked (for example `--remap-path-prefix`), and `function`
160/// comes from `std::any::type_name`, whose exact form is not guaranteed
161/// stable across compiler versions.
162pub fn write<W: io::Write>(out: W) -> io::Result<()> {
163 #[cfg(feature = "full")]
164 {
165 let mut out = out;
166 for event in assertion_events() {
167 serde_json::to_writer(&mut out, &event)?;
168 out.write_all(b"\n")?;
169 }
170 Ok(())
171 }
172 #[cfg(not(feature = "full"))]
173 {
174 let _ = out;
175 Ok(())
176 }
177}
178
179/// Everything `antithesis_init()` registers, in order: the assertion events
180/// [`write()`] emits, then the guidance the numeric and boolean macros also
181/// declare. Init and `write()` share the assertion sequence, so they cannot
182/// disagree about it.
183#[cfg(feature = "full")]
184pub(crate) fn registrations() -> impl Iterator<Item = Value> {
185 let guidance = assert::ANTITHESIS_GUIDANCE_CATALOG.iter().map(guidance::catalog_event);
186 assertion_events().chain(guidance)
187}
188
189#[cfg(feature = "full")]
190fn assertion_events() -> impl Iterator<Item = Value> {
191 assert::ANTITHESIS_CATALOG.iter().map(assert::catalog_event)
192}
193
194// The explicit deref is needed on older compilers, which otherwise infer
195// `T = str` from the field type and reject the call.
196#[cfg(feature = "full")]
197#[allow(clippy::explicit_auto_deref)]
198fn assertion_of(info: &assert::AssertionCatalogInfo) -> Assertion {
199 Assertion {
200 id: info.id,
201 message: info.message,
202 kind: kind_of(info.assert_type, info.must_hit),
203 location: SourceLocation {
204 file: info.file,
205 begin_line: info.begin_line,
206 begin_column: info.begin_column,
207 class: info.class,
208 function: *Lazy::force(info.function),
209 },
210 }
211}
212
213#[cfg(feature = "full")]
214fn kind_of(assert_type: assert::AssertType, must_hit: bool) -> AssertionKind {
215 match (assert_type, must_hit) {
216 (assert::AssertType::Always, true) => AssertionKind::Always,
217 (assert::AssertType::Always, false) => AssertionKind::AlwaysOrUnreachable,
218 (assert::AssertType::Sometimes, _) => AssertionKind::Sometimes,
219 (assert::AssertType::Reachability, true) => AssertionKind::Reachable,
220 (assert::AssertType::Reachability, false) => AssertionKind::Unreachable,
221 }
222}