execution_engine/
errors.rs1use std::path::PathBuf;
7use thiserror::Error;
8use uuid::Uuid;
9
10#[derive(Debug, Error)]
12pub enum ExecutionError {
13 #[error("Validation error: {0}")]
15 Validation(#[from] ValidationError),
16
17 #[error("Execution not found: {0}")]
19 NotFound(Uuid),
20
21 #[error("Execution timeout after {0}ms")]
23 Timeout(u64),
24
25 #[error("Execution cancelled")]
27 Cancelled,
28
29 #[error("Command failed with exit code {0}")]
31 CommandFailed(i32),
32
33 #[error("Process spawn failed: {0}")]
35 SpawnFailed(String),
36
37 #[error("IO error: {0}")]
39 Io(#[from] std::io::Error),
40
41 #[error("Serialization error: {0}")]
43 Serialization(#[from] serde_json::Error),
44
45 #[error("Output size exceeded maximum of {0} bytes")]
47 OutputSizeExceeded(usize),
48
49 #[error("Concurrency limit reached: {0} executions running")]
51 ConcurrencyLimitReached(usize),
52
53 #[error("Invalid configuration: {0}")]
55 InvalidConfig(String),
56
57 #[error("Internal error: {0}")]
59 Internal(String),
60}
61
62#[derive(Debug, Error)]
64pub enum ValidationError {
65 #[error("Invalid command format: {0}")]
67 InvalidCommand(String),
68
69 #[error("Script file not found: {0}")]
71 ScriptNotFound(PathBuf),
72
73 #[error("Script file not executable: {0}")]
75 ScriptNotExecutable(PathBuf),
76
77 #[error("Timeout exceeds maximum allowed: {0}ms > {1}ms")]
79 TimeoutTooLarge(u64, u64),
80
81 #[error("Invalid working directory: {0}")]
83 InvalidWorkingDir(PathBuf),
84
85 #[error("Working directory does not exist: {0}")]
87 WorkingDirNotFound(PathBuf),
88
89 #[error("Missing required field: {0}")]
91 MissingField(String),
92
93 #[error("Invalid execution plan: {0}")]
95 InvalidPlan(String),
96
97 #[error("Command cannot be empty")]
99 EmptyCommand,
100
101 #[error("Invalid dependency graph: {0}")]
103 InvalidDependencyGraph(String),
104
105 #[error("Program not found in PATH: {0}")]
107 ProgramNotFound(String),
108}
109
110pub type Result<T> = std::result::Result<T, ExecutionError>;
112
113pub type ValidationResult<T> = std::result::Result<T, ValidationError>;
115
116impl ExecutionError {
121 #[must_use]
123 pub fn is_retryable(&self) -> bool {
124 matches!(
125 self,
126 ExecutionError::Timeout(_)
127 | ExecutionError::Io(_)
128 | ExecutionError::SpawnFailed(_)
129 | ExecutionError::ConcurrencyLimitReached(_)
130 )
131 }
132
133 #[must_use]
135 pub fn is_terminal(&self) -> bool {
136 !self.is_retryable()
137 }
138
139 #[must_use]
141 pub fn error_code(&self) -> &str {
142 match self {
143 ExecutionError::Validation(_) => "VALIDATION_ERROR",
144 ExecutionError::NotFound(_) => "NOT_FOUND",
145 ExecutionError::Timeout(_) => "TIMEOUT",
146 ExecutionError::Cancelled => "CANCELLED",
147 ExecutionError::CommandFailed(_) => "COMMAND_FAILED",
148 ExecutionError::SpawnFailed(_) => "SPAWN_FAILED",
149 ExecutionError::Io(_) => "IO_ERROR",
150 ExecutionError::Serialization(_) => "SERIALIZATION_ERROR",
151 ExecutionError::OutputSizeExceeded(_) => "OUTPUT_SIZE_EXCEEDED",
152 ExecutionError::ConcurrencyLimitReached(_) => "CONCURRENCY_LIMIT_REACHED",
153 ExecutionError::InvalidConfig(_) => "INVALID_CONFIG",
154 ExecutionError::Internal(_) => "INTERNAL_ERROR",
155 }
156 }
157}
158
159impl ValidationError {
160 pub fn invalid_command<S: Into<String>>(msg: S) -> Self {
162 ValidationError::InvalidCommand(msg.into())
163 }
164
165 pub fn missing_field<S: Into<String>>(field: S) -> Self {
167 ValidationError::MissingField(field.into())
168 }
169
170 pub fn invalid_plan<S: Into<String>>(msg: S) -> Self {
172 ValidationError::InvalidPlan(msg.into())
173 }
174}
175
176#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_execution_error_display() {
186 let err = ExecutionError::Timeout(5000);
187 assert_eq!(err.to_string(), "Execution timeout after 5000ms");
188
189 let err = ExecutionError::CommandFailed(1);
190 assert_eq!(err.to_string(), "Command failed with exit code 1");
191
192 let err = ExecutionError::Cancelled;
193 assert_eq!(err.to_string(), "Execution cancelled");
194 }
195
196 #[test]
197 fn test_validation_error_display() {
198 let err = ValidationError::EmptyCommand;
199 assert_eq!(err.to_string(), "Command cannot be empty");
200
201 let err = ValidationError::TimeoutTooLarge(10000, 5000);
202 assert_eq!(
203 err.to_string(),
204 "Timeout exceeds maximum allowed: 10000ms > 5000ms"
205 );
206
207 let err = ValidationError::MissingField("command".to_string());
208 assert_eq!(err.to_string(), "Missing required field: command");
209 }
210
211 #[test]
212 fn test_error_from_validation() {
213 let validation_err = ValidationError::EmptyCommand;
214 let exec_err: ExecutionError = validation_err.into();
215
216 match exec_err {
217 ExecutionError::Validation(e) => {
218 assert_eq!(e.to_string(), "Command cannot be empty");
219 }
220 _ => panic!("Expected Validation error"),
221 }
222 }
223
224 #[test]
225 fn test_error_is_retryable() {
226 assert!(ExecutionError::Timeout(5000).is_retryable());
227 assert!(ExecutionError::SpawnFailed("error".to_string()).is_retryable());
228 assert!(ExecutionError::ConcurrencyLimitReached(100).is_retryable());
229
230 assert!(!ExecutionError::Cancelled.is_retryable());
231 assert!(!ExecutionError::CommandFailed(1).is_retryable());
232 assert!(!ExecutionError::NotFound(Uuid::new_v4()).is_retryable());
233 }
234
235 #[test]
236 fn test_error_is_terminal() {
237 assert!(!ExecutionError::Timeout(5000).is_terminal());
238 assert!(ExecutionError::Cancelled.is_terminal());
239 assert!(ExecutionError::CommandFailed(1).is_terminal());
240 }
241
242 #[test]
243 fn test_error_code() {
244 assert_eq!(ExecutionError::Timeout(5000).error_code(), "TIMEOUT");
245 assert_eq!(ExecutionError::Cancelled.error_code(), "CANCELLED");
246 assert_eq!(
247 ExecutionError::CommandFailed(1).error_code(),
248 "COMMAND_FAILED"
249 );
250 assert_eq!(
251 ExecutionError::NotFound(Uuid::new_v4()).error_code(),
252 "NOT_FOUND"
253 );
254 assert_eq!(
255 ExecutionError::OutputSizeExceeded(1000).error_code(),
256 "OUTPUT_SIZE_EXCEEDED"
257 );
258 }
259
260 #[test]
261 fn test_validation_error_helpers() {
262 let err = ValidationError::invalid_command("invalid syntax");
263 assert_eq!(err.to_string(), "Invalid command format: invalid syntax");
264
265 let err = ValidationError::missing_field("timeout_ms");
266 assert_eq!(err.to_string(), "Missing required field: timeout_ms");
267
268 let err = ValidationError::invalid_plan("circular dependency");
269 assert_eq!(
270 err.to_string(),
271 "Invalid execution plan: circular dependency"
272 );
273 }
274
275 #[test]
276 fn test_result_type_alias() {
277 fn returns_result() -> Result<i32> {
278 Ok(42)
279 }
280
281 fn returns_validation_result() -> ValidationResult<String> {
282 Ok("valid".to_string())
283 }
284
285 assert_eq!(returns_result().unwrap(), 42);
286 assert_eq!(returns_validation_result().unwrap(), "valid");
287 }
288
289 #[test]
290 fn test_error_chain() {
291 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
292 let exec_err: ExecutionError = io_err.into();
293
294 match exec_err {
295 ExecutionError::Io(e) => {
296 assert_eq!(e.kind(), std::io::ErrorKind::NotFound);
297 }
298 _ => panic!("Expected IO error"),
299 }
300 }
301}