From eb4a737b5c843234569defaae1955d4ff89f73fe Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 31 Mar 2026 08:19:39 -0500 Subject: [PATCH 01/10] refactor: Pull out a metadata mod --- src/lib.rs | 67 +++---------------------------------------------- src/metadata.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 63 deletions(-) create mode 100644 src/metadata.rs diff --git a/src/lib.rs b/src/lib.rs index 0b518ce..355d7f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,76 +48,17 @@ #[cfg(doctest)] pub struct ReadmeDoctests; +mod metadata; + pub mod report; +pub use metadata::Metadata; + use report::{Method, Report}; -use std::borrow::Cow; use std::io::Result as IoResult; use std::panic::PanicHookInfo; use std::path::{Path, PathBuf}; -/// A convenient metadata struct that describes a crate -/// -/// See [`metadata!`] -pub struct Metadata { - name: Cow<'static, str>, - version: Cow<'static, str>, - authors: Option>, - homepage: Option>, - repository: Option>, - support: Option>, -} - -impl Metadata { - /// See [`metadata!`] - pub fn new(name: impl Into>, version: impl Into>) -> Self { - Self { - name: name.into(), - version: version.into(), - authors: None, - homepage: None, - repository: None, - support: None, - } - } - - /// The list of authors of the crate - pub fn authors(mut self, value: impl Into>) -> Self { - let value = value.into(); - if !value.is_empty() { - self.authors = value.into(); - } - self - } - - /// The URL of the crate's website - pub fn homepage(mut self, value: impl Into>) -> Self { - let value = value.into(); - if !value.is_empty() { - self.homepage = value.into(); - } - self - } - - /// The URL of the crate's repository - pub fn repository(mut self, value: impl Into>) -> Self { - let value = value.into(); - if !value.is_empty() { - self.repository = value.into(); - } - self - } - - /// The support information - pub fn support(mut self, value: impl Into>) -> Self { - let value = value.into(); - if !value.is_empty() { - self.support = value.into(); - } - self - } -} - /// Initialize [`Metadata`] #[macro_export] macro_rules! metadata { diff --git a/src/metadata.rs b/src/metadata.rs new file mode 100644 index 0000000..6473874 --- /dev/null +++ b/src/metadata.rs @@ -0,0 +1,63 @@ +use std::borrow::Cow; + +/// A convenient metadata struct that describes a crate +/// +/// See [`metadata!`][crate::metadata!] +pub struct Metadata { + pub(crate) name: Cow<'static, str>, + pub(crate) version: Cow<'static, str>, + pub(crate) authors: Option>, + pub(crate) homepage: Option>, + pub(crate) repository: Option>, + pub(crate) support: Option>, +} + +impl Metadata { + /// See [`metadata!`][crate::metadata!] + pub fn new(name: impl Into>, version: impl Into>) -> Self { + Self { + name: name.into(), + version: version.into(), + authors: None, + homepage: None, + repository: None, + support: None, + } + } + + /// The list of authors of the crate + pub fn authors(mut self, value: impl Into>) -> Self { + let value = value.into(); + if !value.is_empty() { + self.authors = value.into(); + } + self + } + + /// The URL of the crate's website + pub fn homepage(mut self, value: impl Into>) -> Self { + let value = value.into(); + if !value.is_empty() { + self.homepage = value.into(); + } + self + } + + /// The URL of the crate's repository + pub fn repository(mut self, value: impl Into>) -> Self { + let value = value.into(); + if !value.is_empty() { + self.repository = value.into(); + } + self + } + + /// The support information + pub fn support(mut self, value: impl Into>) -> Self { + let value = value.into(); + if !value.is_empty() { + self.support = value.into(); + } + self + } +} From aa97ea63e83184154a5da31c92d31d1b221a8f6b Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 31 Mar 2026 08:21:58 -0500 Subject: [PATCH 02/10] refactor: Merge print_msg functions --- src/lib.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 355d7f2..fc75cc6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -148,26 +148,26 @@ impl Default for PanicStyle { } /// Utility function that prints a message to our human users -#[cfg(feature = "color")] pub fn print_msg>(file_path: Option

, meta: &Metadata) -> IoResult<()> { - use std::io::Write as _; - - let stderr = anstream::stderr(); - let mut stderr = stderr.lock(); + #[cfg(feature = "color")] + { + use std::io::Write as _; - write!(stderr, "{}", anstyle::AnsiColor::Red.render_fg())?; - write_msg(&mut stderr, file_path, meta)?; - write!(stderr, "{}", anstyle::Reset.render())?; + let stderr = anstream::stderr(); + let mut stderr = stderr.lock(); - Ok(()) -} + write!(stderr, "{}", anstyle::AnsiColor::Red.render_fg())?; + write_msg(&mut stderr, file_path, meta)?; + write!(stderr, "{}", anstyle::Reset.render())?; + } -#[cfg(not(feature = "color"))] -pub fn print_msg>(file_path: Option

, meta: &Metadata) -> IoResult<()> { - let stderr = std::io::stderr(); - let mut stderr = stderr.lock(); + #[cfg(not(feature = "color"))] + { + let stderr = std::io::stderr(); + let mut stderr = stderr.lock(); - write_msg(&mut stderr, file_path, meta)?; + write_msg(&mut stderr, file_path, meta)?; + } Ok(()) } From 5b340e53a98b838913491cdfc4d89beaeafbb0e9 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 31 Mar 2026 08:24:46 -0500 Subject: [PATCH 03/10] refactor: Pull out panic logic --- src/lib.rs | 182 ++------------------------------------------------- src/panic.rs | 178 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 177 deletions(-) create mode 100644 src/panic.rs diff --git a/src/lib.rs b/src/lib.rs index fc75cc6..1b641dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,15 +49,14 @@ pub struct ReadmeDoctests; mod metadata; +mod panic; pub mod report; pub use metadata::Metadata; - -use report::{Method, Report}; - -use std::io::Result as IoResult; -use std::panic::PanicHookInfo; -use std::path::{Path, PathBuf}; +pub use panic::PanicStyle; +pub use panic::handle_dump; +pub use panic::print_msg; +pub use panic::setup_panic; /// Initialize [`Metadata`] #[macro_export] @@ -102,174 +101,3 @@ macro_rules! setup_panic { $crate::setup_panic!($crate::metadata!()); }; } - -#[doc(hidden)] -pub fn setup_panic(meta: impl Fn() -> Metadata) { - #![allow(deprecated)] - - #[allow(unused_imports)] - use std::panic; - - match PanicStyle::default() { - PanicStyle::Debug => {} - PanicStyle::Human => { - let meta = meta(); - - panic::set_hook(Box::new(move |info: &PanicHookInfo<'_>| { - let file_path = handle_dump(&meta, info); - print_msg(file_path, &meta) - .expect("human-panic: printing error message to console failed"); - })); - } - } -} - -/// Style of panic to be used -#[non_exhaustive] -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum PanicStyle { - /// Normal panic - Debug, - /// Human-formatted panic - Human, -} - -impl Default for PanicStyle { - fn default() -> Self { - if cfg!(debug_assertions) { - PanicStyle::Debug - } else { - match ::std::env::var("RUST_BACKTRACE") { - Ok(_) => PanicStyle::Debug, - Err(_) => PanicStyle::Human, - } - } - } -} - -/// Utility function that prints a message to our human users -pub fn print_msg>(file_path: Option

, meta: &Metadata) -> IoResult<()> { - #[cfg(feature = "color")] - { - use std::io::Write as _; - - let stderr = anstream::stderr(); - let mut stderr = stderr.lock(); - - write!(stderr, "{}", anstyle::AnsiColor::Red.render_fg())?; - write_msg(&mut stderr, file_path, meta)?; - write!(stderr, "{}", anstyle::Reset.render())?; - } - - #[cfg(not(feature = "color"))] - { - let stderr = std::io::stderr(); - let mut stderr = stderr.lock(); - - write_msg(&mut stderr, file_path, meta)?; - } - - Ok(()) -} - -fn write_msg>( - buffer: &mut impl std::io::Write, - file_path: Option

, - meta: &Metadata, -) -> IoResult<()> { - let Metadata { - name, - authors, - homepage, - repository, - support, - .. - } = meta; - - writeln!(buffer, "Well, this is embarrassing.\n")?; - writeln!( - buffer, - "{name} had a problem and crashed. To help us diagnose the \ - problem you can send us a crash report.\n" - )?; - writeln!( - buffer, - "We have generated a report file at \"{}\". Submit an \ - issue or email with the subject of \"{} Crash Report\" and include the \ - report as an attachment.\n", - match file_path { - Some(fp) => format!("{}", fp.as_ref().display()), - None => "".to_owned(), - }, - name - )?; - - if let Some(homepage) = homepage { - writeln!(buffer, "- Homepage: {homepage}")?; - } else if let Some(repository) = repository { - writeln!(buffer, "- Repository: {repository}")?; - } - if let Some(authors) = authors { - writeln!(buffer, "- Authors: {authors}")?; - } - if let Some(support) = support { - writeln!(buffer, "\nTo submit the crash report:\n\n{support}")?; - } - writeln!( - buffer, - "\nWe take privacy seriously, and do not perform any \ - automated error collection. In order to improve the software, we rely on \ - people to submit reports.\n" - )?; - writeln!(buffer, "Thank you kindly!")?; - - Ok(()) -} - -/// Utility function which will handle dumping information to disk -#[allow(deprecated)] -pub fn handle_dump(meta: &Metadata, panic_info: &PanicHookInfo<'_>) -> Option { - let mut expl = String::new(); - - let message = match ( - panic_info.payload().downcast_ref::<&str>(), - panic_info.payload().downcast_ref::(), - ) { - (Some(s), _) => Some((*s).to_owned()), - (_, Some(s)) => Some(s.to_owned()), - (None, None) => None, - }; - - let cause = match message { - Some(m) => m, - None => "Unknown".into(), - }; - - match panic_info.location() { - Some(location) => expl.push_str(&format!( - "Panic occurred in file '{}' at line {}\n", - location.file(), - location.line() - )), - None => expl.push_str("Panic location unknown.\n"), - } - - let report = Report::new(&meta.name, &meta.version, Method::Panic, expl, cause); - - if let Ok(f) = report.persist() { - Some(f) - } else { - use std::io::Write as _; - let stderr = std::io::stderr(); - let mut stderr = stderr.lock(); - - let _ = writeln!( - stderr, - "{}", - report - .serialize() - .expect("only doing toml compatible types") - ); - None - } -} diff --git a/src/panic.rs b/src/panic.rs new file mode 100644 index 0000000..7dae682 --- /dev/null +++ b/src/panic.rs @@ -0,0 +1,178 @@ +use std::io::Result as IoResult; +use std::panic::PanicHookInfo; +use std::path::{Path, PathBuf}; + +use crate::Metadata; +use crate::report::Method; +use crate::report::Report; + +#[doc(hidden)] +pub fn setup_panic(meta: impl Fn() -> Metadata) { + #![allow(deprecated)] + + #[allow(unused_imports)] + use std::panic; + + match PanicStyle::default() { + PanicStyle::Debug => {} + PanicStyle::Human => { + let meta = meta(); + + panic::set_hook(Box::new(move |info: &PanicHookInfo<'_>| { + let file_path = handle_dump(&meta, info); + print_msg(file_path, &meta) + .expect("human-panic: printing error message to console failed"); + })); + } + } +} + +/// Style of panic to be used +#[non_exhaustive] +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum PanicStyle { + /// Normal panic + Debug, + /// Human-formatted panic + Human, +} + +impl Default for PanicStyle { + fn default() -> Self { + if cfg!(debug_assertions) { + PanicStyle::Debug + } else { + match ::std::env::var("RUST_BACKTRACE") { + Ok(_) => PanicStyle::Debug, + Err(_) => PanicStyle::Human, + } + } + } +} + +/// Utility function that prints a message to our human users +pub fn print_msg>(file_path: Option

, meta: &Metadata) -> IoResult<()> { + #[cfg(feature = "color")] + { + use std::io::Write as _; + + let stderr = anstream::stderr(); + let mut stderr = stderr.lock(); + + write!(stderr, "{}", anstyle::AnsiColor::Red.render_fg())?; + write_msg(&mut stderr, file_path, meta)?; + write!(stderr, "{}", anstyle::Reset.render())?; + } + + #[cfg(not(feature = "color"))] + { + let stderr = std::io::stderr(); + let mut stderr = stderr.lock(); + + write_msg(&mut stderr, file_path, meta)?; + } + + Ok(()) +} + +fn write_msg>( + buffer: &mut impl std::io::Write, + file_path: Option

, + meta: &Metadata, +) -> IoResult<()> { + let Metadata { + name, + authors, + homepage, + repository, + support, + .. + } = meta; + + writeln!(buffer, "Well, this is embarrassing.\n")?; + writeln!( + buffer, + "{name} had a problem and crashed. To help us diagnose the \ + problem you can send us a crash report.\n" + )?; + writeln!( + buffer, + "We have generated a report file at \"{}\". Submit an \ + issue or email with the subject of \"{} Crash Report\" and include the \ + report as an attachment.\n", + match file_path { + Some(fp) => format!("{}", fp.as_ref().display()), + None => "".to_owned(), + }, + name + )?; + + if let Some(homepage) = homepage { + writeln!(buffer, "- Homepage: {homepage}")?; + } else if let Some(repository) = repository { + writeln!(buffer, "- Repository: {repository}")?; + } + if let Some(authors) = authors { + writeln!(buffer, "- Authors: {authors}")?; + } + if let Some(support) = support { + writeln!(buffer, "\nTo submit the crash report:\n\n{support}")?; + } + writeln!( + buffer, + "\nWe take privacy seriously, and do not perform any \ + automated error collection. In order to improve the software, we rely on \ + people to submit reports.\n" + )?; + writeln!(buffer, "Thank you kindly!")?; + + Ok(()) +} + +/// Utility function which will handle dumping information to disk +#[allow(deprecated)] +pub fn handle_dump(meta: &Metadata, panic_info: &PanicHookInfo<'_>) -> Option { + let mut expl = String::new(); + + let message = match ( + panic_info.payload().downcast_ref::<&str>(), + panic_info.payload().downcast_ref::(), + ) { + (Some(s), _) => Some((*s).to_owned()), + (_, Some(s)) => Some(s.to_owned()), + (None, None) => None, + }; + + let cause = match message { + Some(m) => m, + None => "Unknown".into(), + }; + + match panic_info.location() { + Some(location) => expl.push_str(&format!( + "Panic occurred in file '{}' at line {}\n", + location.file(), + location.line() + )), + None => expl.push_str("Panic location unknown.\n"), + } + + let report = Report::new(&meta.name, &meta.version, Method::Panic, expl, cause); + + if let Ok(f) = report.persist() { + Some(f) + } else { + use std::io::Write as _; + let stderr = std::io::stderr(); + let mut stderr = stderr.lock(); + + let _ = writeln!( + stderr, + "{}", + report + .serialize() + .expect("only doing toml compatible types") + ); + None + } +} From 8126ed2a7c124a61a32f0ce260a59fa45590fccd Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 31 Mar 2026 08:28:32 -0500 Subject: [PATCH 04/10] feat: Add Report::with_panic --- src/panic.rs | 28 +--------------------------- src/report.rs | 38 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/panic.rs b/src/panic.rs index 7dae682..4fd7bd3 100644 --- a/src/panic.rs +++ b/src/panic.rs @@ -3,7 +3,6 @@ use std::panic::PanicHookInfo; use std::path::{Path, PathBuf}; use crate::Metadata; -use crate::report::Method; use crate::report::Report; #[doc(hidden)] @@ -132,32 +131,7 @@ fn write_msg>( /// Utility function which will handle dumping information to disk #[allow(deprecated)] pub fn handle_dump(meta: &Metadata, panic_info: &PanicHookInfo<'_>) -> Option { - let mut expl = String::new(); - - let message = match ( - panic_info.payload().downcast_ref::<&str>(), - panic_info.payload().downcast_ref::(), - ) { - (Some(s), _) => Some((*s).to_owned()), - (_, Some(s)) => Some(s.to_owned()), - (None, None) => None, - }; - - let cause = match message { - Some(m) => m, - None => "Unknown".into(), - }; - - match panic_info.location() { - Some(location) => expl.push_str(&format!( - "Panic occurred in file '{}' at line {}\n", - location.file(), - location.line() - )), - None => expl.push_str("Panic location unknown.\n"), - } - - let report = Report::new(&meta.name, &meta.version, Method::Panic, expl, cause); + let report = Report::with_panic(meta, panic_info); if let Ok(f) = report.persist() { Some(f) diff --git a/src/report.rs b/src/report.rs index c79090e..158fd5d 100644 --- a/src/report.rs +++ b/src/report.rs @@ -3,14 +3,18 @@ //! A `Report` contains the metadata collected about the event //! to construct a helpful error message. -use backtrace::Backtrace; -use serde_derive::Serialize; use std::error::Error; use std::fmt::Write as FmtWrite; use std::mem; +use std::panic::PanicHookInfo; use std::{env, path::Path, path::PathBuf}; + +use backtrace::Backtrace; +use serde_derive::Serialize; use uuid::Uuid; +use crate::Metadata; + /// Method of failure. #[derive(Debug, Serialize, Clone, Copy)] #[non_exhaustive] @@ -60,6 +64,36 @@ impl Report { } } + #[allow(deprecated)] + pub fn with_panic(meta: &Metadata, panic_info: &PanicHookInfo<'_>) -> Self { + let mut expl = String::new(); + + let message = match ( + panic_info.payload().downcast_ref::<&str>(), + panic_info.payload().downcast_ref::(), + ) { + (Some(s), _) => Some((*s).to_owned()), + (_, Some(s)) => Some(s.to_owned()), + (None, None) => None, + }; + + let cause = match message { + Some(m) => m, + None => "Unknown".into(), + }; + + match panic_info.location() { + Some(location) => expl.push_str(&format!( + "Panic occurred in file '{}' at line {}\n", + location.file(), + location.line() + )), + None => expl.push_str("Panic location unknown.\n"), + } + + Self::new(&meta.name, &meta.version, Method::Panic, expl, cause) + } + /// Serialize the `Report` to a TOML string. pub fn serialize(&self) -> Option { toml::to_string_pretty(&self).ok() From 842d9166f02962d16b5c6240a236270d05a4ee0c Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 31 Mar 2026 08:32:35 -0500 Subject: [PATCH 05/10] refactor: Move off handle_dump --- src/panic.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/panic.rs b/src/panic.rs index 4fd7bd3..aefb88a 100644 --- a/src/panic.rs +++ b/src/panic.rs @@ -18,8 +18,22 @@ pub fn setup_panic(meta: impl Fn() -> Metadata) { let meta = meta(); panic::set_hook(Box::new(move |info: &PanicHookInfo<'_>| { - let file_path = handle_dump(&meta, info); - print_msg(file_path, &meta) + let report = Report::with_panic(&meta, info); + let file_path = report.persist().ok(); + if file_path.is_none() { + use std::io::Write as _; + let stderr = std::io::stderr(); + let mut stderr = stderr.lock(); + + let _ = writeln!( + stderr, + "{}", + report + .serialize() + .expect("only doing toml compatible types") + ); + } + print_msg(file_path.as_deref(), &meta) .expect("human-panic: printing error message to console failed"); })); } From 584293036657503b52a0f045fda4e8fe62c345fa Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 31 Mar 2026 08:36:43 -0500 Subject: [PATCH 06/10] test: Verify backtrace behavior --- tests/single-panic/tests/integration.rs | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/single-panic/tests/integration.rs b/tests/single-panic/tests/integration.rs index b1db125..93fc3c2 100644 --- a/tests/single-panic/tests/integration.rs +++ b/tests/single-panic/tests/integration.rs @@ -65,6 +65,50 @@ backtrace = """ root.close().unwrap(); } +#[test] +#[cfg_attr(debug_assertions, ignore)] +fn release_with_backtraces() { + let root = snapbox::dir::DirRoot::mutable_temp().unwrap(); + let root_path = root.path().unwrap(); + + #[cfg(unix)] + let envs = [("TMPDIR", root_path)]; + #[cfg(not(unix))] + let envs: [(&str, &str); 0] = []; + + snapbox::cmd::Command::new(snapbox::cmd::cargo_bin!("single-panic-test")) + .envs(envs) + .env("RUST_BACKTRACE", "1") + .assert() + .stderr_eq(snapbox::str![[r#" + +thread 'main' ([..]) panicked at tests/single-panic/src/main.rs:[..]: +OMG EVERYTHING IS ON FIRE!!! +stack backtrace: +... +note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. + +"#]]) + .code(101); + + #[cfg(unix)] + { + let files = root_path + .read_dir() + .unwrap() + .map(|e| { + let e = e.unwrap(); + let path = e.path(); + let content = std::fs::read_to_string(&path); + (path, content) + }) + .collect::>(); + assert!(files.is_empty(), "{files:?}"); + } + + root.close().unwrap(); +} + #[test] #[cfg_attr(not(debug_assertions), ignore)] fn debug() { From 539d6e0cd196c4fb78f9cf29ed5dc594aedd41eb Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 31 Mar 2026 08:38:35 -0500 Subject: [PATCH 07/10] test: Verify CI behavior --- tests/custom-panic/tests/integration.rs | 2 + tests/single-panic/tests/integration.rs | 71 +++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/tests/custom-panic/tests/integration.rs b/tests/custom-panic/tests/integration.rs index 5051512..d0fb072 100644 --- a/tests/custom-panic/tests/integration.rs +++ b/tests/custom-panic/tests/integration.rs @@ -2,6 +2,7 @@ #[cfg_attr(debug_assertions, ignore)] fn release() { snapbox::cmd::Command::new(snapbox::cmd::cargo_bin!("custom-panic-test")) + .env_remove("CI") .assert() .stderr_eq(snapbox::str![[r#" Well, this is embarrassing. @@ -29,6 +30,7 @@ Thank you kindly! #[cfg_attr(not(debug_assertions), ignore)] fn debug() { snapbox::cmd::Command::new(snapbox::cmd::cargo_bin!("custom-panic-test")) + .env_remove("CI") .assert() .stderr_eq(snapbox::str![[r#" diff --git a/tests/single-panic/tests/integration.rs b/tests/single-panic/tests/integration.rs index 93fc3c2..8dcc60d 100644 --- a/tests/single-panic/tests/integration.rs +++ b/tests/single-panic/tests/integration.rs @@ -11,6 +11,7 @@ fn release() { snapbox::cmd::Command::new(snapbox::cmd::cargo_bin!("single-panic-test")) .envs(envs) + .env_remove("CI") .assert() .stderr_eq(snapbox::str![[r#" Well, this is embarrassing. @@ -78,6 +79,7 @@ fn release_with_backtraces() { snapbox::cmd::Command::new(snapbox::cmd::cargo_bin!("single-panic-test")) .envs(envs) + .env_remove("CI") .env("RUST_BACKTRACE", "1") .assert() .stderr_eq(snapbox::str![[r#" @@ -109,6 +111,74 @@ note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose bac root.close().unwrap(); } +#[test] +#[cfg_attr(debug_assertions, ignore)] +fn release_with_ci() { + let root = snapbox::dir::DirRoot::mutable_temp().unwrap(); + let root_path = root.path().unwrap(); + + #[cfg(unix)] + let envs = [("TMPDIR", root_path)]; + #[cfg(not(unix))] + let envs: [(&str, &str); 0] = []; + + snapbox::cmd::Command::new(snapbox::cmd::cargo_bin!("single-panic-test")) + .envs(envs) + .env("CI", "1") + .assert() + .stderr_eq(snapbox::str![[r#" +Well, this is embarrassing. + +single-panic-test had a problem and crashed. To help us diagnose the problem you can send us a crash report. + +We have generated a report file at "[..].toml". Submit an issue or email with the subject of "single-panic-test Crash Report" and include the report as an attachment. + +- Authors: Human Panic Authors + +We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports. + +Thank you kindly! + +"#]]) + .code(101); + + #[cfg(unix)] + { + let mut files = root_path + .read_dir() + .unwrap() + .map(|e| { + let e = e.unwrap(); + let path = e.path(); + let content = std::fs::read_to_string(&path); + (path, content) + }) + .collect::>(); + assert_eq!(files.len(), 1, "{files:?}"); + let (_, report) = files.pop().unwrap(); + let report = report.unwrap(); + snapbox::assert_data_eq!( + report, + snapbox::str![[r#" +name = "single-panic-test" +operating_system = "[..]" +crate_version = "0.1.0" +explanation = """ +Panic occurred in file 'tests/single-panic/src/main.rs' at line [..] +""" +cause = "OMG EVERYTHING IS ON FIRE!!!" +method = "Panic" +backtrace = """ +... +""" + +"#]] + ); + } + + root.close().unwrap(); +} + #[test] #[cfg_attr(not(debug_assertions), ignore)] fn debug() { @@ -122,6 +192,7 @@ fn debug() { snapbox::cmd::Command::new(snapbox::cmd::cargo_bin!("single-panic-test")) .envs(envs) + .env_remove("CI") .assert() .stderr_eq(snapbox::str![[r#" From 843c4feae03d9bbaf53ff7fa58eb351b4159a8ff Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 31 Mar 2026 08:42:49 -0500 Subject: [PATCH 08/10] fix: Don't write to tmpdir in CI Skip writing to temporary file in CI which is then typically inaccessible from an already failed CI job. In those cases, it's better to dump directly to stderr, since that'll typically be captured by console logging. Inspired by zizmor --- src/panic.rs | 7 ++++- tests/single-panic/tests/integration.rs | 36 ++++++++++--------------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/panic.rs b/src/panic.rs index aefb88a..e87129e 100644 --- a/src/panic.rs +++ b/src/panic.rs @@ -19,7 +19,7 @@ pub fn setup_panic(meta: impl Fn() -> Metadata) { panic::set_hook(Box::new(move |info: &PanicHookInfo<'_>| { let report = Report::with_panic(&meta, info); - let file_path = report.persist().ok(); + let file_path = if is_ci() { None } else { report.persist().ok() }; if file_path.is_none() { use std::io::Write as _; let stderr = std::io::stderr(); @@ -40,6 +40,11 @@ pub fn setup_panic(meta: impl Fn() -> Metadata) { } } +/// Returns whether we are running in a CI environment. +fn is_ci() -> bool { + std::env::var_os("CI").is_some() +} + /// Style of panic to be used #[non_exhaustive] #[derive(Copy, Clone, PartialEq, Eq)] diff --git a/tests/single-panic/tests/integration.rs b/tests/single-panic/tests/integration.rs index 8dcc60d..f6ff0b3 100644 --- a/tests/single-panic/tests/integration.rs +++ b/tests/single-panic/tests/integration.rs @@ -127,11 +127,22 @@ fn release_with_ci() { .env("CI", "1") .assert() .stderr_eq(snapbox::str![[r#" +name = "single-panic-test" +operating_system = "[..]" +crate_version = "0.1.0" +explanation = [..] +Panic occurred in file 'tests/single-panic/src/main.rs' at line 27 +[..] +cause = "OMG EVERYTHING IS ON FIRE!!!" +method = "Panic" +backtrace = [..] +... + Well, this is embarrassing. single-panic-test had a problem and crashed. To help us diagnose the problem you can send us a crash report. -We have generated a report file at "[..].toml". Submit an issue or email with the subject of "single-panic-test Crash Report" and include the report as an attachment. +We have generated a report file at "". Submit an issue or email with the subject of "single-panic-test Crash Report" and include the report as an attachment. - Authors: Human Panic Authors @@ -144,7 +155,7 @@ Thank you kindly! #[cfg(unix)] { - let mut files = root_path + let files = root_path .read_dir() .unwrap() .map(|e| { @@ -154,26 +165,7 @@ Thank you kindly! (path, content) }) .collect::>(); - assert_eq!(files.len(), 1, "{files:?}"); - let (_, report) = files.pop().unwrap(); - let report = report.unwrap(); - snapbox::assert_data_eq!( - report, - snapbox::str![[r#" -name = "single-panic-test" -operating_system = "[..]" -crate_version = "0.1.0" -explanation = """ -Panic occurred in file 'tests/single-panic/src/main.rs' at line [..] -""" -cause = "OMG EVERYTHING IS ON FIRE!!!" -method = "Panic" -backtrace = """ -... -""" - -"#]] - ); + assert_eq!(files.len(), 0, "{files:?}"); } root.close().unwrap(); From 1bc43d81c6dd99092584abfde28f61fa86952f25 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 31 Mar 2026 08:45:37 -0500 Subject: [PATCH 09/10] fix: Skip file write message since output is right there --- src/panic.rs | 17 ++++++++--------- tests/single-panic/tests/integration.rs | 2 -- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/panic.rs b/src/panic.rs index e87129e..e50ce0d 100644 --- a/src/panic.rs +++ b/src/panic.rs @@ -113,17 +113,16 @@ fn write_msg>( "{name} had a problem and crashed. To help us diagnose the \ problem you can send us a crash report.\n" )?; - writeln!( - buffer, - "We have generated a report file at \"{}\". Submit an \ + if let Some(file_path) = file_path { + writeln!( + buffer, + "We have generated a report file at \"{}\". Submit an \ issue or email with the subject of \"{} Crash Report\" and include the \ report as an attachment.\n", - match file_path { - Some(fp) => format!("{}", fp.as_ref().display()), - None => "".to_owned(), - }, - name - )?; + file_path.as_ref().display(), + name + )?; + } if let Some(homepage) = homepage { writeln!(buffer, "- Homepage: {homepage}")?; diff --git a/tests/single-panic/tests/integration.rs b/tests/single-panic/tests/integration.rs index f6ff0b3..b8c58c3 100644 --- a/tests/single-panic/tests/integration.rs +++ b/tests/single-panic/tests/integration.rs @@ -142,8 +142,6 @@ Well, this is embarrassing. single-panic-test had a problem and crashed. To help us diagnose the problem you can send us a crash report. -We have generated a report file at "". Submit an issue or email with the subject of "single-panic-test Crash Report" and include the report as an attachment. - - Authors: Human Panic Authors We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports. From f23f43e33a0fd3191cfcbc24ef55ad55f1df0c1d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 31 Mar 2026 08:46:50 -0500 Subject: [PATCH 10/10] fix: Remove the unprofessioanl whoopsy message --- src/panic.rs | 1 - tests/custom-panic/tests/integration.rs | 4 +--- tests/single-panic/tests/integration.rs | 6 +----- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/panic.rs b/src/panic.rs index e50ce0d..2b135c7 100644 --- a/src/panic.rs +++ b/src/panic.rs @@ -107,7 +107,6 @@ fn write_msg>( .. } = meta; - writeln!(buffer, "Well, this is embarrassing.\n")?; writeln!( buffer, "{name} had a problem and crashed. To help us diagnose the \ diff --git a/tests/custom-panic/tests/integration.rs b/tests/custom-panic/tests/integration.rs index d0fb072..6fed0a9 100644 --- a/tests/custom-panic/tests/integration.rs +++ b/tests/custom-panic/tests/integration.rs @@ -5,11 +5,9 @@ fn release() { .env_remove("CI") .assert() .stderr_eq(snapbox::str![[r#" -Well, this is embarrassing. - custom-panic-test had a problem and crashed. To help us diagnose the problem you can send us a crash report. -We have generated a report file at "[..].toml". Submit an issue or email with the subject of "custom-panic-test Crash Report" and include the report as an attachment. +We have generated a report file at "[..]". Submit an issue or email with the subject of "custom-panic-test Crash Report" and include the report as an attachment. - Homepage: www.mycompany.com - Authors: My Company Support @@ -138,8 +136,6 @@ method = "Panic" backtrace = [..] ... -Well, this is embarrassing. - single-panic-test had a problem and crashed. To help us diagnose the problem you can send us a crash report. - Authors: Human Panic Authors