Skip to main content

bashkit/interpreter/
state.rs

1//! Interpreter state types
2
3/// Control flow signals from commands like break, continue, return
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum ControlFlow {
6    #[default]
7    None,
8    /// Break out of a loop (with optional level count)
9    Break(u32),
10    /// Continue to next iteration (with optional level count)
11    Continue(u32),
12    /// Return from a function (with exit code)
13    Return(i32),
14}
15
16/// Result of executing a bash script.
17#[derive(Debug, Clone, Default)]
18pub struct ExecResult {
19    /// Standard output
20    pub stdout: String,
21    /// Standard error
22    pub stderr: String,
23    /// Exit code
24    pub exit_code: i32,
25    /// Control flow signal (break, continue, return)
26    pub control_flow: ControlFlow,
27}
28
29impl ExecResult {
30    /// Create a successful result with the given stdout.
31    pub fn ok(stdout: impl Into<String>) -> Self {
32        Self {
33            stdout: stdout.into(),
34            stderr: String::new(),
35            exit_code: 0,
36            control_flow: ControlFlow::None,
37        }
38    }
39
40    /// Create a failed result with the given stderr.
41    pub fn err(stderr: impl Into<String>, exit_code: i32) -> Self {
42        Self {
43            stdout: String::new(),
44            stderr: stderr.into(),
45            exit_code,
46            control_flow: ControlFlow::None,
47        }
48    }
49
50    /// Create a result with stdout and custom exit code.
51    pub fn with_code(stdout: impl Into<String>, exit_code: i32) -> Self {
52        Self {
53            stdout: stdout.into(),
54            stderr: String::new(),
55            exit_code,
56            control_flow: ControlFlow::None,
57        }
58    }
59
60    /// Create a result with a control flow signal
61    pub fn with_control_flow(control_flow: ControlFlow) -> Self {
62        Self {
63            stdout: String::new(),
64            stderr: String::new(),
65            exit_code: 0,
66            control_flow,
67        }
68    }
69
70    /// Check if the result indicates success.
71    pub fn is_success(&self) -> bool {
72        self.exit_code == 0
73    }
74}
75
76#[cfg(test)]
77#[allow(clippy::unwrap_used)]
78mod tests {
79    use super::*;
80
81    // --- ControlFlow ---
82
83    #[test]
84    fn control_flow_default_is_none() {
85        assert_eq!(ControlFlow::default(), ControlFlow::None);
86    }
87
88    #[test]
89    fn control_flow_break_stores_level() {
90        let cf = ControlFlow::Break(2);
91        assert_eq!(cf, ControlFlow::Break(2));
92        assert_ne!(cf, ControlFlow::Break(1));
93    }
94
95    #[test]
96    fn control_flow_continue_stores_level() {
97        let cf = ControlFlow::Continue(3);
98        assert_eq!(cf, ControlFlow::Continue(3));
99    }
100
101    #[test]
102    fn control_flow_return_stores_code() {
103        let cf = ControlFlow::Return(42);
104        assert_eq!(cf, ControlFlow::Return(42));
105    }
106
107    #[test]
108    fn control_flow_variants_not_equal() {
109        assert_ne!(ControlFlow::None, ControlFlow::Break(0));
110        assert_ne!(ControlFlow::Break(1), ControlFlow::Continue(1));
111        assert_ne!(ControlFlow::Continue(1), ControlFlow::Return(1));
112    }
113
114    #[test]
115    fn control_flow_clone() {
116        let cf = ControlFlow::Return(5);
117        let cloned = cf;
118        assert_eq!(cf, cloned);
119    }
120
121    // --- ExecResult::ok ---
122
123    #[test]
124    fn exec_result_ok_sets_stdout() {
125        let r = ExecResult::ok("hello");
126        assert_eq!(r.stdout, "hello");
127        assert_eq!(r.stderr, "");
128        assert_eq!(r.exit_code, 0);
129        assert_eq!(r.control_flow, ControlFlow::None);
130    }
131
132    #[test]
133    fn exec_result_ok_empty_string() {
134        let r = ExecResult::ok("");
135        assert_eq!(r.stdout, "");
136        assert!(r.is_success());
137    }
138
139    #[test]
140    fn exec_result_ok_accepts_string() {
141        let s = String::from("owned");
142        let r = ExecResult::ok(s);
143        assert_eq!(r.stdout, "owned");
144    }
145
146    // --- ExecResult::err ---
147
148    #[test]
149    fn exec_result_err_sets_stderr_and_code() {
150        let r = ExecResult::err("bad command", 127);
151        assert_eq!(r.stdout, "");
152        assert_eq!(r.stderr, "bad command");
153        assert_eq!(r.exit_code, 127);
154        assert_eq!(r.control_flow, ControlFlow::None);
155    }
156
157    #[test]
158    fn exec_result_err_is_not_success() {
159        let r = ExecResult::err("fail", 1);
160        assert!(!r.is_success());
161    }
162
163    #[test]
164    fn exec_result_err_with_code_zero_is_success() {
165        // Edge case: err constructor with exit_code 0
166        let r = ExecResult::err("warning", 0);
167        assert!(r.is_success());
168    }
169
170    // --- ExecResult::with_code ---
171
172    #[test]
173    fn exec_result_with_code_sets_stdout_and_code() {
174        let r = ExecResult::with_code("partial", 2);
175        assert_eq!(r.stdout, "partial");
176        assert_eq!(r.stderr, "");
177        assert_eq!(r.exit_code, 2);
178        assert_eq!(r.control_flow, ControlFlow::None);
179    }
180
181    #[test]
182    fn exec_result_with_code_zero() {
183        let r = ExecResult::with_code("ok", 0);
184        assert!(r.is_success());
185    }
186
187    #[test]
188    fn exec_result_with_code_negative() {
189        let r = ExecResult::with_code("", -1);
190        assert!(!r.is_success());
191        assert_eq!(r.exit_code, -1);
192    }
193
194    // --- ExecResult::with_control_flow ---
195
196    #[test]
197    fn exec_result_with_control_flow_break() {
198        let r = ExecResult::with_control_flow(ControlFlow::Break(1));
199        assert_eq!(r.stdout, "");
200        assert_eq!(r.stderr, "");
201        assert_eq!(r.exit_code, 0);
202        assert_eq!(r.control_flow, ControlFlow::Break(1));
203    }
204
205    #[test]
206    fn exec_result_with_control_flow_continue() {
207        let r = ExecResult::with_control_flow(ControlFlow::Continue(1));
208        assert_eq!(r.control_flow, ControlFlow::Continue(1));
209    }
210
211    #[test]
212    fn exec_result_with_control_flow_return() {
213        let r = ExecResult::with_control_flow(ControlFlow::Return(0));
214        assert_eq!(r.control_flow, ControlFlow::Return(0));
215    }
216
217    #[test]
218    fn exec_result_with_control_flow_none() {
219        let r = ExecResult::with_control_flow(ControlFlow::None);
220        assert_eq!(r.control_flow, ControlFlow::None);
221        assert!(r.is_success());
222    }
223
224    // --- ExecResult::is_success ---
225
226    #[test]
227    fn exec_result_is_success_true_for_zero() {
228        let r = ExecResult::ok("x");
229        assert!(r.is_success());
230    }
231
232    #[test]
233    fn exec_result_is_success_false_for_nonzero() {
234        let r = ExecResult::err("x", 1);
235        assert!(!r.is_success());
236        let r2 = ExecResult::with_code("", 255);
237        assert!(!r2.is_success());
238    }
239
240    // --- ExecResult::default ---
241
242    #[test]
243    fn exec_result_default() {
244        let r = ExecResult::default();
245        assert_eq!(r.stdout, "");
246        assert_eq!(r.stderr, "");
247        assert_eq!(r.exit_code, 0);
248        assert_eq!(r.control_flow, ControlFlow::None);
249        assert!(r.is_success());
250    }
251
252    // --- Debug ---
253
254    #[test]
255    fn exec_result_debug_format() {
256        let r = ExecResult::ok("test");
257        let dbg = format!("{:?}", r);
258        assert!(dbg.contains("ExecResult"));
259        assert!(dbg.contains("test"));
260    }
261
262    #[test]
263    fn control_flow_debug_format() {
264        let cf = ControlFlow::Break(3);
265        let dbg = format!("{:?}", cf);
266        assert!(dbg.contains("Break"));
267        assert!(dbg.contains("3"));
268    }
269}