use std::backtrace::{Backtrace, BacktraceStatus};
use std::cell::{Cell, RefCell};
use std::panic::{self, AssertUnwindSafe, catch_unwind};
use std::sync::{Arc, Once};
use crate::antithesis::TestLocation;
use crate::backend::{Failure, TestCaseResult};
use crate::control::{
AssumeFailed, InternalError, InvalidArgument, LoopDone, StopTest, currently_in_test_context,
hegel_internal_error, with_test_context,
};
use crate::ffi::{CTestCase, RunHandle, SettingsHandle};
use crate::runner::{Mode, Settings, Verbosity};
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(
c_tc: CTestCase,
test_fn: &mut dyn FnMut(TestCase),
is_final: bool,
mode: Mode,
verbosity: Verbosity,
) -> (
TestCaseResult,
Option<Box<dyn std::any::Any + Send>>,
Option<String>,
) {
let verbose = matches!(verbosity, Verbosity::Verbose | Verbosity::Debug);
let quiet = verbosity == Verbosity::Quiet;
let should_emit = (is_final && !quiet) || verbose;
CAPTURE_BACKTRACE.with(|c| c.set(should_emit));
take_panic_info();
let c_tc = Arc::new(c_tc);
let tc = TestCase::new(Arc::clone(&c_tc), should_emit, mode);
let result = with_test_context(|| catch_unwind(AssertUnwindSafe(|| test_fn(tc))));
let (tc_result, payload, diagnostic) = match result {
Ok(()) => (TestCaseResult::Valid, None, None),
Err(e) if e.downcast_ref::<AssumeFailed>().is_some() => {
(TestCaseResult::Invalid, None, None)
}
Err(e) if e.downcast_ref::<StopTest>().is_some() => (TestCaseResult::Overrun, None, None),
Err(e) if e.downcast_ref::<LoopDone>().is_some() => (TestCaseResult::Valid, None, 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 (thread_name, thread_id, location, backtrace) =
take_panic_info().unwrap_or_else(unknown_panic_info);
let captured = if is_final && !quiet {
let msg = panic_message(&e);
Some(render_diagnostic(
&thread_name,
&thread_id,
&location,
&msg,
&backtrace,
))
} else {
if verbose {
let msg = panic_message(&e);
let diagnostic =
render_diagnostic(&thread_name, &thread_id, &location, &msg, &backtrace);
for line in diagnostic.trim_end_matches('\n').split('\n') {
crate::test_case::emit_verbose_line(line);
}
}
None
};
let failure = TestCaseResult::Interesting(Failure {
origin: format!("Panic at {}", location),
});
(failure, Some(e), captured)
}
};
if verbose {
emit_verbose_stop_reason(&tc_result);
}
report_outcome(&c_tc, &tc_result);
(tc_result, payload, diagnostic)
}
fn report_outcome(handle: &CTestCase, result: &TestCaseResult) {
use hegel_c::hegel_status_t as Status;
let (status, origin) = match result {
TestCaseResult::Valid => (Status::HEGEL_STATUS_VALID, None),
TestCaseResult::Invalid => (Status::HEGEL_STATUS_INVALID, None),
TestCaseResult::Overrun => (Status::HEGEL_STATUS_OVERRUN, None),
TestCaseResult::Interesting(failure) => (
Status::HEGEL_STATUS_INTERESTING,
Some(failure.origin.as_str()),
),
};
handle
.mark_complete(status, origin)
.unwrap_or_else(|rc| crate::test_case::raise_for_rc(rc));
}
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, reproduce_blob: Option<&str>) -> Option<String> {
if !settings.print_blob {
return None;
}
let blob = reproduce_blob?;
Some(format!(
"\nTo reproduce this failure, add the attribute below \
#[hegel::test]:\n #[hegel::reproduce_failure(\"{blob}\")]"
))
}
const FLAKY_DIAGNOSTIC: &str = "Flaky test detected: Your test produced different outcomes \
when run with the same generated data — it failed when it \
previously succeeded, or succeeded when it previously failed. \
This usually means your test depends on external state such as \
global variables, system time, or external random number generators.";
pub(crate) fn drive<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 mode = settings.mode;
let verbosity = settings.verbosity;
let quiet = verbosity == Verbosity::Quiet;
let c_settings = SettingsHandle::build(settings, database_key);
let run = match RunHandle::start(&c_settings) {
Ok(run) => run,
Err(message) => panic!("{message}"), };
if mode == Mode::SingleTestCase {
drive_single_case(&run, &mut test_fn, verbosity, test_location);
return;
}
while let Some(c_tc) = run.next_test_case() {
run_test_case(c_tc, &mut test_fn, false, mode, verbosity);
}
let result = run.result();
use hegel_c::hegel_run_status_t as RunStatus;
let status = result.status();
emit_antithesis_assertion(status != RunStatus::HEGEL_RUN_STATUS_PASSED, test_location);
match status {
RunStatus::HEGEL_RUN_STATUS_PASSED => {}
RunStatus::HEGEL_RUN_STATUS_ERROR => {
let message = result
.error()
.unwrap_or_else(|| "the run failed with an unknown error".to_string());
panic!("{message}");
}
RunStatus::HEGEL_RUN_STATUS_FAILED => {
let count = result.failure_count();
let multiple = count > 1;
if multiple && !quiet {
eprintln!("Property-based test failed with {count} distinct failures.");
}
let mut last_payload: Option<Box<dyn std::any::Any + Send>> = None;
for index in 0..count {
if multiple && !quiet {
eprintln!();
}
let blob = result
.failure(index)
.reproduce_blob
.unwrap_or_else(|| hegel_internal_error!("failure {index} has no blob"));
let c_tc = match CTestCase::from_blob(&c_settings, &blob) {
Ok(c_tc) => c_tc,
Err(message) => panic!("{message}"), };
let (tc_result, payload, diagnostic) =
run_test_case(c_tc, &mut test_fn, true, mode, verbosity);
if !matches!(tc_result, TestCaseResult::Interesting(_)) {
panic!("{FLAKY_DIAGNOSTIC}");
}
if let Some(diagnostic) = diagnostic {
eprint!("{diagnostic}");
}
if let Some(line) = reproducer_line(settings, Some(blob.as_str())) {
eprintln!("{line}");
}
last_payload = payload;
}
if multiple {
std::panic::resume_unwind(Box::new(format!(
"Property-based test failed with {count} distinct failures."
)));
} else {
std::panic::resume_unwind(
last_payload.expect("a re-failing replay carries a panic payload"),
);
}
}
}
}
fn drive_single_case(
run: &RunHandle,
test_fn: &mut dyn FnMut(TestCase),
verbosity: Verbosity,
test_location: Option<&TestLocation>,
) {
let c_tc = run
.next_test_case()
.expect("a SingleTestCase run produces exactly one test case");
let (result, payload, diagnostic) =
run_test_case(c_tc, test_fn, true, Mode::SingleTestCase, verbosity);
if matches!(result, TestCaseResult::Interesting(_)) {
emit_antithesis_assertion(true, test_location);
if let Some(diagnostic) = diagnostic {
eprint!("{diagnostic}");
}
std::panic::resume_unwind(
payload.expect("an interesting case carries the caught panic payload"),
);
}
emit_antithesis_assertion(false, test_location);
}
pub(crate) fn drive_blob_replay<F>(
test_fn: F,
settings: &Settings,
database_key: Option<&str>,
blob: &str,
test_location: Option<&TestLocation>,
) where
F: FnMut(TestCase),
{
init_panic_hook();
require_antithesis_feature();
let mut test_fn = test_fn;
let c_settings = SettingsHandle::build(settings, database_key);
let c_tc = match CTestCase::from_blob(&c_settings, blob) {
Ok(c_tc) => c_tc,
Err(message) => panic!("{message}"),
};
let (result, payload, diagnostic) =
run_test_case(c_tc, &mut test_fn, true, settings.mode, settings.verbosity);
if let Some(diagnostic) = diagnostic {
eprint!("{diagnostic}");
}
match result {
TestCaseResult::Interesting(_) => {
emit_antithesis_assertion(true, test_location);
match payload {
Some(payload) => std::panic::resume_unwind(payload),
None => unreachable!(), }
}
_ => {
emit_antithesis_assertion(false, test_location);
panic!(
"reproduce_failure: the supplied failure blob no longer reproduces a \
failure. The failure may have been fixed, or the blob is stale."
);
}
}
}
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;