diff --git a/wherr/Cargo.toml b/wherr/Cargo.toml index dfe93e3..4f696c8 100644 --- a/wherr/Cargo.toml +++ b/wherr/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/joelonsql/wherr" [features] anyhow = ["dep:anyhow"] +backtrace = [] [dependencies] wherr-macro = "0.1" @@ -24,3 +25,8 @@ required-features = ["anyhow"] name = "anyhow_error_tests" path = "tests/anyhow_error_tests.rs" required-features = ["anyhow"] + +[[test]] +name = "backtrace_tests" +path = "tests/backtrace_tests.rs" +required-features = ["backtrace"] \ No newline at end of file diff --git a/wherr/examples/with_wherr.rs b/wherr/examples/with_wherr.rs index 9f4dca2..aaf3d53 100644 --- a/wherr/examples/with_wherr.rs +++ b/wherr/examples/with_wherr.rs @@ -1,7 +1,13 @@ +#[cfg(not(feature = "backtrace"))] +type MyError = Box; + +#[cfg(feature = "backtrace")] +type MyError = wherr::GenericWherrError; + use wherr::wherr; #[wherr] -fn concat_files(path1: &str, path2: &str) -> Result> { +fn concat_files(path1: &str, path2: &str) -> Result { let mut content1 = std::fs::read_to_string(path1)?; let content2 = std::fs::read_to_string(path2)?; diff --git a/wherr/src/backtrace.rs b/wherr/src/backtrace.rs new file mode 100644 index 0000000..d64806a --- /dev/null +++ b/wherr/src/backtrace.rs @@ -0,0 +1,122 @@ +use std::{fmt, error::Error}; + +pub use wherr_macro::wherr; +pub type GenericError = Box; +pub type GenericWherrError = Box; + +pub trait WherrError: Error { + fn into_inner(&self) -> Option<&GenericError>; + fn take_inner(&mut self) -> Option; + fn locations(&self) -> &[Location]; + fn push_location(&mut self, file: &'static str, line: u32); + fn stack(&self) -> String { + let mut result = String::with_capacity(128); + for loc in self.locations() { + result.push_str("at "); + result.push_str(loc.file); + result.push(':'); + result.push_str(loc.line.to_string().as_str()); + result.push('\n'); + } + result + } +} + +impl WherrError for WherrWithBacktrace { + fn into_inner(&self) -> Option<&GenericError> { + self.inner.as_ref() + } + + fn take_inner(&mut self) -> Option { + self.inner.take() + } + + fn locations(&self) -> &[Location] { + self.locations.as_slice() + } + + fn push_location(&mut self, file: &'static str, line: u32) { + self.locations.push(Location { file, line }); + } +} + +pub struct WherrWithBacktrace { + pub inner: Option, + + pub locations: Vec, +} + +#[derive(Debug)] +pub struct Location { + pub file: &'static str, + pub line: u32, +} + +impl WherrWithBacktrace { + pub fn new(err: GenericError) -> Self { + WherrWithBacktrace { + inner: Some(err), + locations: Vec::default(), + } + } +} + +impl Default for WherrWithBacktrace { + fn default() -> Self { + WherrWithBacktrace { + inner: None, + locations: Vec::default(), + } + } +} + +impl fmt::Display for WherrWithBacktrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for source in &self.inner { + write!(f, "{}", source)?; + } + + for loc in self.locations.iter() { + write!(f, "\nat {}:{}", loc.file, loc.line)?; + } + + Ok(()) + } +} + +impl fmt::Debug for WherrWithBacktrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.inner)?; + for loc in self.locations.iter() { + write!(f, "\nat {}:{}", loc.file, loc.line)?; + } + + Ok(()) + } +} + +impl std::error::Error for WherrWithBacktrace { } + +pub fn wherrapper( + result: Result, + file: &'static str, + line: u32, +) -> Result +where + E: Into + 'static, +{ + match result { + Ok(val) => Ok(val), + Err(err) => { + let mut wherr_error: GenericWherrError = err.into(); + wherr_error.push_location(file, line); + Err(wherr_error) + } + } +} + +impl From for GenericWherrError { + fn from(error: E) -> Self { + Box::new(WherrWithBacktrace::new(Box::new(error))) + } +} diff --git a/wherr/src/lib.rs b/wherr/src/lib.rs index 5342c91..2c68a09 100644 --- a/wherr/src/lib.rs +++ b/wherr/src/lib.rs @@ -4,6 +4,12 @@ //! //! The `wherr` attribute macro, defined in the `wherr_macro` crate, is re-exported here for ease of use. +#[cfg(feature = "backtrace")] +mod backtrace; + +#[cfg(feature = "backtrace")] +pub use self::backtrace::*; + use std::fmt; // Re-export the procedural macro from the `wherr_macro` crate. @@ -75,6 +81,7 @@ impl std::error::Error for Wherr {} /// # Returns /// If the original error is already of type `Wherr`, it is returned as is. /// Otherwise, the original error is wrapped inside a `Wherr` and returned. +#[cfg(not(feature = "backtrace"))] pub fn wherrapper( result: Result, file: &'static str, diff --git a/wherr/tests/anyhow_error_tests.rs b/wherr/tests/anyhow_error_tests.rs index 97ace30..4d92391 100644 --- a/wherr/tests/anyhow_error_tests.rs +++ b/wherr/tests/anyhow_error_tests.rs @@ -68,7 +68,7 @@ fn test_wherr_macro() { Ok(_) => panic!("Expected an error"), Err(err) => { let wherr = err.downcast::().expect("Expected a Wherr error"); - assert_eq!(wherr.file, "wherr/tests/anyhow_error_tests.rs"); + assert_eq!(wherr.file.replace('\\', "/"), "wherr/tests/anyhow_error_tests.rs"); assert_eq!(wherr.line, 46); assert_eq!(wherr.inner.to_string(), "invalid digit found in string"); } diff --git a/wherr/tests/backtrace_tests.rs b/wherr/tests/backtrace_tests.rs new file mode 100644 index 0000000..cd3ec05 --- /dev/null +++ b/wherr/tests/backtrace_tests.rs @@ -0,0 +1,31 @@ +#![cfg(feature = "backtrace")] + +use wherr::wherr; +use wherr::GenericWherrError; + +const ERROR_MESSAGE: &str = "Test error"; + +#[test] +fn test_backtrace() { + // Act + let result = f1(); + + // Assert + assert!(result.is_err()); + let wherr = result.unwrap_err(); + assert_eq!(wherr.locations().len(), 2); + let error_message = wherr.to_string(); + assert_eq!(error_message.replace("\\", "/"), format!("{ERROR_MESSAGE}\nat wherr/tests/backtrace_tests.rs:29\nat wherr/tests/backtrace_tests.rs:23")); +} + +#[wherr] +fn f1() -> Result<(), GenericWherrError> { + f2()?; + Ok(()) +} + +#[wherr] +fn f2() -> Result<(), GenericWherrError> { + Err(std::io::Error::new(std::io::ErrorKind::Other, ERROR_MESSAGE))?; + Ok(()) +} \ No newline at end of file diff --git a/wherr/tests/box_error_tests.rs b/wherr/tests/box_error_tests.rs index c2f3100..d3c32d7 100644 --- a/wherr/tests/box_error_tests.rs +++ b/wherr/tests/box_error_tests.rs @@ -1,4 +1,4 @@ -#![cfg(not(feature = "anyhow"))] +#![cfg(not(any(feature = "anyhow", feature = "backtrace")))] use wherr::{wherr, wherrapper, Wherr}; @@ -69,7 +69,7 @@ fn test_wherr_macro() { Ok(_) => panic!("Expected an error"), Err(err) => { let wherr = err.downcast::().expect("Expected a Wherr error"); - assert_eq!(wherr.file, "wherr/tests/box_error_tests.rs"); + assert_eq!(wherr.file.replace("\\", "/"), "wherr/tests/box_error_tests.rs"); assert_eq!(wherr.line, 47); assert_eq!(wherr.inner.to_string(), "invalid digit found in string"); }