Skip to main content

torrust_tracker_deployer_lib/shared/command/
error.rs

1//! Command execution error types
2//!
3//! This module provides error types for command execution failures,
4//! including startup errors and execution errors with detailed context.
5
6use std::path::PathBuf;
7
8use thiserror::Error;
9
10/// Errors that can occur during command execution
11#[derive(Error, Debug)]
12pub enum CommandError {
13    /// The command could not be started (e.g., command not found, permission denied)
14    #[error("Failed to start command '{command}': {source}")]
15    StartupFailed {
16        command: String,
17        #[source]
18        source: std::io::Error,
19    },
20
21    /// The working directory does not exist
22    #[error("Working directory does not exist: '{working_dir}'")]
23    WorkingDirectoryNotFound { working_dir: PathBuf },
24
25    /// The command was started but exited with a non-zero status code
26    #[error(
27        "Command '{command}' failed with exit code {exit_code}\nStdout: {stdout}\nStderr: {stderr}"
28    )]
29    ExecutionFailed {
30        command: String,
31        exit_code: String,
32        stdout: String,
33        stderr: String,
34    },
35}
36
37impl crate::shared::Traceable for CommandError {
38    fn trace_format(&self) -> String {
39        match self {
40            Self::StartupFailed { command, source } => {
41                format!("CommandError: Failed to start '{command}' - {source}")
42            }
43            Self::WorkingDirectoryNotFound { working_dir } => {
44                format!(
45                    "CommandError: Working directory does not exist - '{}'",
46                    working_dir.display()
47                )
48            }
49            Self::ExecutionFailed {
50                command,
51                exit_code,
52                stdout,
53                stderr,
54            } => {
55                format!("CommandError: Command '{command}' failed with exit code {exit_code}\nStdout: {stdout}\nStderr: {stderr}")
56            }
57        }
58    }
59
60    fn trace_source(&self) -> Option<&dyn crate::shared::Traceable> {
61        // std::io::Error doesn't implement Traceable, so we return None
62        None
63    }
64
65    fn error_kind(&self) -> crate::shared::ErrorKind {
66        crate::shared::ErrorKind::CommandExecution
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use std::error::Error;
74    use std::io;
75
76    #[test]
77    fn it_should_format_startup_failed_error_correctly() {
78        let io_error = io::Error::new(io::ErrorKind::NotFound, "command not found");
79        let error = CommandError::StartupFailed {
80            command: "nonexistent_command".to_string(),
81            source: io_error,
82        };
83
84        let error_message = error.to_string();
85        assert!(error_message.contains("Failed to start command 'nonexistent_command'"));
86        assert!(error_message.contains("command not found"));
87    }
88
89    #[test]
90    fn it_should_format_execution_failed_error_correctly() {
91        let error = CommandError::ExecutionFailed {
92            command: "false".to_string(),
93            exit_code: "1".to_string(),
94            stdout: String::new(),
95            stderr: "command failed".to_string(),
96        };
97
98        let error_message = error.to_string();
99        assert!(error_message.contains("Command 'false' failed with exit code 1"));
100        assert!(error_message.contains("Stderr: command failed"));
101    }
102
103    #[test]
104    fn it_should_preserve_source_error_chain() {
105        let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "permission denied");
106        let error = CommandError::StartupFailed {
107            command: "restricted_command".to_string(),
108            source: io_error,
109        };
110
111        // Test that the source error is preserved
112        assert!(error.source().is_some());
113        assert_eq!(error.source().unwrap().to_string(), "permission denied");
114    }
115
116    #[test]
117    fn it_should_format_working_directory_not_found_error_correctly() {
118        let error = CommandError::WorkingDirectoryNotFound {
119            working_dir: PathBuf::from("/nonexistent/path/to/dir"),
120        };
121
122        let error_message = error.to_string();
123        assert!(error_message.contains("Working directory does not exist"));
124        assert!(error_message.contains("/nonexistent/path/to/dir"));
125    }
126}