use std::backtrace::{Backtrace, BacktraceStatus};
use std::cell::RefCell;
use std::panic::{self, AssertUnwindSafe, catch_unwind};
use std::sync::Once;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::antithesis::TestLocation;
use crate::backend::{DataSource, Failure, TestCaseResult, TestRunner};
use crate::control::{currently_in_test_context, with_test_context};
use crate::runner::{Mode, Phase, Settings};
use crate::test_case::{ASSUME_FAIL_STRING, LOOP_DONE_STRING, STOP_TEST_STRING, TestCase};
static PANIC_HOOK_INIT: Once = Once::new();
thread_local! {
static LAST_PANIC_INFO: RefCell<Option<(String, String, String, Backtrace)>> =
const { RefCell::new(None) };
}
fn take_panic_info() -> Option<(String, String, String, Backtrace)> {
LAST_PANIC_INFO.with(|info| info.borrow_mut().take())
}
pub(crate) fn init_panic_hook() {
PANIC_HOOK_INIT.call_once(|| {
let prev_hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
if !currently_in_test_context() {
prev_hook(info);
return;
}
let thread = std::thread::current();
let thread_name = thread.name().unwrap_or("<unnamed>").to_string();
let thread_id = format!("{:?}", thread.id())
.trim_start_matches("ThreadId(")
.trim_end_matches(')')
.to_string();
let location = info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
.unwrap_or_else(|| "<unknown>".to_string());
let backtrace = Backtrace::capture();
LAST_PANIC_INFO
.with(|l| *l.borrow_mut() = Some((thread_name, thread_id, location, backtrace)));
}));
});
}
fn format_backtrace(bt: &Backtrace, full: bool) -> String {
let backtrace_str = format!("{}", bt);
if full {
return backtrace_str;
}
filter_short_backtrace(&backtrace_str)
}
fn filter_short_backtrace(backtrace_str: &str) -> String {
let lines: Vec<&str> = backtrace_str.lines().collect();
let mut start_idx = 0;
let mut end_idx = lines.len();
for (i, line) in lines.iter().enumerate() {
if line.contains("__rust_end_short_backtrace") {
for (j, next_line) in lines.iter().enumerate().skip(i + 1) {
if next_line
.trim_start()
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit())
{
start_idx = j;
break;
}
}
}
if line.contains("__rust_begin_short_backtrace") {
for (j, prev_line) in lines
.iter()
.enumerate()
.take(i + 1)
.collect::<Vec<_>>()
.into_iter()
.rev()
{
if prev_line
.trim_start()
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit())
{
end_idx = j;
break;
}
}
break;
}
}
let filtered: Vec<&str> = lines[start_idx..end_idx].to_vec();
let mut new_frame_num = 0usize;
let mut result = Vec::new();
for line in filtered {
let trimmed = line.trim_start();
if trimmed.chars().next().is_some_and(|c| c.is_ascii_digit()) {
if let Some(colon_pos) = trimmed.find(':') {
let rest = &trimmed[colon_pos..];
result.push(format!("{:>4}{}", new_frame_num, rest));
new_frame_num += 1;
} else {
result.push(line.to_string());
}
} else {
result.push(line.to_string());
}
}
result.join("\n")
}
pub(crate) fn unknown_panic_info() -> (String, String, String, Backtrace) {
(
"<unknown>".to_string(),
"?".to_string(),
"<unknown>".to_string(),
Backtrace::disabled(),
)
}
pub(crate) fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
}
}
pub(crate) fn run_test_case(
data_source: Box<dyn DataSource>,
test_fn: &mut dyn FnMut(TestCase),
is_final: bool,
mode: Mode,
verbosity: crate::runner::Verbosity,
) -> TestCaseResult {
let tc = TestCase::new(data_source, is_final, mode);
let result = with_test_context(|| catch_unwind(AssertUnwindSafe(|| test_fn(tc.clone()))));
let tc_result = match &result {
Ok(()) => TestCaseResult::Valid,
Err(e) => {
let msg = panic_message(e);
if msg == ASSUME_FAIL_STRING {
TestCaseResult::Invalid
} else if msg == STOP_TEST_STRING {
TestCaseResult::Overrun
} else if msg == LOOP_DONE_STRING {
TestCaseResult::Valid
} else {
let (thread_name, thread_id, location, backtrace) =
take_panic_info().unwrap_or_else(unknown_panic_info);
let diagnostic =
render_diagnostic(&thread_name, &thread_id, &location, &msg, &backtrace);
TestCaseResult::Interesting(Failure {
panic_message: msg,
diagnostic,
origin: format!("Panic at {}", location),
})
}
}
};
tc.mark_complete(&tc_result);
let _ = (is_final, verbosity);
tc_result
}
fn render_diagnostic(
thread_name: &str,
thread_id: &str,
location: &str,
msg: &str,
backtrace: &Backtrace,
) -> String {
let mut out = String::new();
out.push_str(&format!(
"thread '{}' ({}) panicked at {}:\n",
thread_name, thread_id, location
));
out.push_str(msg);
out.push('\n');
if backtrace.status() == BacktraceStatus::Captured {
let is_full = std::env::var("RUST_BACKTRACE")
.map(|v| v == "full")
.unwrap_or(false);
let formatted = format_backtrace(backtrace, is_full);
out.push_str(&format!("stack backtrace:\n{}\n", formatted));
if !is_full {
out.push_str(
"note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.\n",
);
}
}
out
}
pub(crate) fn drive<R, F>(
runner: R,
test_fn: F,
settings: &Settings,
database_key: Option<&str>,
test_location: Option<&TestLocation>,
) where
R: TestRunner,
F: FnMut(TestCase),
{
init_panic_hook();
if !settings.phases.contains(&Phase::Generate) {
return;
}
let mut test_fn = test_fn;
let got_interesting = AtomicBool::new(false);
let mode = settings.mode;
let verbosity = settings.verbosity;
let result = runner.run(settings, database_key, &mut |backend, is_final| {
let tc_result = run_test_case(backend, &mut test_fn, is_final, mode, verbosity);
if matches!(&tc_result, TestCaseResult::Interesting(_)) {
got_interesting.store(true, Ordering::SeqCst);
}
});
let test_failed = !result.passed || got_interesting.load(Ordering::SeqCst);
crate::antithesis::require_antithesis_feature(
crate::antithesis::is_running_in_antithesis(),
cfg!(feature = "antithesis"),
);
#[cfg(feature = "antithesis")]
if crate::antithesis::is_running_in_antithesis() {
if let Some(loc) = test_location {
crate::antithesis::emit_assertion(loc, !test_failed);
}
}
let _ = test_location;
if !test_failed {
return;
}
let quiet = verbosity == crate::runner::Verbosity::Quiet;
match result.failures.as_slice() {
[] => panic!("Property test failed: unknown"),
[failure] => {
if !quiet {
eprint!("{}", failure.diagnostic);
}
panic!("Property test failed: {}", failure.panic_message);
}
failures => {
let n = failures.len();
if !quiet {
eprintln!("Hegel found {} failing test cases:", n);
for failure in failures {
eprint!("{}", failure.diagnostic);
}
}
panic!("Property-based test failed with {} distinct failures.", n);
}
}
}
#[cfg(test)]
#[path = "../tests/embedded/run_lifecycle_tests.rs"]
mod tests;