oxur_repl/eval/
output_capture.rs

1//! Output Capture for REPL Execution
2//!
3//! Captures stdout and stderr during code execution to return as part of
4//! the evaluation result. This allows REPL clients to display all output
5//! from executed code.
6//!
7//! Based on ODD-0026: Oxur REPL Evaluation Strategy
8
9use std::sync::{Arc, Mutex};
10
11/// Captured output from code execution
12#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct CapturedOutput {
14    /// Standard output (stdout)
15    pub stdout: String,
16
17    /// Standard error (stderr)
18    pub stderr: String,
19}
20
21impl CapturedOutput {
22    /// Create a new empty captured output
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    /// Check if any output was captured
28    pub fn is_empty(&self) -> bool {
29        self.stdout.is_empty() && self.stderr.is_empty()
30    }
31
32    /// Get stdout as Option (None if empty)
33    pub fn stdout_option(&self) -> Option<String> {
34        if self.stdout.is_empty() {
35            None
36        } else {
37            Some(self.stdout.clone())
38        }
39    }
40
41    /// Get stderr as Option (None if empty)
42    pub fn stderr_option(&self) -> Option<String> {
43        if self.stderr.is_empty() {
44            None
45        } else {
46            Some(self.stderr.clone())
47        }
48    }
49}
50
51/// Output capturer for a single execution
52///
53/// Provides a simple interface for capturing output during code execution.
54/// Uses Arc<Mutex<String>> internally to allow sharing across threads.
55#[derive(Debug, Default)]
56pub struct OutputCapturer {
57    /// Captured stdout
58    stdout: Arc<Mutex<String>>,
59
60    /// Captured stderr
61    stderr: Arc<Mutex<String>>,
62}
63
64impl OutputCapturer {
65    /// Create a new output capturer
66    pub fn new() -> Self {
67        Self {
68            stdout: Arc::new(Mutex::new(String::new())),
69            stderr: Arc::new(Mutex::new(String::new())),
70        }
71    }
72
73    /// Capture stdout
74    ///
75    /// Appends the given string to the captured stdout.
76    pub fn capture_stdout(&self, output: &str) {
77        if let Ok(mut stdout) = self.stdout.lock() {
78            stdout.push_str(output);
79        }
80    }
81
82    /// Capture stderr
83    ///
84    /// Appends the given string to the captured stderr.
85    pub fn capture_stderr(&self, output: &str) {
86        if let Ok(mut stderr) = self.stderr.lock() {
87            stderr.push_str(output);
88        }
89    }
90
91    /// Get the captured output
92    pub fn get_output(&self) -> CapturedOutput {
93        let stdout = self.stdout.lock().map(|s| s.clone()).unwrap_or_default();
94
95        let stderr = self.stderr.lock().map(|s| s.clone()).unwrap_or_default();
96
97        CapturedOutput { stdout, stderr }
98    }
99
100    /// Clear all captured output
101    pub fn clear(&self) {
102        if let Ok(mut stdout) = self.stdout.lock() {
103            stdout.clear();
104        }
105        if let Ok(mut stderr) = self.stderr.lock() {
106            stderr.clear();
107        }
108    }
109
110    /// Execute a function and capture its output
111    ///
112    /// This is a helper for testing and will be used when we integrate
113    /// with actual code execution.
114    ///
115    /// # Example
116    ///
117    /// ```
118    /// use oxur_repl::eval::output_capture::OutputCapturer;
119    ///
120    /// let capturer = OutputCapturer::new();
121    ///
122    /// let result = capturer.with_capture(|| {
123    ///     // Simulate some output
124    ///     capturer.capture_stdout("Hello, ");
125    ///     capturer.capture_stdout("world!\n");
126    ///     capturer.capture_stderr("Warning: test\n");
127    ///     42
128    /// });
129    ///
130    /// assert_eq!(result, 42);
131    /// let output = capturer.get_output();
132    /// assert_eq!(output.stdout, "Hello, world!\n");
133    /// assert_eq!(output.stderr, "Warning: test\n");
134    /// ```
135    pub fn with_capture<F, R>(&self, f: F) -> R
136    where
137        F: FnOnce() -> R,
138    {
139        // Clear previous output
140        self.clear();
141
142        // Execute and return the result
143        f()
144    }
145}
146
147/// Simulate compiled code execution with output
148///
149/// This is a placeholder for when we actually execute compiled code.
150/// It demonstrates how output capture will work with real execution.
151pub fn simulate_execution(code: impl AsRef<str>, capturer: &OutputCapturer) -> String {
152    let code = code.as_ref();
153    // Simulate different types of code execution
154
155    // Check for eprintln! (check this first since it contains "println")
156    if code.contains("eprintln!(\"") {
157        if let Some(start) = code.find("eprintln!(\"") {
158            if let Some(end) = code[start..].find("\")") {
159                let msg = &code[start + 11..start + end];
160                capturer.capture_stderr(&format!("{}\n", msg));
161            }
162        }
163    }
164
165    // Also check for println! (use more specific match to avoid matching eprintln)
166    if let Some(start) = code.find("println!(\"") {
167        // Make sure this isn't part of "eprintln"
168        let is_println = if start > 0 { !code[..start].ends_with('e') } else { true };
169
170        if is_println {
171            if let Some(end) = code[start..].find("\")") {
172                let msg = &code[start + 10..start + end];
173                capturer.capture_stdout(&format!("{}\n", msg));
174            }
175        }
176    }
177
178    // Return a placeholder execution result
179    format!("executed: {}", code.chars().take(50).collect::<String>())
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_captured_output_new() {
188        let output = CapturedOutput::new();
189        assert!(output.is_empty());
190        assert_eq!(output.stdout, "");
191        assert_eq!(output.stderr, "");
192    }
193
194    #[test]
195    fn test_captured_output_is_empty() {
196        let mut output = CapturedOutput::new();
197        assert!(output.is_empty());
198
199        output.stdout = "test".to_string();
200        assert!(!output.is_empty());
201
202        output.stdout.clear();
203        output.stderr = "error".to_string();
204        assert!(!output.is_empty());
205    }
206
207    #[test]
208    fn test_captured_output_options() {
209        let output = CapturedOutput { stdout: "output".to_string(), stderr: "".to_string() };
210
211        assert_eq!(output.stdout_option(), Some("output".to_string()));
212        assert_eq!(output.stderr_option(), None);
213    }
214
215    #[test]
216    fn test_output_capturer_new() {
217        let capturer = OutputCapturer::new();
218        let output = capturer.get_output();
219        assert!(output.is_empty());
220    }
221
222    #[test]
223    fn test_capture_stdout() {
224        let capturer = OutputCapturer::new();
225
226        capturer.capture_stdout("Hello, ");
227        capturer.capture_stdout("world!\n");
228
229        let output = capturer.get_output();
230        assert_eq!(output.stdout, "Hello, world!\n");
231        assert_eq!(output.stderr, "");
232    }
233
234    #[test]
235    fn test_capture_stderr() {
236        let capturer = OutputCapturer::new();
237
238        capturer.capture_stderr("Error: ");
239        capturer.capture_stderr("something went wrong\n");
240
241        let output = capturer.get_output();
242        assert_eq!(output.stdout, "");
243        assert_eq!(output.stderr, "Error: something went wrong\n");
244    }
245
246    #[test]
247    fn test_capture_both() {
248        let capturer = OutputCapturer::new();
249
250        capturer.capture_stdout("Output line 1\n");
251        capturer.capture_stderr("Warning: test\n");
252        capturer.capture_stdout("Output line 2\n");
253
254        let output = capturer.get_output();
255        assert_eq!(output.stdout, "Output line 1\nOutput line 2\n");
256        assert_eq!(output.stderr, "Warning: test\n");
257    }
258
259    #[test]
260    fn test_clear() {
261        let capturer = OutputCapturer::new();
262
263        capturer.capture_stdout("test");
264        capturer.capture_stderr("error");
265
266        assert!(!capturer.get_output().is_empty());
267
268        capturer.clear();
269
270        let output = capturer.get_output();
271        assert!(output.is_empty());
272    }
273
274    #[test]
275    fn test_with_capture() {
276        let capturer = OutputCapturer::new();
277
278        let result = capturer.with_capture(|| {
279            capturer.capture_stdout("captured\n");
280            42
281        });
282
283        assert_eq!(result, 42);
284        assert_eq!(capturer.get_output().stdout, "captured\n");
285    }
286
287    #[test]
288    fn test_with_capture_clears_previous() {
289        let capturer = OutputCapturer::new();
290
291        capturer.capture_stdout("old output\n");
292
293        capturer.with_capture(|| {
294            capturer.capture_stdout("new output\n");
295        });
296
297        let output = capturer.get_output();
298        assert_eq!(output.stdout, "new output\n");
299    }
300
301    #[test]
302    fn test_simulate_execution_println() {
303        let capturer = OutputCapturer::new();
304
305        let code = r#"println!("Hello from Oxur")"#;
306        simulate_execution(code, &capturer);
307
308        let output = capturer.get_output();
309        assert_eq!(output.stdout, "Hello from Oxur\n");
310        assert_eq!(output.stderr, "");
311    }
312
313    #[test]
314    fn test_simulate_execution_eprintln() {
315        let capturer = OutputCapturer::new();
316
317        let code = r#"eprintln!("Error message")"#;
318        simulate_execution(code, &capturer);
319
320        let output = capturer.get_output();
321        assert_eq!(output.stdout, "");
322        assert_eq!(output.stderr, "Error message\n");
323    }
324
325    #[test]
326    fn test_simulate_execution_both() {
327        let capturer = OutputCapturer::new();
328
329        let code = r#"println!("output"); eprintln!("error")"#;
330        simulate_execution(code, &capturer);
331
332        let output = capturer.get_output();
333        assert_eq!(output.stdout, "output\n");
334        assert_eq!(output.stderr, "error\n");
335    }
336
337    #[test]
338    fn test_simulate_execution_no_output() {
339        let capturer = OutputCapturer::new();
340
341        let code = "let x = 42;";
342        let result = simulate_execution(code, &capturer);
343
344        assert!(result.contains("executed"));
345        let output = capturer.get_output();
346        assert!(output.is_empty());
347    }
348
349    #[test]
350    fn test_thread_safety() {
351        use std::thread;
352
353        let capturer = Arc::new(OutputCapturer::new());
354        let mut handles = vec![];
355
356        for i in 0..10 {
357            let capturer_clone = Arc::clone(&capturer);
358            let handle = thread::spawn(move || {
359                capturer_clone.capture_stdout(&format!("Thread {}\n", i));
360            });
361            handles.push(handle);
362        }
363
364        for handle in handles {
365            handle.join().unwrap();
366        }
367
368        let output = capturer.get_output();
369        // Should have captured output from all 10 threads
370        assert_eq!(output.stdout.lines().count(), 10);
371    }
372}