use std::error::Error;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum ExitCode {
Success = 0,
TestFailure = 1,
UserError = 2,
SystemError = 3,
}
impl ExitCode {
pub fn code(self) -> u8 {
self as u8
}
}
impl fmt::Display for ExitCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.code())
}
}
#[derive(Debug, thiserror::Error)]
pub enum CoreError {
#[error("{0}")]
User(String),
#[error("{0}")]
TestFailure(String),
#[error("{message}")]
System {
message: String,
#[source]
source: Option<Box<dyn Error + Send + Sync>>,
},
}
impl CoreError {
pub fn user(message: impl Into<String>) -> Self {
Self::User(message.into())
}
pub fn system(message: impl Into<String>) -> Self {
Self::System {
message: message.into(),
source: None,
}
}
pub fn system_with(
message: impl Into<String>,
source: impl Error + Send + Sync + 'static,
) -> Self {
Self::System {
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn exit_code(&self) -> ExitCode {
match self {
Self::User(_) => ExitCode::UserError,
Self::TestFailure(_) => ExitCode::TestFailure,
Self::System { .. } => ExitCode::SystemError,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EngineErrorClass {
Infra,
AssertFailed,
UserInput,
Setup,
}
#[derive(Debug, thiserror::Error)]
#[error("{message}")]
pub struct EngineError {
pub class: EngineErrorClass,
pub message: String,
#[source]
pub source: Option<Box<dyn Error + Send + Sync>>,
}
impl EngineError {
pub fn infra(message: impl Into<String>) -> Self {
Self {
class: EngineErrorClass::Infra,
message: message.into(),
source: None,
}
}
pub fn assert_failed(message: impl Into<String>) -> Self {
Self {
class: EngineErrorClass::AssertFailed,
message: message.into(),
source: None,
}
}
pub fn user_input(message: impl Into<String>) -> Self {
Self {
class: EngineErrorClass::UserInput,
message: message.into(),
source: None,
}
}
pub fn setup(message: impl Into<String>) -> Self {
Self {
class: EngineErrorClass::Setup,
message: message.into(),
source: None,
}
}
#[must_use]
pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
self.source = Some(Box::new(source));
self
}
}
impl From<EngineError> for CoreError {
fn from(err: EngineError) -> Self {
match err.class {
EngineErrorClass::AssertFailed => Self::TestFailure(err.message),
EngineErrorClass::UserInput => Self::User(err.message),
EngineErrorClass::Infra | EngineErrorClass::Setup => Self::System {
message: err.message,
source: err.source,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn core_error_exit_codes_are_stable() {
assert_eq!(CoreError::user("bad flag").exit_code().code(), 2);
assert_eq!(
CoreError::TestFailure("assert failed".to_owned())
.exit_code()
.code(),
1
);
assert_eq!(CoreError::system("no network").exit_code().code(), 3);
assert_eq!(ExitCode::Success.code(), 0);
}
#[test]
fn engine_errors_fold_into_the_core_taxonomy() {
let assert_failed: CoreError = EngineError::assert_failed("status != 200").into();
assert_eq!(assert_failed.exit_code(), ExitCode::TestFailure);
let infra: CoreError = EngineError::infra("connection refused").into();
assert_eq!(infra.exit_code(), ExitCode::SystemError);
let setup: CoreError = EngineError::setup("libcurl missing").into();
assert_eq!(setup.exit_code(), ExitCode::SystemError);
}
#[test]
fn engine_error_sources_survive_the_fold() {
let io = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
let core: CoreError = EngineError::infra("connect failed").with_source(io).into();
let CoreError::System { source, .. } = &core else {
panic!("expected System variant");
};
assert!(source.is_some());
}
}