cov_mark/
lib.rs

1//! # cov-mark
2//!
3//! This library at its core provides two macros, [`hit!`] and [`check!`],
4//! which can be used to verify that a certain test exercises a certain code
5//! path.
6//!
7//! Here's a short example:
8//!
9//! ```
10//! fn parse_date(s: &str) -> Option<(u32, u32, u32)> {
11//!     if 10 != s.len() {
12//!         // By using `cov_mark::hit!`
13//!         // we signal which test exercises this code.
14//!         cov_mark::hit!(short_date);
15//!         return None;
16//!     }
17//!
18//!     if "-" != &s[4..5] || "-" != &s[7..8] {
19//!         cov_mark::hit!(bad_dashes);
20//!         return None;
21//!     }
22//!     // ...
23//! #    unimplemented!()
24//! }
25//!
26//! #[test]
27//! fn test_parse_date() {
28//!     {
29//!         // `cov_mark::check!` creates a guard object
30//!         // that verifies that by the end of the scope we've
31//!         // executed the corresponding `cov_mark::hit`.
32//!         cov_mark::check!(short_date);
33//!         assert!(parse_date("92").is_none());
34//!     }
35//!
36//! //  This will fail. Although the test looks like
37//! //  it exercises the second condition, it does not.
38//! //  The call to `check!` call catches this bug in the test.
39//! //  {
40//! //      cov_mark::check!(bad_dashes);
41//! //      assert!(parse_date("27.2.2013").is_none());
42//! //  }
43//!
44//!     {
45//!         cov_mark::check!(bad_dashes);
46//!         assert!(parse_date("27.02.2013").is_none());
47//!     }
48//! }
49//!
50//! # fn main() {}
51//! ```
52//!
53//! Here's why coverage marks are useful:
54//!
55//! * Verifying that something doesn't happen for the *right* reason.
56//! * Finding the test that exercises the code (grep for `check!(mark_name)`).
57//! * Finding the code that the test is supposed to check (grep for `hit!(mark_name)`).
58//! * Making sure that code and tests don't diverge during refactorings.
59//! * (If used pervasively) Verifying that each branch has a corresponding test.
60//!
61//! # Limitations
62//!
63//! * Names of marks must be globally unique.
64//!
65//! # Implementation Details
66//!
67//! Each coverage mark is an `AtomicUsize` counter. [`hit!`] increments
68//! this counter, [`check!`] returns a guard object which checks that
69//! the mark was incremented.
70//! Each counter is stored as a thread-local, allowing for accurate per-thread
71//! counting.
72//!
73//! # Porting existing tests to cov-mark
74//!
75//! When incrementally outfitting a set of tests with markers, [`survey`] may be useful.
76
77#![deny(rustdoc::broken_intra_doc_links)]
78#![allow(clippy::test_attr_in_doctest)]
79
80/// Hit a mark with a specified name.
81///
82/// # Example
83///
84/// ```
85/// fn safe_divide(dividend: u32, divisor: u32) -> u32 {
86///     if divisor == 0 {
87///         cov_mark::hit!(safe_divide_zero);
88///         return 0;
89///     }
90///     dividend / divisor
91/// }
92/// ```
93#[macro_export]
94macro_rules! hit {
95    ($ident:ident) => {
96        $crate::__rt::hit(stringify!($ident))
97    };
98}
99
100/// Checks that a specified mark was hit.
101///
102/// # Example
103///
104/// ```
105/// #[test]
106/// fn test_safe_divide_by_zero() {
107///     cov_mark::check!(safe_divide_zero);
108///     assert_eq!(safe_divide(92, 0), 0);
109/// }
110/// # fn safe_divide(dividend: u32, divisor: u32) -> u32 {
111/// #     if divisor == 0 {
112/// #         cov_mark::hit!(safe_divide_zero);
113/// #         return 0;
114/// #     }
115/// #     dividend / divisor
116/// # }
117/// ```
118#[macro_export]
119macro_rules! check {
120    ($ident:ident) => {
121        let _guard = $crate::__rt::Guard::new(stringify!($ident), None, $crate::assert!());
122    };
123}
124
125/// Checks that a specified mark was hit exactly the specified number of times.
126///
127/// # Example
128///
129/// ```
130/// struct CoveredDropper;
131/// impl Drop for CoveredDropper {
132///     fn drop(&mut self) {
133///         cov_mark::hit!(covered_dropper_drops);
134///     }
135/// }
136///
137/// #[test]
138/// fn drop_count_test() {
139///     cov_mark::check_count!(covered_dropper_drops, 2);
140///     let _covered_dropper1 = CoveredDropper;
141///     let _covered_dropper2 = CoveredDropper;
142/// }
143/// ```
144#[macro_export]
145macro_rules! check_count {
146    ($ident:ident, $count: literal) => {
147        let _guard = $crate::__rt::Guard::new(stringify!($ident), Some($count), $crate::assert!());
148    };
149}
150
151/// Survey which marks are hit.
152///
153/// # Example
154///
155/// ```
156/// struct CoveredDropper;
157/// impl Drop for CoveredDropper {
158///     fn drop(&mut self) {
159///         cov_mark::hit!(covered_dropper_drops);
160///     }
161/// }
162///
163/// # fn safe_divide(dividend: u32, divisor: u32) -> u32 {
164/// #     if divisor == 0 {
165/// #         cov_mark::hit!(safe_divide_zero);
166/// #         return 0;
167/// #     }
168/// #     dividend / divisor
169/// # }
170///
171/// #[test]
172/// fn drop_count_test() {
173///     let _survey = cov_mark::survey(); // sets a drop guard that tracks hits
174///     let _covered_dropper1 = CoveredDropper;
175///     let _covered_dropper2 = CoveredDropper;
176///     safe_divide(92, 0);
177///     // prints
178///     // "mark safe_divide_zero ... hit once"
179///     // "mark covered_dropper_drops ... hit 2 times"
180/// }
181/// ```
182pub fn survey() -> __rt::SurveyGuard {
183    __rt::SurveyGuard::new()
184}
185
186#[doc(hidden)]
187#[macro_export]
188macro_rules! assert {
189    () => {
190        |expected_hits, hit_count, mark| match expected_hits {
191            Some(hits) => assert!(
192                hit_count == hits,
193                "mark {mark} was hit {hit_count} times, expected {hits}"
194            ),
195            None => assert!(hit_count > 0, "mark {mark} was not hit"),
196        }
197    };
198}
199
200#[doc(hidden)]
201pub type AssertCallback = fn(Option<usize>, usize, &'static str);
202
203#[doc(hidden)]
204#[cfg(feature = "enable")]
205pub mod __rt {
206    use std::{
207        cell::RefCell,
208        sync::atomic::{AtomicUsize, Ordering::Relaxed},
209    };
210
211    use super::AssertCallback;
212
213    /// Even with
214    /// <https://github.com/rust-lang/rust/commit/641d3b09f41b441f2c2618de32983ad3d13ea3f8>,
215    /// a `thread_local` generates significantly more verbose assembly on x86
216    /// than atomic, so we'll use atomic for the fast path
217    static LEVEL: AtomicUsize = AtomicUsize::new(0);
218    const SURVEY_LEVEL: usize = !(usize::MAX >> 1);
219
220    thread_local! {
221        static ACTIVE: RefCell<Vec<GuardInner>> = const { RefCell::new(Vec::new()) };
222        static SURVEY_RESPONSE: RefCell<Vec<GuardInner>> = const { RefCell::new(Vec::new()) };
223    }
224
225    #[inline(always)]
226    pub fn hit(key: &'static str) {
227        let level = LEVEL.load(Relaxed);
228        if level > 0 {
229            if level >= SURVEY_LEVEL {
230                add_to_survey(key);
231            }
232            hit_cold(key);
233        }
234
235        #[cold]
236        fn hit_cold(key: &'static str) {
237            ACTIVE.with(|it| it.borrow_mut().iter_mut().for_each(|g| g.hit(key)))
238        }
239
240        #[cold]
241        fn add_to_survey(mark: &'static str) {
242            SURVEY_RESPONSE.with(|it| {
243                let mut it = it.borrow_mut();
244                for survey in it.iter_mut() {
245                    if survey.mark == mark {
246                        survey.hits = survey.hits.saturating_add(1);
247                        return;
248                    }
249                }
250                it.push(GuardInner {
251                    mark,
252                    hits: 1,
253                    expected_hits: None,
254                });
255            });
256        }
257    }
258
259    #[derive(PartialEq, Eq, PartialOrd, Ord)]
260    struct GuardInner {
261        mark: &'static str,
262        hits: usize,
263        expected_hits: Option<usize>,
264    }
265
266    pub struct Guard {
267        mark: &'static str,
268        f: AssertCallback,
269    }
270
271    impl GuardInner {
272        fn hit(&mut self, key: &'static str) {
273            if key == self.mark {
274                self.hits = self.hits.saturating_add(1);
275            }
276        }
277    }
278
279    impl Guard {
280        // macro expansion inserts a [`AssertCallback`] defined in user code, so the panic location
281        // points to user code instead of cov-mark internals
282        pub fn new(mark: &'static str, expected_hits: Option<usize>, f: AssertCallback) -> Guard {
283            LEVEL.fetch_add(1, Relaxed);
284            ACTIVE.with(|it| {
285                it.borrow_mut().push(GuardInner {
286                    mark,
287                    hits: 0,
288                    expected_hits,
289                })
290            });
291            Guard { mark, f }
292        }
293    }
294
295    impl Drop for Guard {
296        fn drop(&mut self) {
297            LEVEL.fetch_sub(1, Relaxed);
298            let last = ACTIVE.with(|it| it.borrow_mut().pop());
299
300            if std::thread::panicking() {
301                return;
302            }
303
304            let last = last.unwrap();
305            assert_eq!(last.mark, self.mark);
306            let hit_count = last.hits;
307            (self.f)(last.expected_hits, hit_count, self.mark)
308        }
309    }
310
311    pub struct SurveyGuard;
312
313    impl SurveyGuard {
314        #[allow(clippy::new_without_default)]
315        pub fn new() -> SurveyGuard {
316            LEVEL.fetch_or(SURVEY_LEVEL, Relaxed);
317            SurveyGuard
318        }
319    }
320
321    impl Drop for SurveyGuard {
322        fn drop(&mut self) {
323            LEVEL.fetch_and(!SURVEY_LEVEL, Relaxed);
324
325            if std::thread::panicking() {
326                return;
327            }
328
329            SURVEY_RESPONSE.with(|it| {
330                let mut it = it.borrow_mut();
331                it.sort();
332                for g in it.iter() {
333                    let hit_count = g.hits;
334                    if hit_count == 1 {
335                        eprintln!("mark {} ... hit once", g.mark);
336                    } else if 1 < hit_count {
337                        eprintln!("mark {} ... hit {} times", g.mark, hit_count);
338                    }
339                }
340                it.clear();
341            });
342        }
343    }
344}
345
346#[doc(hidden)]
347#[cfg(not(feature = "enable"))]
348pub mod __rt {
349    #[inline(always)]
350    pub fn hit(_: &'static str) {}
351
352    #[non_exhaustive]
353    pub struct Guard;
354
355    impl Guard {
356        pub fn new(_: &'static str, _: Option<usize>, _: super::AssertCallback) -> Guard {
357            Guard
358        }
359    }
360
361    pub struct SurveyGuard;
362
363    impl SurveyGuard {
364        #[allow(clippy::new_without_default)]
365        pub fn new() -> SurveyGuard {
366            SurveyGuard
367        }
368    }
369}