blob: 0acc4f9457c27f4e76010ba9c11571c54bb79580 [file] [log] [blame]
Li-Yu Yu5e1e5d82024-09-06 11:48:25 +00001//! # 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#![cfg_attr(nightly_docs, deny(broken_intra_doc_links))]
74#![cfg_attr(nightly_docs, feature(doc_cfg))]
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 hit_count,
227 hits
228 ),
229 None => assert!(hit_count > 0, "mark was not hit"),
230 }
231 }
232 }
233}
234
235#[doc(hidden)]
236#[cfg(not(feature = "enable"))]
237pub mod __rt {
238 #[inline(always)]
239 pub fn hit(_: &'static str) {}
240
241 #[non_exhaustive]
242 pub struct Guard;
243
244 impl Guard {
245 pub fn new(_: &'static str, _: Option<usize>) -> Guard {
246 Guard
247 }
248 }
249}