| use std::backtrace::{Backtrace, BacktraceStatus}; |
| use std::cell::Cell; |
| use std::fmt::{Display, Write}; |
| use std::panic::PanicHookInfo; |
| use std::sync::{Arc, LazyLock, Mutex}; |
| use std::{env, mem, panic, thread}; |
| |
| type PanicHook = Box<dyn Fn(&PanicHookInfo<'_>) + Sync + Send + 'static>; |
| type CaptureBuf = Arc<Mutex<String>>; |
| |
| thread_local!( |
| static CAPTURE_BUF: Cell<Option<CaptureBuf>> = const { Cell::new(None) }; |
| ); |
| |
| /// Installs a custom panic hook that will divert panic output to a thread-local |
| /// capture buffer, but only for threads that have a capture buffer set. |
| /// |
| /// Otherwise, the custom hook delegates to a copy of the default panic hook. |
| pub(crate) fn install_panic_hook() { |
| let default_hook = panic::take_hook(); |
| panic::set_hook(Box::new(move |info| custom_panic_hook(&default_hook, info))); |
| } |
| |
| pub(crate) fn set_capture_buf(buf: CaptureBuf) { |
| CAPTURE_BUF.set(Some(buf)); |
| } |
| |
| pub(crate) fn take_capture_buf() -> Option<CaptureBuf> { |
| CAPTURE_BUF.take() |
| } |
| |
| fn custom_panic_hook(default_hook: &PanicHook, info: &panic::PanicHookInfo<'_>) { |
| // Temporarily taking the capture buffer means that if a panic occurs in |
| // the subsequent code, that panic will fall back to the default hook. |
| let Some(buf) = take_capture_buf() else { |
| // There was no capture buffer, so delegate to the default hook. |
| default_hook(info); |
| return; |
| }; |
| |
| let mut out = buf.lock().unwrap_or_else(|e| e.into_inner()); |
| |
| let thread = thread::current().name().unwrap_or("(test runner)").to_owned(); |
| let location = get_location(info); |
| let payload = info.payload_as_str().unwrap_or("Box<dyn Any>"); |
| let backtrace = Backtrace::capture(); |
| |
| writeln!(out, "\nthread '{thread}' panicked at {location}:\n{payload}").unwrap(); |
| match backtrace.status() { |
| BacktraceStatus::Captured => { |
| let bt = trim_backtrace(backtrace.to_string()); |
| write!(out, "stack backtrace:\n{bt}",).unwrap(); |
| } |
| BacktraceStatus::Disabled => { |
| writeln!( |
| out, |
| "note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace", |
| ) |
| .unwrap(); |
| } |
| _ => {} |
| } |
| |
| drop(out); |
| set_capture_buf(buf); |
| } |
| |
| fn get_location<'a>(info: &'a PanicHookInfo<'_>) -> &'a dyn Display { |
| match info.location() { |
| Some(location) => location, |
| None => &"(unknown)", |
| } |
| } |
| |
| fn rust_backtrace_full() -> bool { |
| static RUST_BACKTRACE_FULL: LazyLock<bool> = |
| LazyLock::new(|| matches!(env::var("RUST_BACKTRACE").as_deref(), Ok("full"))); |
| *RUST_BACKTRACE_FULL |
| } |
| |
| /// On stable, short backtraces are only available to the default panic hook, |
| /// so if we want something similar we have to resort to string processing. |
| fn trim_backtrace(full_backtrace: String) -> String { |
| if rust_backtrace_full() { |
| return full_backtrace; |
| } |
| |
| let mut buf = String::with_capacity(full_backtrace.len()); |
| // Don't print any frames until after the first `__rust_end_short_backtrace`. |
| let mut on = false; |
| // After the short-backtrace state is toggled, skip its associated "at" if present. |
| let mut skip_next_at = false; |
| |
| let mut lines = full_backtrace.lines(); |
| while let Some(line) = lines.next() { |
| if mem::replace(&mut skip_next_at, false) && line.trim_start().starts_with("at ") { |
| continue; |
| } |
| |
| if line.contains("__rust_end_short_backtrace") { |
| on = true; |
| skip_next_at = true; |
| continue; |
| } |
| if line.contains("__rust_begin_short_backtrace") { |
| on = false; |
| skip_next_at = true; |
| continue; |
| } |
| |
| if on { |
| writeln!(buf, "{line}").unwrap(); |
| } |
| } |
| |
| writeln!( |
| buf, |
| "note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace." |
| ) |
| .unwrap(); |
| |
| buf |
| } |