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 {
178 match err.class {
179 EngineErrorClass::AssertFailed => Self::TestFailure(err.message),
180 EngineErrorClass::UserInput => Self::User(err.message),
181 EngineErrorClass::Infra | EngineErrorClass::Setup => Self::System {
182 message: err.message,
183 source: err.source,
184 },
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
195 fn core_error_exit_codes_are_stable() {
196 assert_eq!(CoreError::user("bad flag").exit_code().code(), 2);
197 assert_eq!(
198 CoreError::TestFailure("assert failed".to_owned())
199 .exit_code()
200 .code(),
201 1
202 );
203 assert_eq!(CoreError::system("no network").exit_code().code(), 3);
204 assert_eq!(ExitCode::Success.code(), 0);
205 }
206
207 #[test]
208 fn engine_errors_fold_into_the_core_taxonomy() {
209 let assert_failed: CoreError = EngineError::assert_failed("status != 200").into();
210 assert_eq!(assert_failed.exit_code(), ExitCode::TestFailure);
211
212 let infra: CoreError = EngineError::infra("connection refused").into();
213 assert_eq!(infra.exit_code(), ExitCode::SystemError);
214
215 let setup: CoreError = EngineError::setup("libcurl missing").into();
216 assert_eq!(setup.exit_code(), ExitCode::SystemError);
217 }
218
219 #[test]
220 fn engine_error_sources_survive_the_fold() {
221 let io = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
222 let core: CoreError = EngineError::infra("connect failed").with_source(io).into();
223 let CoreError::System { source, .. } = &core else {
224 panic!("expected System variant");
225 };
226 assert!(source.is_some());
227 }
228}