reproducible_panic/
lib.rs

1//! A panic hook that mimics the default panic hook, but without printing non-reproducible information.
2//!
3//! This is useful for snapshot tests where you compare the output of a program to verify it is still functioning correct.
4//! If the program panics, the default hook includes the ID of the panicking thread, which is different on every run.
5//!
6//! Rather than trying to filter it out, you can have the program install this panic hook to prevent it from being printed in the first place.
7//!
8//! # Example
9//!
10//! ```rust,should_panic
11//! fn main() {
12//!   reproducible_panic::install();
13//!   panic!("Oh no!");
14//! }
15//! ```
16//!
17//! Produces the following output:
18//!
19//! ```text
20//! thread 'main' panicked at examples/example.rs:3:5
21//! Oh no!
22//! note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
23//! ```
24//!
25//! In contrast, with the default panic hook the first line would look like this:
26//!
27//! ```text
28//! thread 'main' (12993) panicked at examples/example.rs:3:5:
29//! ```
30//!
31//! Note the "12993" in the output. This number will be different every time you run the program, ruining your snapshot tests.
32
33// The `fn main()` is included to show intended use in a full program.
34#![allow(clippy::needless_doctest_main)]
35
36// Need to use the deprecated `std::panic::PanicInfo` to support Rust 1.70.
37#![allow(deprecated)]
38
39use std::panic::PanicInfo as PanicHookInfo;
40
41use std::sync::atomic::{AtomicBool, Ordering};
42use std::io::Write;
43
44/// Install [`panic_hook()`] as the global panic hook.
45pub fn install() {
46	std::panic::set_hook(Box::new(panic_hook));
47}
48
49/// A panic hook that doesn't print any non-reproducible information by default.
50///
51/// The hook tries to mimic the default hook, except that it does not print non-reproducible information like the ID of the panicking thread by default.
52///
53/// However, if you set `RUST_BACKTRACE=full`, the printed backtrace will almost certainly include non-reproducible output.
54pub fn panic_hook(info: &PanicHookInfo<'_>) {
55	let backtrace = std::backtrace::Backtrace::capture();
56	let location = info.location();
57	let msg = payload_as_str(info);
58	let current_thread = std::thread::current();
59	let thread_name = current_thread.name().unwrap_or("<unnamed>");
60	let mut stderr = std::io::stderr().lock();
61
62
63	if let Some(location) = location {
64		writeln!(stderr, "\nthread '{thread_name}' panicked at {location}").ok();
65	} else {
66		writeln!(stderr, "\nthread '{thread_name}' panicked").ok();
67	}
68	if let Some(msg) = msg {
69		writeln!(stderr, "{msg}").ok();
70	}
71
72	static FIRST_PANIC: AtomicBool = AtomicBool::new(true);
73
74	match backtrace.status() {
75		std::backtrace::BacktraceStatus::Captured => {
76			if std::env::var_os("RUST_BACKTRACE").is_some_and(|x| x == "full") {
77				writeln!(&mut stderr, "stack backtrace:\n{backtrace:#}").ok();
78			} else {
79				writeln!(&mut stderr, "stack backtrace:\n{backtrace}").ok();
80			}
81		}
82		std::backtrace::BacktraceStatus::Disabled => {
83			if FIRST_PANIC.swap(false, Ordering::Relaxed) {
84				writeln!(
85					&mut stderr,
86					"note: run with `RUST_BACKTRACE=1` environment variable to display a \
87					backtrace"
88				).ok();
89				if cfg!(miri) {
90					writeln!(
91						&mut stderr,
92						"note: in Miri, you may have to set `MIRIFLAGS=-Zmiri-env-forward=RUST_BACKTRACE` \
93						for the environment variable to have an effect"
94					).ok();
95				}
96			}
97		}
98		std::backtrace::BacktraceStatus::Unsupported => (),
99		_ => (),
100	}
101}
102
103pub fn payload_as_str<'a>(info: &'a PanicHookInfo<'a>) -> Option<&'a str> {
104	if let Some(s) = info.payload().downcast_ref::<&str>() {
105		Some(s)
106	} else if let Some(s) = info.payload().downcast_ref::<String>() {
107		Some(s)
108	} else {
109		None
110	}
111}