shell_tunnel/execution/
result.rs1use std::time::Duration;
4
5#[derive(Debug, Clone)]
7pub struct ExecutionResult {
8 pub raw_output: Vec<u8>,
10 pub text_output: String,
12 pub exit_code: Option<i32>,
14 pub duration: Duration,
16 pub timed_out: bool,
18}
19
20impl ExecutionResult {
21 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 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 pub fn with_exit_code(mut self, code: i32) -> Self {
45 self.exit_code = Some(code);
46 self
47 }
48
49 pub fn success(&self) -> bool {
51 self.exit_code == Some(0)
52 }
53
54 pub fn failed(&self) -> bool {
56 self.timed_out || matches!(self.exit_code, Some(c) if c != 0)
57 }
58
59 pub fn output_trimmed(&self) -> &str {
61 self.text_output.trim()
62 }
63
64 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#[derive(Debug, Clone)]
84pub struct OutputChunk {
85 pub raw: Vec<u8>,
87 pub text: String,
89 pub source: OutputSource,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum OutputSource {
96 Stdout,
98 Stderr,
100 Combined,
102}
103
104impl OutputChunk {
105 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 pub fn stdout(raw: Vec<u8>) -> Self {
113 Self::new(raw, OutputSource::Stdout)
114 }
115
116 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}