use std::error::Error as StdError;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, IbuError>;
#[derive(Error, Debug)]
pub enum IbuError {
#[error("I/O error")]
Io(#[from] std::io::Error),
#[error("Niffler error")]
Niffler(#[from] niffler::Error),
#[error("Invalid magic number, expected ({expected:#x}), found ({actual:#x})")]
InvalidMagicNumber { expected: u32, actual: u32 },
#[error("Truncated record at position {pos}")]
TruncatedRecord { pos: usize },
#[error("Invalid version found, expected ({expected}), found ({actual})")]
InvalidVersion { expected: u32, actual: u32 },
#[error("Invalid barcode length: {0} (must be 1-32)")]
InvalidBarcodeLength(u32),
#[error("Invalid UMI length: {0} (must be 1-32)")]
InvalidUmiLength(u32),
#[error("Invalid map size - not a multiple of record size")]
InvalidMapSize,
#[error("Invalid index ({idx}) - Must be less than {max}")]
InvalidIndex { idx: usize, max: usize },
#[error("Processing error: {0}")]
Process(Box<dyn StdError + Send + Sync>),
}
pub trait IntoIbuError {
fn into_ibu_error(self) -> IbuError;
}
impl<E> IntoIbuError for E
where
E: std::error::Error + Send + Sync + 'static,
{
fn into_ibu_error(self) -> IbuError {
IbuError::Process(self.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fmt;
#[derive(Debug)]
struct CustomError(String);
impl fmt::Display for CustomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Custom error: {}", self.0)
}
}
impl std::error::Error for CustomError {}
#[test]
fn test_error_display_messages() {
let err = IbuError::InvalidMagicNumber {
expected: 0x21554249,
actual: 0x12345678,
};
let display = format!("{}", err);
assert!(display.contains("0x21554249"));
assert!(display.contains("0x12345678"));
let err = IbuError::InvalidVersion {
expected: 2,
actual: 1,
};
let display = format!("{}", err);
assert!(display.contains("expected (2)"));
assert!(display.contains("found (1)"));
let err = IbuError::TruncatedRecord { pos: 1024 };
let display = format!("{}", err);
assert!(display.contains("1024"));
let err = IbuError::InvalidBarcodeLength(33);
let display = format!("{}", err);
assert!(display.contains("33"));
assert!(display.contains("1-32"));
let err = IbuError::InvalidUmiLength(0);
let display = format!("{}", err);
assert!(display.contains("0"));
assert!(display.contains("1-32"));
let err = IbuError::InvalidMapSize;
let display = format!("{}", err);
assert!(display.contains("not a multiple"));
let err = IbuError::InvalidIndex { idx: 100, max: 50 };
let display = format!("{}", err);
assert!(display.contains("100"));
assert!(display.contains("50"));
let custom_err = CustomError("test error".to_string());
let err = IbuError::Process(custom_err.into());
let display = format!("{}", err);
assert!(display.contains("Processing error"));
}
#[test]
fn test_error_debug() {
let err = IbuError::InvalidMagicNumber {
expected: 0x21554249,
actual: 0x12345678,
};
let debug = format!("{:?}", err);
assert!(debug.contains("InvalidMagicNumber"));
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
let ibu_err: IbuError = io_err.into();
match ibu_err {
IbuError::Io(inner) => {
assert_eq!(inner.kind(), std::io::ErrorKind::NotFound);
}
_ => panic!("Expected Io variant"),
}
}
#[cfg(feature = "niffler")]
#[test]
fn test_niffler_error_conversion() {
use std::any::TypeId;
assert_eq!(
TypeId::of::<niffler::Error>(),
TypeId::of::<niffler::Error>()
);
}
#[test]
fn test_into_ibu_error_trait() {
let custom_err = CustomError("test".to_string());
let ibu_err = custom_err.into_ibu_error();
match ibu_err {
IbuError::Process(boxed) => {
let display = format!("{}", boxed);
assert!(display.contains("Custom error: test"));
}
_ => panic!("Expected Process variant"),
}
}
#[test]
fn test_result_type_alias() {
fn test_function() -> Result<i32> {
Ok(42)
}
fn failing_function() -> Result<i32> {
Err(IbuError::InvalidMapSize)
}
assert_eq!(test_function().unwrap(), 42);
assert!(failing_function().is_err());
}
#[test]
fn test_error_source_chain() {
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
let ibu_err = IbuError::Io(io_err);
let source = ibu_err.source();
assert!(source.is_some());
if let Some(source) = source {
let io_source = source.downcast_ref::<std::io::Error>();
assert!(io_source.is_some());
assert_eq!(
io_source.unwrap().kind(),
std::io::ErrorKind::PermissionDenied
);
}
}
#[test]
fn test_error_send_sync() {
fn is_send<T: Send>() {}
fn is_sync<T: Sync>() {}
is_send::<IbuError>();
is_sync::<IbuError>();
}
#[test]
fn test_custom_error_in_process() {
#[derive(Debug)]
struct ThreadError {
thread_id: usize,
message: String,
}
impl fmt::Display for ThreadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Thread {} error: {}", self.thread_id, self.message)
}
}
impl std::error::Error for ThreadError {}
let thread_err = ThreadError {
thread_id: 3,
message: "Processing failed".to_string(),
};
let ibu_err = thread_err.into_ibu_error();
let display = format!("{}", ibu_err);
assert!(display.contains("Thread 3 error"));
assert!(display.contains("Processing failed"));
}
}