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#![deny(rustdoc::broken_intra_doc_links)]
74#![allow(clippy::test_attr_in_doctest)]
75
76/// Hit a mark with a specified name.
77///
78/// # Example
79///
80/// ```
81/// fn safe_divide(dividend: u32, divisor: u32) -> u32 {
82///     if divisor == 0 {
83///         cov_mark::hit!(save_divide_zero);
84///         return 0;
85///     }
86///     dividend / divisor
87/// }
88/// ```
89#[macro_export]
90macro_rules! hit {
91    ($ident:ident) => {
92        $crate::__rt::hit(stringify!($ident))
93    };
94}
95
96/// Checks that a specified mark was hit.
97///
98/// # Example
99///
100/// ```
101/// #[test]
102/// fn test_safe_divide_by_zero() {
103///     cov_mark::check!(save_divide_zero);
104///     assert_eq!(safe_divide(92, 0), 0);
105/// }
106/// # fn safe_divide(dividend: u32, divisor: u32) -> u32 {
107/// #     if divisor == 0 {
108/// #         cov_mark::hit!(save_divide_zero);
109/// #         return 0;
110/// #     }
111/// #     dividend / divisor
112/// # }
113/// ```
114#[macro_export]
115macro_rules! check {
116    ($ident:ident) => {
117        let _guard = $crate::__rt::Guard::new(stringify!($ident), None);
118    };
119}
120
121/// Checks that a specified mark was hit exactly the specified number of times.
122///
123/// # Example
124///
125/// ```
126/// struct CoveredDropper;
127/// impl Drop for CoveredDropper {
128///     fn drop(&mut self) {
129///         cov_mark::hit!(covered_dropper_drops);
130///     }
131/// }
132///
133/// #[test]
134/// fn drop_count_test() {
135///     cov_mark::check_count!(covered_dropper_drops, 2);
136///     let _covered_dropper1 = CoveredDropper;
137///     let _covered_dropper2 = CoveredDropper;
138/// }
139/// ```
140#[macro_export]
141macro_rules! check_count {
142    ($ident:ident, $count: literal) => {
143        let _guard = $crate::__rt::Guard::new(stringify!($ident), Some($count));
144    };
145}
146
147#[doc(hidden)]
148#[cfg(feature = "enable")]
149pub mod __rt {
150    use std::{
151        cell::{Cell, RefCell},
152        rc::Rc,
153        sync::atomic::{AtomicUsize, Ordering::Relaxed},
154    };
155
156    /// Even with
157    /// https://github.com/rust-lang/rust/commit/641d3b09f41b441f2c2618de32983ad3d13ea3f8,
158    /// a `thread_local` generates significantly more verbose assembly on x86
159    /// than atomic, so we'll use atomic for the fast path
160    static LEVEL: AtomicUsize = AtomicUsize::new(0);
161
162    thread_local! {
163        static ACTIVE: RefCell<Vec<Rc<GuardInner>>> = Default::default();
164    }
165
166    #[inline(always)]
167    pub fn hit(key: &'static str) {
168        if LEVEL.load(Relaxed) > 0 {
169            hit_cold(key);
170        }
171
172        #[cold]
173        fn hit_cold(key: &'static str) {
174            ACTIVE.with(|it| it.borrow().iter().for_each(|g| g.hit(key)))
175        }
176    }
177
178    struct GuardInner {
179        mark: &'static str,
180        hits: Cell<usize>,
181        expected_hits: Option<usize>,
182    }
183
184    pub struct Guard {
185        inner: Rc<GuardInner>,
186    }
187
188    impl GuardInner {
189        fn hit(&self, key: &'static str) {
190            if key == self.mark {
191                self.hits.set(self.hits.get().saturating_add(1))
192            }
193        }
194    }
195
196    impl Guard {
197        pub fn new(mark: &'static str, expected_hits: Option<usize>) -> Guard {
198            let inner = GuardInner {
199                mark,
200                hits: Cell::new(0),
201                expected_hits,
202            };
203            let inner = Rc::new(inner);
204            LEVEL.fetch_add(1, Relaxed);
205            ACTIVE.with(|it| it.borrow_mut().push(Rc::clone(&inner)));
206            Guard { inner }
207        }
208    }
209
210    impl Drop for Guard {
211        fn drop(&mut self) {
212            LEVEL.fetch_sub(1, Relaxed);
213            let last = ACTIVE.with(|it| it.borrow_mut().pop());
214
215            if std::thread::panicking() {
216                return;
217            }
218
219            let last = last.unwrap();
220            assert!(Rc::ptr_eq(&last, &self.inner));
221            let hit_count = last.hits.get();
222            match last.expected_hits {
223                Some(hits) => assert!(
224                    hit_count == hits,
225                    "{} mark was hit {} times, expected {}",
226                    self.inner.mark,
227                    hit_count,
228                    hits
229                ),
230                None => assert!(hit_count > 0, "{} mark was not hit", self.inner.mark),
231            }
232        }
233    }
234}
235
236#[doc(hidden)]
237#[cfg(not(feature = "enable"))]
238pub mod __rt {
239    #[inline(always)]
240    pub fn hit(_: &'static str) {}
241
242    #[non_exhaustive]
243    pub struct Guard;
244
245    impl Guard {
246        pub fn new(_: &'static str, _: Option<usize>) -> Guard {
247            Guard
248        }
249    }
250}