Skip to main content

shell_tunnel/execution/
result.rs

1//! Execution result types.
2
3use std::time::Duration;
4
5/// Result of command execution.
6#[derive(Debug, Clone)]
7pub struct ExecutionResult {
8    /// Raw output from the terminal.
9    pub raw_output: Vec<u8>,
10    /// Sanitized text output (ANSI codes stripped).
11    pub text_output: String,
12    /// Exit code (if command completed).
13    pub exit_code: Option<i32>,
14    /// Execution duration.
15    pub duration: Duration,
16    /// Whether execution timed out.
17    pub timed_out: bool,
18}
19
20impl ExecutionResult {
21    /// Create a new execution result.
22    pub fn new(raw_output: Vec<u8>, text_output: String, duration: Duration) -> Self {
23        Self {
24            raw_output,
25            text_output,
26            exit_code: None,
27            duration,
28            timed_out: false,
29        }
30    }
31
32    /// Create a result indicating timeout.
33    pub fn timeout(raw_output: Vec<u8>, text_output: String, duration: Duration) -> Self {
34        Self {
35            raw_output,
36            text_output,
37            exit_code: None,
38            duration,
39            timed_out: true,
40        }
41    }
42
43    /// Set the exit code.
44    pub fn with_exit_code(mut self, code: i32) -> Self {
45        self.exit_code = Some(code);
46        self
47    }
48
49    /// Check if command succeeded (exit code 0).
50    pub fn success(&self) -> bool {
51        self.exit_code == Some(0)
52    }
53
54    /// Check if command failed (non-zero exit code or timeout).
55    pub fn failed(&self) -> bool {
56        self.timed_out || matches!(self.exit_code, Some(c) if c != 0)
57    }
58
59    /// Get output as string, trimmed.
60    pub fn output_trimmed(&self) -> &str {
61        self.text_output.trim()
62    }
63
64    /// Get output lines.
65    pub fn output_lines(&self) -> impl Iterator<Item = &str> {
66        self.text_output.lines()
67    }
68}
69
70impl Default for ExecutionResult {
71    fn default() -> Self {
72        Self {
73            raw_output: Vec::new(),
74            text_output: String::new(),
75            exit_code: None,
76            duration: Duration::ZERO,
77            timed_out: false,
78        }
79    }
80}
81
82/// Streaming output chunk from execution.
83#[derive(Debug, Clone)]
84pub struct OutputChunk {
85    /// Raw bytes.
86    pub raw: Vec<u8>,
87    /// Decoded text (best effort).
88    pub text: String,
89    /// Stream source.
90    pub source: OutputSource,
91}
92
93/// Source of output data.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum OutputSource {
96    /// Standard output.
97    Stdout,
98    /// Standard error (if separate).
99    Stderr,
100    /// Combined output.
101    Combined,
102}
103
104impl OutputChunk {
105    /// Create a new output chunk.
106    pub fn new(raw: Vec<u8>, source: OutputSource) -> Self {
107        let text = String::from_utf8_lossy(&raw).into_owned();
108        Self { raw, text, source }
109    }
110
111    /// Create a stdout chunk.
112    pub fn stdout(raw: Vec<u8>) -> Self {
113        Self::new(raw, OutputSource::Stdout)
114    }
115
116    /// Create a combined output chunk.
117    pub fn combined(raw: Vec<u8>) -> Self {
118        Self::new(raw, OutputSource::Combined)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_execution_result_new() {
128        let result = ExecutionResult::new(
129            b"hello\n".to_vec(),
130            "hello\n".to_string(),
131            Duration::from_millis(100),
132        );
133
134        assert_eq!(result.raw_output, b"hello\n");
135        assert_eq!(result.text_output, "hello\n");
136        assert_eq!(result.duration, Duration::from_millis(100));
137        assert!(!result.timed_out);
138        assert!(result.exit_code.is_none());
139    }
140
141    #[test]
142    fn test_execution_result_success() {
143        let result = ExecutionResult::default().with_exit_code(0);
144        assert!(result.success());
145        assert!(!result.failed());
146    }
147
148    #[test]
149    fn test_execution_result_failed() {
150        let result = ExecutionResult::default().with_exit_code(1);
151        assert!(!result.success());
152        assert!(result.failed());
153    }
154
155    #[test]
156    fn test_execution_result_timeout() {
157        let result = ExecutionResult::timeout(vec![], String::new(), Duration::from_secs(30));
158        assert!(result.timed_out);
159        assert!(result.failed());
160    }
161
162    #[test]
163    fn test_output_trimmed() {
164        let result = ExecutionResult::new(vec![], "  hello world  \n".to_string(), Duration::ZERO);
165        assert_eq!(result.output_trimmed(), "hello world");
166    }
167
168    #[test]
169    fn test_output_lines() {
170        let result =
171            ExecutionResult::new(vec![], "line1\nline2\nline3".to_string(), Duration::ZERO);
172        let lines: Vec<_> = result.output_lines().collect();
173        assert_eq!(lines, vec!["line1", "line2", "line3"]);
174    }
175
176    #[test]
177    fn test_output_chunk_stdout() {
178        let chunk = OutputChunk::stdout(b"test output".to_vec());
179        assert_eq!(chunk.source, OutputSource::Stdout);
180        assert_eq!(chunk.text, "test output");
181    }
182
183    #[test]
184    fn test_output_chunk_combined() {
185        let chunk = OutputChunk::combined(b"mixed output".to_vec());
186        assert_eq!(chunk.source, OutputSource::Combined);
187    }
188}