use std::cell::Cell;
thread_local! {
static EXPECTED: Cell<usize> = const { Cell::new(0) };
}
#[derive(Debug)]
pub struct ExpectedPanic(());
impl ExpectedPanic {
#[allow(clippy::new_without_default)] pub fn new() -> Self {
adjust(1);
Self(())
}
}
impl Drop for ExpectedPanic {
fn drop(&mut self) {
adjust(-1);
}
}
fn adjust(delta: isize) {
let _ = EXPECTED.try_with(|c| {
let next = if delta >= 0 {
c.get().saturating_add(delta as usize)
} else {
c.get().saturating_sub(delta.unsigned_abs())
};
c.set(next);
});
}
pub fn panic_is_expected() -> bool {
EXPECTED.try_with(|c| c.get() > 0).unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_guard_marks_and_unmarks_the_thread() {
assert!(!panic_is_expected());
{
let _g = ExpectedPanic::new();
assert!(panic_is_expected());
}
assert!(!panic_is_expected());
}
#[test]
fn guards_nest() {
let outer = ExpectedPanic::new();
{
let _inner = ExpectedPanic::new();
assert!(panic_is_expected());
}
assert!(panic_is_expected(), "the outer guard is still live");
drop(outer);
assert!(!panic_is_expected());
}
#[test]
fn the_guard_survives_the_unwind_it_exists_for() {
let caught = {
let _g = ExpectedPanic::new();
std::panic::catch_unwind(|| {
assert!(panic_is_expected(), "marked while inside");
panic!("expected");
})
};
assert!(caught.is_err());
assert!(!panic_is_expected(), "unmarked once the guard is dropped");
}
#[test]
fn a_hook_shaped_like_main_s_does_not_restore_for_a_guarded_panic() {
thread_local! {
static RESTORED: Cell<usize> = const { Cell::new(0) };
}
fn restored() -> usize {
RESTORED.with(Cell::get)
}
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {
if panic_is_expected() {
return;
}
let _ = RESTORED.try_with(|c| c.set(c.get() + 1));
}));
let guarded = {
let _g = ExpectedPanic::new();
std::panic::catch_unwind(|| panic!("caught"))
};
assert!(guarded.is_err());
assert_eq!(
restored(),
0,
"a caught panic must not tear the terminal down"
);
let bare = std::panic::catch_unwind(|| panic!("uncaught by intent"));
assert!(bare.is_err());
assert_eq!(
restored(),
1,
"an unexpected panic must still restore the terminal"
);
std::panic::set_hook(previous);
}
#[test]
fn the_flag_does_not_leak_to_another_thread() {
let _g = ExpectedPanic::new();
assert!(panic_is_expected());
let seen = std::thread::spawn(panic_is_expected).join().unwrap();
assert!(!seen, "another thread must not inherit the guard");
}
}