leviath_core/panic_payload.rs
1//! Rendering a caught panic payload as a human-readable message.
2//!
3//! Every place that recovers from a panic with [`std::panic::catch_unwind`]
4//! needs the same three-way downcast, so it lives here once rather than being
5//! re-derived per crate (`leviath-scripting`'s Rhai host-function guards and
6//! `leviath-runtime`'s pipeline-tick isolation both use it).
7
8use std::any::Any;
9
10/// Text used when a panic payload is neither a `String` nor a `&'static str`
11/// (e.g. `std::panic::panic_any(42)`).
12const UNKNOWN: &str = "unknown panic";
13
14/// Render a [`catch_unwind`](std::panic::catch_unwind) payload as a message.
15///
16/// `panic!("{x}")` yields a `String` payload and `panic!("literal")` a
17/// `&'static str`; anything else (`panic_any`) has no text to show and renders
18/// as `"unknown panic"`.
19pub fn panic_message(payload: &(dyn Any + Send)) -> String {
20 if let Some(s) = payload.downcast_ref::<String>() {
21 return s.clone();
22 }
23 if let Some(s) = payload.downcast_ref::<&'static str>() {
24 return (*s).to_string();
25 }
26 UNKNOWN.to_string()
27}
28
29#[cfg(test)]
30mod tests {
31 use super::*;
32
33 /// Run `f`, expecting it to panic, and return the rendered payload.
34 fn caught(f: impl FnOnce()) -> String {
35 let prev = std::panic::take_hook();
36 std::panic::set_hook(Box::new(|_| {})); // silence the expected panic
37 let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f))
38 .expect_err("the closure must panic");
39 std::panic::set_hook(prev);
40 panic_message(payload.as_ref())
41 }
42
43 #[test]
44 fn renders_all_three_payload_kinds() {
45 // `panic!("{}", …)` → String payload.
46 let formatted = "formatted message";
47 assert_eq!(caught(|| panic!("{formatted}")), "formatted message");
48 // `panic!("literal")` → &'static str payload.
49 assert_eq!(caught(|| panic!("literal message")), "literal message");
50 // Anything else has no text.
51 assert_eq!(caught(|| std::panic::panic_any(42_i32)), UNKNOWN);
52 }
53}