diff --git a/src/lib.rs b/src/lib.rs index 0b518ce..1b641dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,75 +48,15 @@ #[cfg(doctest)] pub struct ReadmeDoctests; -pub mod report; -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, - } - } +mod metadata; +mod panic; - /// 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 - } -} +pub mod report; +pub use metadata::Metadata; +pub use panic::PanicStyle; +pub use panic::handle_dump; +pub use panic::print_msg; +pub use panic::setup_panic; /// Initialize [`Metadata`] #[macro_export] @@ -161,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 -#[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(); - - write!(stderr, "{}", anstyle::AnsiColor::Red.render_fg())?; - write_msg(&mut stderr, file_path, meta)?; - write!(stderr, "{}", anstyle::Reset.render())?; - - Ok(()) -} - -#[cfg(not(feature = "color"))] -pub fn print_msg>(file_path: Option

, meta: &Metadata) -> IoResult<()> { - 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/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 + } +} diff --git a/src/panic.rs b/src/panic.rs new file mode 100644 index 0000000..2b135c7 --- /dev/null +++ b/src/panic.rs @@ -0,0 +1,169 @@ +use std::io::Result as IoResult; +use std::panic::PanicHookInfo; +use std::path::{Path, PathBuf}; + +use crate::Metadata; +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 report = Report::with_panic(&meta, info); + 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(); + 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"); + })); + } + } +} + +/// 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)] +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, + "{name} had a problem and crashed. To help us diagnose the \ + problem you can send us a crash report.\n" + )?; + 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", + file_path.as_ref().display(), + 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 report = Report::with_panic(meta, panic_info); + + 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/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() diff --git a/tests/custom-panic/tests/integration.rs b/tests/custom-panic/tests/integration.rs index 5051512..6fed0a9 100644 --- a/tests/custom-panic/tests/integration.rs +++ b/tests/custom-panic/tests/integration.rs @@ -2,13 +2,12 @@ #[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. - 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 @@ -65,6 +64,107 @@ 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_remove("CI") + .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(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#" +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 = [..] +... + +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 + +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 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(), 0, "{files:?}"); + } + + root.close().unwrap(); +} + #[test] #[cfg_attr(not(debug_assertions), ignore)] fn debug() { @@ -78,6 +178,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#"