use std::backtrace::{Backtrace, BacktraceStatus};
use std::cell::{Cell, RefCell};
use std::panic::{self, AssertUnwindSafe, catch_unwind};
use std::sync::Once;
use crate::antithesis::TestLocation;
use crate::backend::{DataSource, Exploration, Failure, TestCaseResult, TestRunner};
use crate::control::{
AssumeFailed, InternalError, InvalidArgument, LoopDone, StopTest, currently_in_test_context,
with_test_context,
};
use crate::runner::{Mode, Settings};
use crate::test_case::TestCase;
static PANIC_HOOK_INIT: Once = Once::new();
thread_local! {
static LAST_PANIC_INFO: RefCell<Option<(String, String, String, Backtrace)>> =
const { RefCell::new(None) };
static CAPTURE_BACKTRACE: Cell<bool> = const { Cell::new(false) };
}
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 = if CAPTURE_BACKTRACE.get() {
Backtrace::capture()
} else {
Backtrace::disabled()
};
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(),
)
}
#[doc(hidden)]
pub 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 + Send + Sync>,
test_fn: &mut dyn FnMut(TestCase),
is_final: bool,
mode: Mode,
verbosity: crate::runner::Verbosity,
) -> (TestCaseResult, Option<Box<dyn std::any::Any + Send>>) {
let verbose = matches!(
verbosity,
crate::runner::Verbosity::Verbose | crate::runner::Verbosity::Debug
);
let quiet = verbosity == crate::runner::Verbosity::Quiet;
let should_emit = (is_final && !quiet) || verbose;
CAPTURE_BACKTRACE.with(|c| c.set(should_emit));
let tc = TestCase::new(data_source, should_emit, mode);
let result = with_test_context(|| catch_unwind(AssertUnwindSafe(|| test_fn(tc.clone()))));
let (tc_result, payload) = match result {
Ok(()) => (TestCaseResult::Valid, None),
Err(e) if e.downcast_ref::<AssumeFailed>().is_some() => (TestCaseResult::Invalid, None),
Err(e) if e.downcast_ref::<StopTest>().is_some() => (TestCaseResult::Overrun, None),
Err(e) if e.downcast_ref::<LoopDone>().is_some() => (TestCaseResult::Valid, None),
Err(e) => {
let e = match e.downcast::<InvalidArgument>() {
Ok(invalid) => std::panic::resume_unwind(Box::new(invalid.0)),
Err(e) => e,
};
let e = match e.downcast::<InternalError>() {
Ok(internal) => std::panic::resume_unwind(Box::new(internal.0)),
Err(e) => e,
};
let msg = panic_message(&e);
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);
if is_final && !quiet {
eprint!("{diagnostic}");
} else if verbose {
for line in diagnostic.trim_end_matches('\n').split('\n') {
crate::test_case::emit_verbose_line(line);
}
}
let failure = TestCaseResult::Interesting(Failure {
panic_message: msg,
origin: format!("Panic at {}", location),
reproduce_blob: None,
});
(failure, Some(e))
}
};
if verbose {
emit_verbose_stop_reason(&tc_result);
}
tc.mark_complete(&tc_result);
(tc_result, payload)
}
fn emit_verbose_stop_reason(result: &TestCaseResult) {
match result {
TestCaseResult::Invalid => {
crate::test_case::emit_verbose_line("Test case stopped: failed assumption");
}
TestCaseResult::Overrun => {
crate::test_case::emit_verbose_line("Test case stopped: out of data");
}
TestCaseResult::Valid | TestCaseResult::Interesting(_) => {}
}
}
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
}
fn reproducer_line(settings: &Settings, failure: &crate::backend::Failure) -> Option<String> {
if !settings.print_blob {
return None;
}
let blob = failure.reproduce_blob.as_ref()?;
Some(format!(
"\nTo reproduce this failure, add the attribute below \
#[hegel::test]:\n #[hegel::reproduce_failure(\"{blob}\")]"
))
}
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();
require_antithesis_feature();
let mut test_fn = test_fn;
let mode = settings.mode;
let verbosity = settings.verbosity;
let exploration = {
let mut explore_case = |backend: Box<dyn DataSource + Send + Sync>| {
run_test_case(backend, &mut test_fn, false, mode, verbosity);
};
runner.explore(settings, database_key, &mut explore_case)
};
let test_failed = !matches!(exploration, Ok(Exploration::Passed));
emit_antithesis_assertion(test_failed, test_location);
if !test_failed {
return;
}
let quiet = verbosity == crate::runner::Verbosity::Quiet;
let counterexamples = match exploration {
Err(error) => panic!("{error}"),
Ok(Exploration::Passed) => unreachable!(),
Ok(Exploration::Counterexamples(counterexamples)) => counterexamples,
};
let multiple = counterexamples.len() > 1;
if multiple && !quiet {
eprintln!(
"Property-based test failed with {} distinct failures.",
counterexamples.len()
);
}
let last_payload: RefCell<Option<Box<dyn std::any::Any + Send>>> = RefCell::new(None);
let mut final_case = |backend: Box<dyn DataSource + Send + Sync>| {
let (_, payload) = run_test_case(backend, &mut test_fn, true, mode, verbosity);
*last_payload.borrow_mut() = payload;
};
let mut reported: Vec<String> = Vec::new();
for counterexample in counterexamples {
if multiple && !quiet {
eprintln!();
}
let failure = match runner.replay_final(counterexample, &mut final_case) {
Ok(failure) => failure,
Err(error) => panic!("{error}"),
};
if let Some(line) = reproducer_line(settings, &failure) {
eprintln!("{line}");
}
reported.push(failure.panic_message);
}
match reported.as_slice() {
[] => panic!("Property test failed: unknown"),
[message] => match last_payload.borrow_mut().take() {
Some(payload) => std::panic::resume_unwind(payload),
None => panic!("Property test failed: {}", message),
},
many => std::panic::resume_unwind(Box::new(format!(
"Property-based test failed with {} distinct failures.",
many.len()
))),
}
}
pub(crate) fn drive_single<F>(
test_fn: F,
settings: &Settings,
database_key: Option<&str>,
test_location: Option<&TestLocation>,
) where
F: FnMut(TestCase),
{
init_panic_hook();
require_antithesis_feature();
let mut test_fn = test_fn;
let last_payload: RefCell<Option<Box<dyn std::any::Any + Send>>> = RefCell::new(None);
let failure =
crate::native::test_runner::run_single_case(settings, database_key, &mut |backend| {
let (_, payload) = run_test_case(
backend,
&mut test_fn,
true,
settings.mode,
settings.verbosity,
);
*last_payload.borrow_mut() = payload;
});
emit_antithesis_assertion(failure.is_some(), test_location);
if failure.is_none() {
return;
}
match last_payload.borrow_mut().take() {
Some(payload) => std::panic::resume_unwind(payload),
None => unreachable!(),
}
}
fn require_antithesis_feature() {
crate::antithesis::require_antithesis_feature(
crate::antithesis::is_running_in_antithesis(),
cfg!(feature = "antithesis"),
);
}
fn emit_antithesis_assertion(test_failed: bool, test_location: Option<&TestLocation>) {
#[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_failed, test_location);
}
#[cfg(test)]
#[path = "../tests/embedded/run_lifecycle_tests.rs"]
mod tests;