1use std::error::Error;
13use std::fmt;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[repr(u8)]
19pub enum ExitCode {
20 Success = 0,
22 TestFailure = 1,
24 UserError = 2,
26 SystemError = 3,
28}
29
30impl ExitCode {
31 pub fn code(self) -> u8 {
33 self as u8
34 }
35}
36
37impl fmt::Display for ExitCode {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 write!(f, "{}", self.code())
40 }
41}
42
43#[derive(Debug, thiserror::Error)]
45pub enum CoreError {
46 #[error("{0}")]
48 User(String),
49 #[error("{0}")]
51 TestFailure(String),
52 #[error("{message}")]
54 System {
55 message: String,
57 #[source]
59 source: Option<Box<dyn Error + Send + Sync>>,
60 },
61}
62
63impl CoreError {
64 pub fn user(message: impl Into<String>) -> Self {
66 Self::User(message.into())
67 }
68
69 pub fn system(message: impl Into<String>) -> Self {
71 Self::System {
72 message: message.into(),
73 source: None,
74 }
75 }
76
77 pub fn system_with(
79 message: impl Into<String>,
80 source: impl Error + Send + Sync + 'static,
81 ) -> Self {
82 Self::System {
83 message: message.into(),
84 source: Some(Box::new(source)),
85 }
86 }
87
88 pub fn exit_code(&self) -> ExitCode {
90 match self {
91 Self::User(_) => ExitCode::UserError,
92 Self::TestFailure(_) => ExitCode::TestFailure,
93 Self::System { .. } => ExitCode::SystemError,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum EngineErrorClass {
101 Infra,
103 AssertFailed,
105 UserInput,
109 Setup,
111}
112
113#[derive(Debug, thiserror::Error)]
115#[error("{message}")]
116pub struct EngineError {
117 pub class: EngineErrorClass,
119 pub message: String,
121 #[source]
123 pub source: Option<Box<dyn Error + Send + Sync>>,
124}
125
126impl EngineError {
127 pub fn infra(message: impl Into<String>) -> Self {
129 Self {
130 class: EngineErrorClass::Infra,
131 message: message.into(),
132 source: None,
133 }
134 }
135
136 pub fn assert_failed(message: impl Into<String>) -> Self {
138 Self {
139 class: EngineErrorClass::AssertFailed,
140 message: message.into(),
141 source: None,
142 }
143 }
144
145 pub fn user_input(message: impl Into<String>) -> Self {
147 Self {
148 class: EngineErrorClass::UserInput,
149 message: message.into(),
150 source: None,
151 }
152 }
153
154 pub fn setup(message: impl Into<String>) -> Self {
156 Self {
157 class: EngineErrorClass::Setup,
158 message: message.into(),
159 source: None,
160 }
161 }
162
163 #[must_use]
165 pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
166 self.source = Some(Box::new(source));
167 self
168 }
169}
170
171impl From<EngineError> for CoreError {
172 fn from(err: EngineError) -> Self {
173 match err.class {
174 EngineErrorClass::AssertFailed => Self::TestFailure(err.message),
175 EngineErrorClass::UserInput => Self::User(err.message),
176 EngineErrorClass::Infra | EngineErrorClass::Setup => Self::System {
177 message: err.message,
178 source: err.source,
179 },
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
190 fn core_error_exit_codes_are_stable() {
191 assert_eq!(CoreError::user("bad flag").exit_code().code(), 2);
192 assert_eq!(
193 CoreError::TestFailure("assert failed".to_owned())
194 .exit_code()
195 .code(),
196 1
197 );
198 assert_eq!(CoreError::system("no network").exit_code().code(), 3);
199 assert_eq!(ExitCode::Success.code(), 0);
200 }
201
202 #[test]
203 fn engine_errors_fold_into_the_core_taxonomy() {
204 let assert_failed: CoreError = EngineError::assert_failed("status != 200").into();
205 assert_eq!(assert_failed.exit_code(), ExitCode::TestFailure);
206
207 let infra: CoreError = EngineError::infra("connection refused").into();
208 assert_eq!(infra.exit_code(), ExitCode::SystemError);
209
210 let setup: CoreError = EngineError::setup("libcurl missing").into();
211 assert_eq!(setup.exit_code(), ExitCode::SystemError);
212 }
213
214 #[test]
215 fn engine_error_sources_survive_the_fold() {
216 let io = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
217 let core: CoreError = EngineError::infra("connect failed").with_source(io).into();
218 let CoreError::System { source, .. } = &core else {
219 panic!("expected System variant");
220 };
221 assert!(source.is_some());
222 }
223}