From 74f1dd43148be7d29b4e4d7d29cc40298d5ba9c5 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Mon, 31 Mar 2025 13:07:27 +0200 Subject: [PATCH 1/6] chore: update coverage workflow to simplify and modernize setup Revised the GitHub Actions workflow for code coverage by updating dependencies, using modern, maintained actions, and improving configuration clarity. Streamlined Rust installation and replaced manual steps with dedicated actions for better reliability. Adjusted Codecov settings for stricter error handling. Signed-off-by: Harald Hoyer --- .github/workflows/coverage.yml | 35 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 21025a8..6492f1a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -13,31 +13,24 @@ on: types: - created + jobs: - test: - name: coverage + coverage: runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always steps: - - uses: actions/checkout@v1 - - uses: dtolnay/rust-toolchain@master - with: - target: x86_64-unknown-linux-gnu - toolchain: nightly - components: llvm-tools-preview - + - uses: actions/checkout@v4 + - name: Install Rust + run: | + rustup update nightly - name: Install cargo-llvm-cov - run: > - curl -LsSf 'https://github.com/taiki-e/cargo-llvm-cov/releases/download/v0.5.23/cargo-llvm-cov-x86_64-unknown-linux-musl.tar.gz' - | tar xzf - - && mv cargo-llvm-cov $HOME/.cargo/bin - - - name: Run cargo-llvm-cov - run: cargo llvm-cov --doctests --all --all-features --lcov --output-path lcov.info - + uses: taiki-e/install-action@cargo-llvm-cov + - name: Generate code coverage + run: cargo +nightly llvm-cov --all-features --workspace --codecov --doctests --output-path codecov.json - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: - directory: ./ - fail_ci_if_error: false - files: ./lcov.info - verbose: true + token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos + files: codecov.json + fail_ci_if_error: true From 5cb96eeee39b4f9efed24ddda81e6dbdf3f6e03a Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Mon, 31 Mar 2025 13:21:34 +0200 Subject: [PATCH 2/6] chore: update Rust installation in coverage workflow Replaced manual Rust installation with dtolnay/rust-toolchain action for better maintainability and clarity. Added necessary components like llvm-tools-preview to support code coverage generation. These changes simplify the workflow setup. --- .github/workflows/coverage.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 6492f1a..7dff904 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,9 +21,11 @@ jobs: CARGO_TERM_COLOR: always steps: - uses: actions/checkout@v4 - - name: Install Rust - run: | - rustup update nightly + - uses: dtolnay/rust-toolchain@master + with: + target: x86_64-unknown-linux-gnu + toolchain: nightly + components: llvm-tools-preview - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage From 9aa0183d654c8394738c2f8dc428df6c0617f38f Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Mon, 31 Mar 2025 13:54:45 +0200 Subject: [PATCH 3/6] tests: add comprehensive unit tests for error handling utilities This commit introduces a series of unit tests to validate various error handling functionalities, including error chaining, root cause extraction, display/debug formatting, annotation, context mapping, downcasting, and custom error kinds. These tests improve code reliability and ensure expected behavior across different error scenarios. --- src/lib.rs | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 372cef2..51eaf7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -607,6 +607,7 @@ macro_rules! str_context { #[derive(Clone)] pub struct $e(pub String); impl $e { + #[allow(dead_code)] pub fn new>(s: S) -> Self { $e(s.into()) } @@ -740,3 +741,137 @@ macro_rules! err_kind { } }; } + +#[cfg(test)] +mod tests { + use super::Context as _; + use super::*; + use std::io; + + #[test] + fn test_error_chain_with_multiple_causes() { + // Create a chain of errors + let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found"); + + str_context!(Level3Error); + str_context!(Level2Error); + str_context!(Level1Error); + + let err = Result::<(), _>::Err(io_error.into()) + .context(Level3Error("level 3".into())) + .context(Level2Error("level 2".into())) + .context(Level1Error("level 1".into())) + .unwrap_err(); + + // Test the error chain + assert!(err.is_chain::()); + assert!(err.find_chain_cause::().is_some()); + assert!(err.find_chain_cause::().is_some()); + assert!(err.find_chain_cause::().is_some()); + } + + #[test] + fn test_error_root_cause() { + let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found"); + + str_context!(WrapperError); + let err = Result::<(), _>::Err(io_error.into()) + .context(WrapperError("wrapper".into())) + .unwrap_err(); + + let root = err.root_cause().unwrap(); + assert!(root.is_chain::()); + } + + #[test] + fn test_error_display_and_debug() { + str_context!(CustomError); + let err = Error::new( + CustomError("test error".into()), + None, + Some("src/lib.rs:100".into()), + ); + + // Test Display formatting + assert_eq!(format!("{}", err), "test error"); + + // Test alternate Display formatting + assert_eq!(format!("{:#}", err), "test error"); + + // Test Debug formatting + let debug_output = format!("{:?}", err); + assert!(debug_output.contains("test error")); + assert!(debug_output.contains("src/lib.rs:100")); + } + + #[test] + fn test_error_annotation() { + let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found"); + let err = Result::<(), _>::Err(io_error.into()) + .annotate() + .unwrap_err(); + + assert!(err.source().is_some()); + err.source() + .unwrap() + .downcast_inner_ref::() + .unwrap(); + } + + #[test] + fn test_map_context() { + let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found"); + + str_context!(MappedError); + let err = Result::<(), _>::Err(io_error.into()) + .map_context(|e| MappedError(format!("Mapped: {}", e))) + .unwrap_err(); + + assert!(err.is_chain::()); + assert!(err.find_chain_cause::().is_some()); + } + + #[test] + fn test_error_downcasting() { + str_context!(OriginalError); + let original = Error::new(OriginalError("test".into()), None, None); + + let error: Box = Box::new(original); + + // Test downcast_chain_ref + assert!(error.is_chain::()); + assert!(error.downcast_chain_ref::().is_some()); + + // Test downcast_inner_ref + let inner = error.downcast_inner_ref::(); + assert!(inner.is_some()); + } + + #[derive(Debug, Clone)] + enum TestErrorKind { + Basic(String), + Complex { message: String }, + } + + impl Display for TestErrorKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TestErrorKind::Basic(msg) => write!(f, "Basic error: {}", msg), + TestErrorKind::Complex { message } => write!(f, "Complex error: {}", message), + } + } + } + + #[test] + fn test_err_kind_macro() { + err_kind!(TestError, TestErrorKind); + + let err = TestError::from(TestErrorKind::Basic("test".into())); + assert!(matches!(err.kind(), TestErrorKind::Basic(_))); + + let complex_err = TestError::from(TestErrorKind::Complex { + message: "test".into(), + }); + assert!(matches!(complex_err.kind(), TestErrorKind::Complex { .. })); + } +} From 5390007cbeb85887e56d9a9c517ea1f0f0a51e85 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Mon, 31 Mar 2025 14:04:01 +0200 Subject: [PATCH 4/6] doc: add usage example for error handling in main function This commit adds a usage example demonstrating how to handle errors returned by the `func1` function in the main function. The example provides clarity on practical error handling and makes the documentation more comprehensive for users. --- src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 51eaf7e..450eab3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -689,6 +689,12 @@ macro_rules! str_context { /// do_some_io(filename).map_context(|e| ErrorKind::from(e))?; /// Ok(()) /// } +/// +/// # fn main() { +/// # if let Err(e) = func1() { +/// # eprintln!("Error:\n{:?}", e); +/// # } +/// # } /// ``` #[macro_export] macro_rules! err_kind { From 75b7fdf3630418d29c4f70c6b8e6b5dc832346fe Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Mon, 31 Mar 2025 14:14:32 +0200 Subject: [PATCH 5/6] tests: add tests for annotated error display, debug, and chaining Introduced tests to verify the behavior of `AnnotatedError` in display, debug, and error chaining scenarios. Ensured proper formatting for standalone errors and preservation of the error chain for wrapped errors. --- src/lib.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 450eab3..982ef83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -874,10 +874,43 @@ mod tests { let err = TestError::from(TestErrorKind::Basic("test".into())); assert!(matches!(err.kind(), TestErrorKind::Basic(_))); + // The annotated error should display "(passed error)" even in a chain + assert_eq!(format!("{}", err), "Basic error: test"); + assert_eq!(format!("{:?}", err), "Basic(\"test\")"); let complex_err = TestError::from(TestErrorKind::Complex { message: "test".into(), }); assert!(matches!(complex_err.kind(), TestErrorKind::Complex { .. })); + // The annotated error should display "(passed error)" even in a chain + assert_eq!(format!("{}", complex_err), "Complex error: test"); + assert_eq!( + format!("{:?}", complex_err), + "Complex { message: \"test\" }" + ); + } + #[test] + fn test_annotated_error_display_and_debug() { + let annotated = AnnotatedError(()); + + // Test Display formatting + assert_eq!(format!("{}", annotated), "(passed error)"); + + // Test Debug formatting + assert_eq!(format!("{:?}", annotated), "(passed error)"); + + // Test with error chain + let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found"); + let err = Result::<(), _>::Err(io_error.into()) + .annotate() + .unwrap_err(); + + // The annotated error should display "(passed error)" even in a chain + assert_eq!(format!("{}", err), "(passed error)"); + assert!(format!("{:?}", err).contains("(passed error)")); + + // Verify the error chain is preserved + assert!(err.source().is_some()); + assert!(err.source().unwrap().is_chain::()); } } From 4c42d3759894492b90af063ad82ceacf0925ee2c Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Mon, 31 Mar 2025 14:45:22 +0200 Subject: [PATCH 6/6] refactor: simplify downcasting logic with `std::mem::transmute` Simplified the downcasting implementations by replacing pointer casting logic with `std::mem::transmute`, ensuring type safety after matching. Added tests to validate various downcasting behaviors for both owned and trait-object error scenarios, improving overall reliability and test coverage. Signed-off-by: Harald Hoyer --- src/lib.rs | 149 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 129 insertions(+), 20 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 982ef83..623523d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -338,11 +338,8 @@ impl ErrorDown for Error { #[inline] fn downcast_chain_ref(&self) -> Option<&Error> { if self.is_chain::() { - #[allow(clippy::cast_ptr_alignment)] - unsafe { - #[allow(trivial_casts)] - Some(*(self as *const dyn StdError as *const &Error)) - } + // Use transmute when we've verified the types match + unsafe { Some(std::mem::transmute::<&Error, &Error>(self)) } } else { None } @@ -351,11 +348,8 @@ impl ErrorDown for Error { #[inline] fn downcast_chain_mut(&mut self) -> Option<&mut Error> { if self.is_chain::() { - #[allow(clippy::cast_ptr_alignment)] - unsafe { - #[allow(trivial_casts)] - Some(&mut *(self as *mut dyn StdError as *mut &mut Error)) - } + // Use transmute when we've verified the types match + unsafe { Some(std::mem::transmute::<&mut Error, &mut Error>(self)) } } else { None } @@ -363,11 +357,8 @@ impl ErrorDown for Error { #[inline] fn downcast_inner_ref(&self) -> Option<&T> { if self.is_chain::() { - #[allow(clippy::cast_ptr_alignment)] - unsafe { - #[allow(trivial_casts)] - Some(&(*(self as *const dyn StdError as *const &Error)).kind) - } + // Use transmute when we've verified the types match + unsafe { Some(std::mem::transmute::<&U, &T>(&self.kind)) } } else { None } @@ -376,11 +367,8 @@ impl ErrorDown for Error { #[inline] fn downcast_inner_mut(&mut self) -> Option<&mut T> { if self.is_chain::() { - #[allow(clippy::cast_ptr_alignment)] - unsafe { - #[allow(trivial_casts)] - Some(&mut (*(self as *mut dyn StdError as *mut &mut Error)).kind) - } + // Use transmute when we've verified the types match + unsafe { Some(std::mem::transmute::<&mut U, &mut T>(&mut self.kind)) } } else { None } @@ -913,4 +901,125 @@ mod tests { assert!(err.source().is_some()); assert!(err.source().unwrap().is_chain::()); } + + // Helper error types for testing + #[derive(Debug)] + struct TestError(String); + + impl std::fmt::Display for TestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } + } + + impl std::error::Error for TestError {} + + #[test] + fn test_downcast_chain_operations() { + // Create a test error chain + let original_error = Error::new( + TestError("test message".to_string()), + None, + Some("test location".to_string()), + ); + + // Test is_chain + assert!(original_error.is_chain::()); + assert!(!original_error.is_chain::()); + + // Test downcast_chain_ref + let downcast_ref = original_error.downcast_chain_ref::(); + assert!(downcast_ref.is_some()); + let downcast_kind = downcast_ref.unwrap().kind(); + assert_eq!(format!("{}", downcast_kind), "test message"); + assert_eq!( + format!("{:?}", downcast_kind), + "TestError(\"test message\")" + ); + + // Test invalid downcast_chain_ref + let invalid_downcast = original_error.downcast_chain_ref::(); + assert!(invalid_downcast.is_none()); + + // Test downcast_chain_mut + let mut mutable_error = original_error; + let downcast_mut = mutable_error.downcast_chain_mut::(); + assert!(downcast_mut.is_some()); + assert_eq!(downcast_mut.unwrap().kind().0, "test message"); + + // Test invalid downcast_chain_mut + let invalid_downcast_mut = mutable_error.downcast_chain_mut::(); + assert!(invalid_downcast_mut.is_none()); + } + + #[test] + fn test_downcast_inner_operations() { + // Create a test error + let mut error = Error::new( + TestError("inner test".to_string()), + None, + Some("test location".to_string()), + ); + + // Test downcast_inner_ref + let inner_ref = error.downcast_inner_ref::(); + assert!(inner_ref.is_some()); + assert_eq!(inner_ref.unwrap().0, "inner test"); + // Test invalid downcast_inner_ref + let invalid_inner = error.downcast_inner_ref::(); + assert!(invalid_inner.is_none()); + + // Test downcast_inner_mut + let inner_mut = error.downcast_inner_mut::(); + assert!(inner_mut.is_some()); + assert_eq!(inner_mut.unwrap().0, "inner test"); + + // Test invalid downcast_inner_mut + let invalid_inner_mut = error.downcast_inner_mut::(); + assert!(invalid_inner_mut.is_none()); + } + + #[test] + fn test_error_down_for_dyn_error() { + // Create a boxed error + let error: Box = Box::new(Error::new( + TestError("dyn test".to_string()), + None, + Some("test location".to_string()), + )); + + // Test is_chain through trait object + assert!(error.is_chain::()); + assert!(!error.is_chain::()); + + // Test downcast_chain_ref through trait object + let chain_ref = error.downcast_chain_ref::(); + assert!(chain_ref.is_some()); + assert_eq!(chain_ref.unwrap().kind().0, "dyn test"); + + // Test downcast_inner_ref through trait object + let inner_ref = error.downcast_inner_ref::(); + assert!(inner_ref.is_some()); + assert_eq!(inner_ref.unwrap().0, "dyn test"); + } + + #[test] + fn test_error_down_with_sync_send() { + // Create a boxed error with Send + Sync + let error: Box = Box::new(Error::new( + TestError("sync test".to_string()), + None, + Some("test location".to_string()), + )); + + // Test operations on Send + Sync error + assert!(error.is_chain::()); + assert!(error.downcast_chain_ref::().is_some()); + assert!(error.downcast_inner_ref::().is_some()); + + // Test invalid downcasts + assert!(!error.is_chain::()); + assert!(error.downcast_chain_ref::().is_none()); + assert!(error.downcast_inner_ref::().is_none()); + } }