use super::types::{Invocation, SUBJECT_PANIC_CAUSE};
use crate::descriptor::ExecutableAttachment;
use crate::report::{FailureClass, FindingLocation, ForeignText, TrialConclusion, TrialFinding};
use std::any::Any;
use std::cell::Cell;
use std::panic::{AssertUnwindSafe, Location, catch_unwind};
use std::sync::OnceLock;
struct PanicOrigin {
file: String,
line: u32,
}
static CAPTURE_HOOK: OnceLock<()> = OnceLock::new();
thread_local! {
static PANIC_ORIGIN: Cell<Option<PanicOrigin>> = const { Cell::new(None) };
}
pub(super) fn caught_conclusion(
attachment: &ExecutableAttachment<Invocation, TrialConclusion>,
invocation: &Invocation,
) -> TrialConclusion {
install_capture_hook();
store_origin(None);
match catch_unwind(AssertUnwindSafe(|| attachment.conclude(invocation))) {
Ok(conclusion) => conclusion,
Err(payload) => {
let origin = taken_origin();
TrialConclusion::Refused(panic_finding(payload.as_ref(), origin.as_ref()))
}
}
}
fn panic_finding(payload: &(dyn Any + Send), origin: Option<&PanicOrigin>) -> TrialFinding {
TrialFinding::established(
FailureClass::SubjectPanic,
SUBJECT_PANIC_CAUSE,
FindingLocation::at(file!(), line!()),
foreign_material(origin, payload_text(payload)),
)
}
fn payload_text(payload: &(dyn Any + Send)) -> Option<&str> {
payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
}
fn foreign_material(origin: Option<&PanicOrigin>, payload: Option<&str>) -> Option<ForeignText> {
let material = match (origin, payload) {
(Some(place), Some(text)) => format!("{}:{}: {text}", place.file, place.line),
(Some(place), None) => format!("{}:{}", place.file, place.line),
(None, Some(text)) => text.to_owned(),
(None, None) => return None,
};
Some(ForeignText::admitted(material.as_bytes()))
}
fn install_capture_hook() {
let _installed = CAPTURE_HOOK.get_or_init(chain_capture_hook);
}
fn chain_capture_hook() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
store_origin(info.location().map(observed));
previous(info);
}));
}
fn observed(location: &Location<'_>) -> PanicOrigin {
PanicOrigin {
file: location.file().to_owned(),
line: location.line(),
}
}
fn store_origin(origin: Option<PanicOrigin>) {
let _stored = PANIC_ORIGIN.try_with(|slot| slot.set(origin));
}
fn taken_origin() -> Option<PanicOrigin> {
PANIC_ORIGIN.try_with(Cell::take).unwrap_or(None)
}