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 test_failure(message: impl Into<String>) -> Self {
71 Self::TestFailure(message.into())
72 }
73
74 pub fn system(message: impl Into<String>) -> Self {
76 Self::System {
77 message: message.into(),
78 source: None,
79 }
80 }
81
82 pub fn system_with(
84 message: impl Into<String>,
85 source: impl Error + Send + Sync + 'static,
86 ) -> Self {
87 Self::System {
88 message: message.into(),
89 source: Some(Box::new(source)),
90 }
91 }
92
93 pub fn exit_code(&self) -> ExitCode {
95 match self {
96 Self::User(_) => ExitCode::UserError,
97 Self::TestFailure(_) => ExitCode::TestFailure,
98 Self::System { .. } => ExitCode::SystemError,
99 }
100 }
101}
102
103impl From<&CoreError> for ExitCode {
104 fn from(err: &CoreError) -> Self {
105 err.exit_code()
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum EngineErrorClass {
112 Infra,
114 AssertFailed,
116 Setup,
118}
119
120#[derive(Debug, thiserror::Error)]
122#[error("{message}")]
123pub struct EngineError {
124 pub class: EngineErrorClass,
126 pub message: String,
128 #[source]
130 pub source: Option<Box<dyn Error + Send + Sync>>,
131}
132
133impl EngineError {
134 pub fn infra(message: impl Into<String>) -> Self {
136 Self {
137 class: EngineErrorClass::Infra,
138 message: message.into(),
139 source: None,
140 }
141 }
142
143 pub fn assert_failed(message: impl Into<String>) -> Self {
145 Self {
146 class: EngineErrorClass::AssertFailed,
147 message: message.into(),
148 source: None,
149 }
150 }
151
152 pub fn setup(message: impl Into<String>) -> Self {
154 Self {
155 class: EngineErrorClass::Setup,
156 message: message.into(),
157 source: None,
158 }
159 }
160
161 #[must_use]
163 pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
164 self.source = Some(Box::new(source));
165 self
166 }
167}
168
169impl From<EngineError> for CoreError {
170 fn from(err: EngineError) -> Self {
171 match err.class {
172 EngineErrorClass::AssertFailed => Self::TestFailure(err.message),
173 EngineErrorClass::Infra | EngineErrorClass::Setup => Self::System {
174 message: err.message,
175 source: err.source,
176 },
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
187 fn core_error_exit_codes_are_stable() {
188 assert_eq!(CoreError::user("bad flag").exit_code().code(), 2);
189 assert_eq!(
190 CoreError::test_failure("assert failed").exit_code().code(),
191 1
192 );
193 assert_eq!(CoreError::system("no network").exit_code().code(), 3);
194 assert_eq!(ExitCode::Success.code(), 0);
195 }
196
197 #[test]
198 fn engine_errors_fold_into_the_core_taxonomy() {
199 let assert_failed: CoreError = EngineError::assert_failed("status != 200").into();
200 assert_eq!(assert_failed.exit_code(), ExitCode::TestFailure);
201
202 let infra: CoreError = EngineError::infra("connection refused").into();
203 assert_eq!(infra.exit_code(), ExitCode::SystemError);
204
205 let setup: CoreError = EngineError::setup("libcurl missing").into();
206 assert_eq!(setup.exit_code(), ExitCode::SystemError);
207 }
208
209 #[test]
210 fn engine_error_sources_survive_the_fold() {
211 let io = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
212 let core: CoreError = EngineError::infra("connect failed").with_source(io).into();
213 let CoreError::System { source, .. } = &core else {
214 panic!("expected System variant");
215 };
216 assert!(source.is_some());
217 }
218}