Compare commits

..

12 commits

Author SHA1 Message Date
Harald Hoyer 28eb28e47d
refactor: simplify downcasting logic with std::mem::transmute (#20)
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.
2025-03-31 14:55:16 +02:00
Harald Hoyer 4c42d37598 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 <harald@hoyer.xyz>
2025-03-31 14:53:52 +02:00
Harald Hoyer 82a4164780
tests: add tests for annotated error display, debug, and chaining (#19)
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.
2025-03-31 14:16:35 +02:00
Harald Hoyer 75b7fdf363 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.
2025-03-31 14:14:43 +02:00
Harald Hoyer d164662537
doc: add usage example for error handling in main function (#18)
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.
2025-03-31 14:05:37 +02:00
Harald Hoyer 5390007cbe 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.
2025-03-31 14:04:15 +02:00
Harald Hoyer 354f7b92ed
tests: add comprehensive unit tests for error handling utilities (#17)
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.
2025-03-31 13:57:25 +02:00
Harald Hoyer 9aa0183d65 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.
2025-03-31 13:54:52 +02:00
Harald Hoyer 46bf63fd32
chore: update Rust installation in coverage workflow (#16)
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.
2025-03-31 13:23:23 +02:00
Harald Hoyer 5cb96eeee3 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.
2025-03-31 13:21:45 +02:00
Harald Hoyer 0cee763264
chore: update coverage workflow to simplify and modernize setup (#15)
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.
2025-03-31 13:15:11 +02:00
Harald Hoyer 74f1dd4314 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 <harald@hoyer.xyz>
2025-03-31 13:14:20 +02:00
2 changed files with 314 additions and 36 deletions

View file

@ -13,31 +13,26 @@ on:
types:
- created
jobs:
test:
name: coverage
coverage:
runs-on: ubuntu-latest
env:
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
target: x86_64-unknown-linux-gnu
toolchain: nightly
components: llvm-tools-preview
- 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

View file

@ -338,11 +338,8 @@ impl<U: 'static + Display + Debug> ErrorDown for Error<U> {
#[inline]
fn downcast_chain_ref<T: 'static + Display + Debug>(&self) -> Option<&Error<T>> {
if self.is_chain::<T>() {
#[allow(clippy::cast_ptr_alignment)]
unsafe {
#[allow(trivial_casts)]
Some(*(self as *const dyn StdError as *const &Error<T>))
}
// Use transmute when we've verified the types match
unsafe { Some(std::mem::transmute::<&Error<U>, &Error<T>>(self)) }
} else {
None
}
@ -351,11 +348,8 @@ impl<U: 'static + Display + Debug> ErrorDown for Error<U> {
#[inline]
fn downcast_chain_mut<T: 'static + Display + Debug>(&mut self) -> Option<&mut Error<T>> {
if self.is_chain::<T>() {
#[allow(clippy::cast_ptr_alignment)]
unsafe {
#[allow(trivial_casts)]
Some(&mut *(self as *mut dyn StdError as *mut &mut Error<T>))
}
// Use transmute when we've verified the types match
unsafe { Some(std::mem::transmute::<&mut Error<U>, &mut Error<T>>(self)) }
} else {
None
}
@ -363,11 +357,8 @@ impl<U: 'static + Display + Debug> ErrorDown for Error<U> {
#[inline]
fn downcast_inner_ref<T: 'static + StdError>(&self) -> Option<&T> {
if self.is_chain::<T>() {
#[allow(clippy::cast_ptr_alignment)]
unsafe {
#[allow(trivial_casts)]
Some(&(*(self as *const dyn StdError as *const &Error<T>)).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<U: 'static + Display + Debug> ErrorDown for Error<U> {
#[inline]
fn downcast_inner_mut<T: 'static + StdError>(&mut self) -> Option<&mut T> {
if self.is_chain::<T>() {
#[allow(clippy::cast_ptr_alignment)]
unsafe {
#[allow(trivial_casts)]
Some(&mut (*(self as *mut dyn StdError as *mut &mut Error<T>)).kind)
}
// Use transmute when we've verified the types match
unsafe { Some(std::mem::transmute::<&mut U, &mut T>(&mut self.kind)) }
} else {
None
}
@ -607,6 +595,7 @@ macro_rules! str_context {
#[derive(Clone)]
pub struct $e(pub String);
impl $e {
#[allow(dead_code)]
pub fn new<S: Into<String>>(s: S) -> Self {
$e(s.into())
}
@ -688,6 +677,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 {
@ -740,3 +735,291 @@ 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::<Level1Error>());
assert!(err.find_chain_cause::<Level2Error>().is_some());
assert!(err.find_chain_cause::<Level3Error>().is_some());
assert!(err.find_chain_cause::<io::Error>().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::<io::Error>());
}
#[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::<io::Error>()
.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::<MappedError>());
assert!(err.find_chain_cause::<io::Error>().is_some());
}
#[test]
fn test_error_downcasting() {
str_context!(OriginalError);
let original = Error::new(OriginalError("test".into()), None, None);
let error: Box<dyn StdError + Send + Sync> = Box::new(original);
// Test downcast_chain_ref
assert!(error.is_chain::<OriginalError>());
assert!(error.downcast_chain_ref::<OriginalError>().is_some());
// Test downcast_inner_ref
let inner = error.downcast_inner_ref::<OriginalError>();
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(_)));
// 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::<io::Error>());
}
// 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::<TestError>());
assert!(!original_error.is_chain::<io::Error>());
// Test downcast_chain_ref
let downcast_ref = original_error.downcast_chain_ref::<TestError>();
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::<io::Error>();
assert!(invalid_downcast.is_none());
// Test downcast_chain_mut
let mut mutable_error = original_error;
let downcast_mut = mutable_error.downcast_chain_mut::<TestError>();
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::<io::Error>();
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::<TestError>();
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::<io::Error>();
assert!(invalid_inner.is_none());
// Test downcast_inner_mut
let inner_mut = error.downcast_inner_mut::<TestError>();
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::<io::Error>();
assert!(invalid_inner_mut.is_none());
}
#[test]
fn test_error_down_for_dyn_error() {
// Create a boxed error
let error: Box<dyn std::error::Error + 'static> = 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::<TestError>());
assert!(!error.is_chain::<io::Error>());
// Test downcast_chain_ref through trait object
let chain_ref = error.downcast_chain_ref::<TestError>();
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::<TestError>();
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<dyn std::error::Error + Send + Sync> = 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::<TestError>());
assert!(error.downcast_chain_ref::<TestError>().is_some());
assert!(error.downcast_inner_ref::<TestError>().is_some());
// Test invalid downcasts
assert!(!error.is_chain::<io::Error>());
assert!(error.downcast_chain_ref::<io::Error>().is_none());
assert!(error.downcast_inner_ref::<io::Error>().is_none());
}
}