Skip to main content

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}