use alloc::boxed::Box;
use alloc::string::{String, ToString};
use core::panic::AssertUnwindSafe;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Safety<T> {
Safe { cases: u64 },
Panicked {
input: T,
message: String,
checked: u64,
},
Refuted { input: T, checked: u64 },
}
impl<T> Safety<T> {
pub fn is_safe(&self) -> bool {
matches!(self, Safety::Safe { .. })
}
pub fn is_vacuous(&self) -> bool {
matches!(self, Safety::Safe { cases: 0 })
}
pub fn is_safe_nonvacuous(&self) -> bool {
matches!(self, Safety::Safe { cases } if *cases > 0)
}
pub fn panicked(&self) -> bool {
matches!(self, Safety::Panicked { .. })
}
pub fn failing_input(&self) -> Option<&T> {
match self {
Safety::Panicked { input, .. } | Safety::Refuted { input, .. } => Some(input),
Safety::Safe { .. } => None,
}
}
pub fn message(&self) -> Option<&str> {
match self {
Safety::Panicked { message, .. } => Some(message),
_ => None,
}
}
pub fn cases(&self) -> u64 {
match self {
Safety::Safe { cases } => *cases,
Safety::Panicked { checked, .. } | Safety::Refuted { checked, .. } => *checked,
}
}
}
fn payload_message(payload: &Box<dyn core::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&'static str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"panic with a non-string payload".to_string()
}
}
type PanicHook = Box<dyn Fn(&std::panic::PanicHookInfo<'_>) + Sync + Send + 'static>;
struct QuietPanics {
previous: Option<PanicHook>,
}
impl QuietPanics {
fn install() -> Self {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
QuietPanics {
previous: Some(previous),
}
}
}
impl Drop for QuietPanics {
fn drop(&mut self) {
if std::thread::panicking() {
return;
}
if let Some(prev) = self.previous.take() {
std::panic::set_hook(prev);
}
}
}
pub fn verify_no_panic<I, T, F, R>(inputs: I, f: F) -> Safety<T>
where
I: IntoIterator<Item = T>,
F: Fn(&T) -> R,
{
let _quiet = QuietPanics::install();
let mut n = 0u64;
for x in inputs {
match std::panic::catch_unwind(AssertUnwindSafe(|| f(&x))) {
Ok(_) => n += 1,
Err(payload) => {
return Safety::Panicked {
input: x,
message: payload_message(&payload),
checked: n,
}
}
}
}
Safety::Safe { cases: n }
}
pub fn verify_no_panic_in<F, R>(lo: u64, hi: u64, f: F) -> Safety<u64>
where
F: Fn(&u64) -> R,
{
verify_no_panic(lo..=hi, f)
}
pub fn verify_no_panic_u8<F, R>(f: F) -> Safety<u8>
where
F: Fn(&u8) -> R,
{
verify_no_panic(0u8..=255, f)
}
pub fn for_all_safe<I, T, F>(inputs: I, pred: F) -> Safety<T>
where
I: IntoIterator<Item = T>,
F: Fn(&T) -> bool,
{
let _quiet = QuietPanics::install();
let mut n = 0u64;
for x in inputs {
match std::panic::catch_unwind(AssertUnwindSafe(|| pred(&x))) {
Ok(true) => n += 1,
Ok(false) => {
return Safety::Refuted {
input: x,
checked: n,
}
}
Err(payload) => {
return Safety::Panicked {
input: x,
message: payload_message(&payload),
checked: n,
}
}
}
}
Safety::Safe { cases: n }
}
pub fn for_all_safe_in<F>(lo: u64, hi: u64, pred: F) -> Safety<u64>
where
F: Fn(&u64) -> bool,
{
for_all_safe(lo..=hi, |x: &u64| pred(x))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_clean_function_is_proven_safe_over_its_domain() {
let s = verify_no_panic_u8(|&x| x as u16 + 1);
assert!(s.is_safe_nonvacuous());
assert_eq!(s.cases(), 256, "every input executed");
assert!(s.failing_input().is_none());
}
#[test]
fn index_out_of_bounds_is_found_without_a_predicate() {
let table = [10u32, 20, 30];
let s = verify_no_panic(0usize..=5, |&i| table[i]);
assert!(s.panicked());
assert_eq!(s.failing_input(), Some(&3), "the first out-of-range index");
assert_eq!(s.cases(), 3, "indices 0, 1, 2 passed first");
assert!(s.message().unwrap().contains("index out of bounds"));
}
#[test]
fn division_by_zero_is_found() {
let s = verify_no_panic(0u32..=10, |&x| 100u32 / x);
assert!(s.panicked());
assert_eq!(s.failing_input(), Some(&0));
}
#[test]
fn unwrap_on_none_is_found() {
let s = verify_no_panic(0u32..=10, |&x| if x < 7 { Some(x) } else { None }.unwrap());
assert!(s.panicked());
assert_eq!(s.failing_input(), Some(&7));
}
#[test]
fn arithmetic_overflow_is_found_under_debug_assertions() {
let s = verify_no_panic(250u8..=255, |&x| x + 10);
assert!(
s.panicked(),
"overflow must be reported under debug-assertions"
);
assert!(s.message().unwrap().contains("overflow"));
}
#[test]
fn a_panic_is_distinguished_from_a_refutation() {
let refuted = for_all_safe(0u32..=10, |&x| x < 5);
assert!(!refuted.is_safe());
assert!(!refuted.panicked(), "a false predicate is not a crash");
assert_eq!(refuted.failing_input(), Some(&5));
let panicked = for_all_safe(0u32..=10, |&x| {
assert!(x < 5, "boom at {x}");
true
});
assert!(
panicked.panicked(),
"a crash is not merely a false predicate"
);
assert_eq!(panicked.failing_input(), Some(&5));
assert!(panicked.message().unwrap().contains("boom at 5"));
}
#[test]
fn unwinding_still_works_after_a_verification_run() {
let s = verify_no_panic(0u32..=3, |&x| {
if x == 2 {
panic!("x")
}
});
assert!(s.panicked());
assert_eq!(s.failing_input(), Some(&2));
let caught = std::panic::catch_unwind(|| panic!("still unwinds"));
assert!(
caught.is_err(),
"panics still unwind after a verification run"
);
let again = verify_no_panic(0u32..=3, |&x| {
if x == 1 {
panic!("y")
}
});
assert_eq!(again.failing_input(), Some(&1));
}
#[test]
fn a_panicking_iterator_does_not_abort_the_process() {
let caught = std::panic::catch_unwind(|| {
let hostile = (0u32..10).inspect(|&x| {
assert!(x < 4, "iterator blew up at {x}");
});
verify_no_panic(hostile, |_| ())
});
assert!(
caught.is_err(),
"the iterator's panic unwinds as a normal panic, not an abort"
);
let s = verify_no_panic(0u32..=3, |_| ());
assert!(s.is_safe_nonvacuous());
}
#[test]
fn an_empty_domain_is_vacuous() {
let s: Safety<u64> = verify_no_panic_in(10, 0, |_| ());
assert!(s.is_vacuous());
assert!(!s.is_safe_nonvacuous());
}
}