use gix_error::{ Class, CorruptionError, Error, ErrorExt, ResourceExhaustionError, ResourceExhaustionKind, RetryableError, ValidationError, can_retry, can_retry_lenient, message, }; #[test] fn classifications_preserve_order_duplicates_and_sources() { fn allocation_failure() -> std::collections::TryReserveError { Vec::::new() .try_reserve(usize::MAX) .expect_err("the maximum capacity cannot be reserved") } let err = Error::from( RetryableError::new(allocation_failure()).and_raise(CorruptionError::new("corrupt input caused allocation")), ); let classifications = err.classify().collect::>(); assert_eq!( classifications .iter() .map(gix_error::Classification::class) .collect::>(), [ Class::Corruption, Class::Retryable, Class::ResourceExhaustion(ResourceExhaustionKind::AllocationFailure), ], "classification follows the error graph without merging independent meanings" ); assert!(classifications[0].error().is::()); assert!(classifications[1].error().is::()); assert!(classifications[2].error().is::()); let duplicate = Error::from( ValidationError::new("first") .raise() .chain(ValidationError::new("second")), ); assert_eq!( duplicate.classify().map(|item| item.class()).collect::>(), [Class::Validation, Class::Validation], "a classification is emitted for each matching error node" ); } #[test] fn io_errors_are_normalized_without_losing_their_origin() { let cases = [ (std::io::ErrorKind::NotFound, Class::NotFound), ( std::io::ErrorKind::OutOfMemory, Class::ResourceExhaustion(ResourceExhaustionKind::AllocationFailure), ), ( std::io::ErrorKind::PermissionDenied, Class::Io(std::io::ErrorKind::PermissionDenied), ), ]; for (io_kind, expected_class) in cases { let err = Error::from_error(std::io::Error::from(io_kind)); let classification = err.classify().next().expect("all I/O errors are classified"); assert_eq!(classification.class(), expected_class); assert_eq!(classification.io_kind(), Some(io_kind)); assert!(classification.error().is::()); } } #[test] fn allocation_limits_are_resources_only() { let err = Error::from_error(ResourceExhaustionError::new( ResourceExhaustionKind::AllocationLimit, "configured allocation limit exceeded", )); assert_eq!( err.classify().map(|item| item.class()).collect::>(), [Class::ResourceExhaustion(ResourceExhaustionKind::AllocationLimit)] ); assert!(!err.is_corrupted()); assert!(!err.can_retry()); } #[test] fn global_retry_policy_is_conservative() { for kind in [std::io::ErrorKind::Interrupted, std::io::ErrorKind::TimedOut] { assert!( can_retry(&std::io::Error::from(kind)), "{kind:?} can be retried globally" ); } for kind in [ std::io::ErrorKind::OutOfMemory, std::io::ErrorKind::ConnectionReset, std::io::ErrorKind::UnexpectedEof, ] { assert!( !can_retry(&std::io::Error::from(kind)), "{kind:?} needs explicit retry policy" ); } assert!(can_retry(&RetryableError::new(message("try again")))); } #[test] fn lenient_retry_policy_preserves_the_previous_io_kinds() { for kind in [ std::io::ErrorKind::Interrupted, std::io::ErrorKind::UnexpectedEof, std::io::ErrorKind::OutOfMemory, std::io::ErrorKind::TimedOut, std::io::ErrorKind::BrokenPipe, std::io::ErrorKind::AddrInUse, std::io::ErrorKind::ConnectionAborted, std::io::ErrorKind::ConnectionReset, std::io::ErrorKind::ConnectionRefused, ] { assert!( can_retry_lenient(&std::io::Error::from(kind)), "{kind:?} is retryable under the lenient policy" ); } assert!( !Error::from_error(std::io::Error::from(std::io::ErrorKind::PermissionDenied)).can_retry_lenient(), "the lenient policy still rejects permanent I/O errors" ); assert!( Error::from_error(RetryableError::new(message("try again"))).can_retry_lenient(), "the lenient policy includes explicitly retryable errors" ); let allocation_failure = Vec::::new() .try_reserve(usize::MAX) .expect_err("the maximum capacity cannot be reserved"); assert!( !Error::from_error(allocation_failure).can_retry_lenient(), "only I/O OutOfMemory errors are covered by the historical policy" ); } #[test] fn unknown_errors_are_omitted() { assert_eq!(Error::from_error(message("unknown")).classify().count(), 0); }