execution_engine/
errors.rs

1//! Error types for Execution Engine
2//!
3//! Comprehensive error handling using thiserror.
4//! See docs/error-handling.md for usage patterns.
5
6use std::path::PathBuf;
7use thiserror::Error;
8use uuid::Uuid;
9
10/// Main error type for execution operations
11#[derive(Debug, Error)]
12pub enum ExecutionError {
13    /// Validation error
14    #[error("Validation error: {0}")]
15    Validation(#[from] ValidationError),
16
17    /// Execution not found
18    #[error("Execution not found: {0}")]
19    NotFound(Uuid),
20
21    /// Execution timeout
22    #[error("Execution timeout after {0}ms")]
23    Timeout(u64),
24
25    /// Execution cancelled
26    #[error("Execution cancelled")]
27    Cancelled,
28
29    /// Command failed with exit code
30    #[error("Command failed with exit code {0}")]
31    CommandFailed(i32),
32
33    /// Process spawn failed
34    #[error("Process spawn failed: {0}")]
35    SpawnFailed(String),
36
37    /// IO error
38    #[error("IO error: {0}")]
39    Io(#[from] std::io::Error),
40
41    /// Serialization error
42    #[error("Serialization error: {0}")]
43    Serialization(#[from] serde_json::Error),
44
45    /// Output size exceeded
46    #[error("Output size exceeded maximum of {0} bytes")]
47    OutputSizeExceeded(usize),
48
49    /// Concurrency limit reached
50    #[error("Concurrency limit reached: {0} executions running")]
51    ConcurrencyLimitReached(usize),
52
53    /// Invalid configuration
54    #[error("Invalid configuration: {0}")]
55    InvalidConfig(String),
56
57    /// Internal error
58    #[error("Internal error: {0}")]
59    Internal(String),
60}
61
62/// Validation-specific errors
63#[derive(Debug, Error)]
64pub enum ValidationError {
65    /// Invalid command format
66    #[error("Invalid command format: {0}")]
67    InvalidCommand(String),
68
69    /// Script file not found
70    #[error("Script file not found: {0}")]
71    ScriptNotFound(PathBuf),
72
73    /// Script file not executable
74    #[error("Script file not executable: {0}")]
75    ScriptNotExecutable(PathBuf),
76
77    /// Timeout exceeds maximum
78    #[error("Timeout exceeds maximum allowed: {0}ms > {1}ms")]
79    TimeoutTooLarge(u64, u64),
80
81    /// Invalid working directory
82    #[error("Invalid working directory: {0}")]
83    InvalidWorkingDir(PathBuf),
84
85    /// Working directory does not exist
86    #[error("Working directory does not exist: {0}")]
87    WorkingDirNotFound(PathBuf),
88
89    /// Missing required field
90    #[error("Missing required field: {0}")]
91    MissingField(String),
92
93    /// Invalid execution plan
94    #[error("Invalid execution plan: {0}")]
95    InvalidPlan(String),
96
97    /// Empty command
98    #[error("Command cannot be empty")]
99    EmptyCommand,
100
101    /// Invalid dependency graph
102    #[error("Invalid dependency graph: {0}")]
103    InvalidDependencyGraph(String),
104
105    /// Program not found in PATH
106    #[error("Program not found in PATH: {0}")]
107    ProgramNotFound(String),
108}
109
110/// Result type alias for execution operations
111pub type Result<T> = std::result::Result<T, ExecutionError>;
112
113/// Result type alias for validation operations
114pub type ValidationResult<T> = std::result::Result<T, ValidationError>;
115
116// ============================================================================
117// Helper implementations
118// ============================================================================
119
120impl ExecutionError {
121    /// Check if error is retryable
122    #[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    /// Check if error is terminal (not retryable)
134    #[must_use]
135    pub fn is_terminal(&self) -> bool {
136        !self.is_retryable()
137    }
138
139    /// Get error code for categorization
140    #[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    /// Create InvalidCommand error with message
161    pub fn invalid_command<S: Into<String>>(msg: S) -> Self {
162        ValidationError::InvalidCommand(msg.into())
163    }
164
165    /// Create MissingField error
166    pub fn missing_field<S: Into<String>>(field: S) -> Self {
167        ValidationError::MissingField(field.into())
168    }
169
170    /// Create InvalidPlan error with message
171    pub fn invalid_plan<S: Into<String>>(msg: S) -> Self {
172        ValidationError::InvalidPlan(msg.into())
173    }
174}
175
176// ============================================================================
177// Tests
178// ============================================================================
179
180#[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}