blob: 0acc4f9457c27f4e76010ba9c11571c54bb79580 [file] [log] [blame] [edit]
//! # cov-mark
//!
//! This library at its core provides two macros, [`hit!`] and [`check!`],
//! which can be used to verify that a certain test exercises a certain code
//! path.
//!
//! Here's a short example:
//!
//! ```
//! fn parse_date(s: &str) -> Option<(u32, u32, u32)> {
//! if 10 != s.len() {
//! // By using `cov_mark::hit!`
//! // we signal which test exercises this code.
//! cov_mark::hit!(short_date);
//! return None;
//! }
//!
//! if "-" != &s[4..5] || "-" != &s[7..8] {
//! cov_mark::hit!(bad_dashes);
//! return None;
//! }
//! // ...
//! # unimplemented!()
//! }
//!
//! #[test]
//! fn test_parse_date() {
//! {
//! // `cov_mark::check!` creates a guard object
//! // that verifies that by the end of the scope we've
//! // executed the corresponding `cov_mark::hit`.
//! cov_mark::check!(short_date);
//! assert!(parse_date("92").is_none());
//! }
//!
//! // This will fail. Although the test looks like
//! // it exercises the second condition, it does not.
//! // The call to `check!` call catches this bug in the test.
//! // {
//! // cov_mark::check!(bad_dashes);
//! // assert!(parse_date("27.2.2013").is_none());
//! // }
//!
//! {
//! cov_mark::check!(bad_dashes);
//! assert!(parse_date("27.02.2013").is_none());
//! }
//! }
//!
//! # fn main() {}
//! ```
//!
//! Here's why coverage marks are useful:
//!
//! * Verifying that something doesn't happen for the *right* reason.
//! * Finding the test that exercises the code (grep for `check!(mark_name)`).
//! * Finding the code that the test is supposed to check (grep for `hit!(mark_name)`).
//! * Making sure that code and tests don't diverge during refactorings.
//! * (If used pervasively) Verifying that each branch has a corresponding test.
//!
//! # Limitations
//!
//! * Names of marks must be globally unique.
//!
//! # Implementation Details
//!
//! Each coverage mark is an `AtomicUsize` counter. [`hit!`] increments
//! this counter, [`check!`] returns a guard object which checks that
//! the mark was incremented.
//! Each counter is stored as a thread-local, allowing for accurate per-thread
//! counting.
#![cfg_attr(nightly_docs, deny(broken_intra_doc_links))]
#![cfg_attr(nightly_docs, feature(doc_cfg))]
/// Hit a mark with a specified name.
///
/// # Example
///
/// ```
/// fn safe_divide(dividend: u32, divisor: u32) -> u32 {
/// if divisor == 0 {
/// cov_mark::hit!(save_divide_zero);
/// return 0;
/// }
/// dividend / divisor
/// }
/// ```
#[macro_export]
macro_rules! hit {
($ident:ident) => {
$crate::__rt::hit(stringify!($ident))
};
}
/// Checks that a specified mark was hit.
///
/// # Example
///
/// ```
/// #[test]
/// fn test_safe_divide_by_zero() {
/// cov_mark::check!(save_divide_zero);
/// assert_eq!(safe_divide(92, 0), 0);
/// }
/// # fn safe_divide(dividend: u32, divisor: u32) -> u32 {
/// # if divisor == 0 {
/// # cov_mark::hit!(save_divide_zero);
/// # return 0;
/// # }
/// # dividend / divisor
/// # }
/// ```
#[macro_export]
macro_rules! check {
($ident:ident) => {
let _guard = $crate::__rt::Guard::new(stringify!($ident), None);
};
}
/// Checks that a specified mark was hit exactly the specified number of times.
///
/// # Example
///
/// ```
/// struct CoveredDropper;
/// impl Drop for CoveredDropper {
/// fn drop(&mut self) {
/// cov_mark::hit!(covered_dropper_drops);
/// }
/// }
///
/// #[test]
/// fn drop_count_test() {
/// cov_mark::check_count!(covered_dropper_drops, 2);
/// let _covered_dropper1 = CoveredDropper;
/// let _covered_dropper2 = CoveredDropper;
/// }
/// ```
#[macro_export]
macro_rules! check_count {
($ident:ident, $count: literal) => {
let _guard = $crate::__rt::Guard::new(stringify!($ident), Some($count));
};
}
#[doc(hidden)]
#[cfg(feature = "enable")]
pub mod __rt {
use std::{
cell::{Cell, RefCell},
rc::Rc,
sync::atomic::{AtomicUsize, Ordering::Relaxed},
};
/// Even with
/// https://github.com/rust-lang/rust/commit/641d3b09f41b441f2c2618de32983ad3d13ea3f8,
/// a `thread_local` generates significantly more verbose assembly on x86
/// than atomic, so we'll use atomic for the fast path
static LEVEL: AtomicUsize = AtomicUsize::new(0);
thread_local! {
static ACTIVE: RefCell<Vec<Rc<GuardInner>>> = Default::default();
}
#[inline(always)]
pub fn hit(key: &'static str) {
if LEVEL.load(Relaxed) > 0 {
hit_cold(key);
}
#[cold]
fn hit_cold(key: &'static str) {
ACTIVE.with(|it| it.borrow().iter().for_each(|g| g.hit(key)))
}
}
struct GuardInner {
mark: &'static str,
hits: Cell<usize>,
expected_hits: Option<usize>,
}
pub struct Guard {
inner: Rc<GuardInner>,
}
impl GuardInner {
fn hit(&self, key: &'static str) {
if key == self.mark {
self.hits.set(self.hits.get().saturating_add(1))
}
}
}
impl Guard {
pub fn new(mark: &'static str, expected_hits: Option<usize>) -> Guard {
let inner = GuardInner {
mark,
hits: Cell::new(0),
expected_hits,
};
let inner = Rc::new(inner);
LEVEL.fetch_add(1, Relaxed);
ACTIVE.with(|it| it.borrow_mut().push(Rc::clone(&inner)));
Guard { inner }
}
}
impl Drop for Guard {
fn drop(&mut self) {
LEVEL.fetch_sub(1, Relaxed);
let last = ACTIVE.with(|it| it.borrow_mut().pop());
if std::thread::panicking() {
return;
}
let last = last.unwrap();
assert!(Rc::ptr_eq(&last, &self.inner));
let hit_count = last.hits.get();
match last.expected_hits {
Some(hits) => assert!(
hit_count == hits,
"mark was hit {} times, expected {}",
hit_count,
hits
),
None => assert!(hit_count > 0, "mark was not hit"),
}
}
}
}
#[doc(hidden)]
#[cfg(not(feature = "enable"))]
pub mod __rt {
#[inline(always)]
pub fn hit(_: &'static str) {}
#[non_exhaustive]
pub struct Guard;
impl Guard {
pub fn new(_: &'static str, _: Option<usize>) -> Guard {
Guard
}
}
}