Skip to main content

verificar/oracle/
executor.rs

1//! Code execution backends for verification
2//!
3//! Provides executors for running code in different languages
4//! with proper I/O capture and timeout handling.
5
6use std::io::Write;
7use std::process::{Command, Stdio};
8use std::time::{Duration, Instant};
9
10use crate::{Error, Language, Result};
11
12use super::ExecutionResult;
13
14/// Code executor trait for running programs
15pub trait Executor: Send + Sync {
16    /// Execute code with the given input
17    ///
18    /// # Errors
19    ///
20    /// Returns an error if execution fails
21    fn execute(&self, code: &str, input: &str, timeout_ms: u64) -> Result<ExecutionResult>;
22
23    /// Get the language this executor handles
24    fn language(&self) -> Language;
25}
26
27/// Python code executor using system Python interpreter
28#[derive(Debug, Default)]
29pub struct PythonExecutor {
30    /// Path to Python interpreter (default: "python3")
31    interpreter: String,
32}
33
34impl PythonExecutor {
35    /// Create a new Python executor with default interpreter
36    #[must_use]
37    pub fn new() -> Self {
38        Self {
39            interpreter: "python3".to_string(),
40        }
41    }
42
43    /// Create a Python executor with custom interpreter path
44    #[must_use]
45    pub fn with_interpreter(interpreter: impl Into<String>) -> Self {
46        Self {
47            interpreter: interpreter.into(),
48        }
49    }
50
51    /// Check if Python is available
52    #[must_use]
53    pub fn is_available(&self) -> bool {
54        Command::new(&self.interpreter)
55            .arg("--version")
56            .stdout(Stdio::null())
57            .stderr(Stdio::null())
58            .status()
59            .is_ok()
60    }
61}
62
63impl Executor for PythonExecutor {
64    fn execute(&self, code: &str, input: &str, timeout_ms: u64) -> Result<ExecutionResult> {
65        use std::sync::atomic::{AtomicU64, Ordering};
66        static COUNTER: AtomicU64 = AtomicU64::new(0);
67
68        let start = Instant::now();
69
70        // Create a temporary file for the code with unique name
71        let temp_dir = std::env::temp_dir();
72        let unique_id = COUNTER.fetch_add(1, Ordering::SeqCst);
73        let temp_file = temp_dir.join(format!("verificar_{}_{}.py", std::process::id(), unique_id));
74
75        std::fs::write(&temp_file, code)
76            .map_err(|e| Error::Verification(format!("Failed to write temp file: {e}")))?;
77
78        let mut cmd = Command::new(&self.interpreter);
79        cmd.arg(&temp_file)
80            .stdin(Stdio::piped())
81            .stdout(Stdio::piped())
82            .stderr(Stdio::piped());
83
84        // Spawn process
85        let mut child = cmd
86            .spawn()
87            .map_err(|e| Error::Verification(format!("Failed to spawn Python: {e}")))?;
88
89        // Write input to stdin
90        if let Some(mut stdin) = child.stdin.take() {
91            let _ = stdin.write_all(input.as_bytes());
92        }
93
94        // Wait with timeout
95        let timeout = Duration::from_millis(timeout_ms);
96        let output = match wait_with_timeout(child, timeout) {
97            Ok(output) => output,
98            Err(e) => {
99                let _ = std::fs::remove_file(&temp_file);
100                return Err(e);
101            }
102        };
103
104        let _ = std::fs::remove_file(&temp_file);
105
106        let duration_ms = start.elapsed().as_millis() as u64;
107
108        Ok(ExecutionResult {
109            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
110            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
111            exit_code: output.status.code().unwrap_or(-1),
112            duration_ms,
113        })
114    }
115
116    fn language(&self) -> Language {
117        Language::Python
118    }
119}
120
121/// Rust code executor using cargo/rustc
122#[derive(Debug, Default)]
123pub struct RustExecutor {
124    /// Path to rustc (default: "rustc")
125    compiler: String,
126}
127
128impl RustExecutor {
129    /// Create a new Rust executor
130    #[must_use]
131    pub fn new() -> Self {
132        Self {
133            compiler: "rustc".to_string(),
134        }
135    }
136
137    /// Check if rustc is available
138    #[must_use]
139    pub fn is_available(&self) -> bool {
140        Command::new(&self.compiler)
141            .arg("--version")
142            .stdout(Stdio::null())
143            .stderr(Stdio::null())
144            .status()
145            .is_ok()
146    }
147}
148
149impl Executor for RustExecutor {
150    fn execute(&self, code: &str, input: &str, timeout_ms: u64) -> Result<ExecutionResult> {
151        use std::sync::atomic::{AtomicU64, Ordering};
152        static COUNTER: AtomicU64 = AtomicU64::new(0);
153
154        let start = Instant::now();
155
156        // Create temp directory for compilation with unique names
157        let temp_dir = std::env::temp_dir();
158        let unique_id = COUNTER.fetch_add(1, Ordering::SeqCst);
159        let source_file =
160            temp_dir.join(format!("verificar_{}_{}.rs", std::process::id(), unique_id));
161        let binary_file = temp_dir.join(format!("verificar_{}_{}", std::process::id(), unique_id));
162
163        std::fs::write(&source_file, code)
164            .map_err(|e| Error::Verification(format!("Failed to write temp file: {e}")))?;
165
166        let compile_output = Command::new(&self.compiler)
167            .arg(&source_file)
168            .arg("-o")
169            .arg(&binary_file)
170            .output()
171            .map_err(|e| Error::Verification(format!("Failed to compile: {e}")))?;
172
173        if !compile_output.status.success() {
174            let _ = std::fs::remove_file(&source_file);
175            return Ok(ExecutionResult {
176                stdout: String::new(),
177                stderr: String::from_utf8_lossy(&compile_output.stderr).to_string(),
178                exit_code: compile_output.status.code().unwrap_or(-1),
179                duration_ms: start.elapsed().as_millis() as u64,
180            });
181        }
182
183        // Run binary
184        let mut cmd = Command::new(&binary_file);
185        cmd.stdin(Stdio::piped())
186            .stdout(Stdio::piped())
187            .stderr(Stdio::piped());
188
189        let mut child = cmd
190            .spawn()
191            .map_err(|e| Error::Verification(format!("Failed to run binary: {e}")))?;
192
193        // Write input
194        if let Some(mut stdin) = child.stdin.take() {
195            let _ = stdin.write_all(input.as_bytes());
196        }
197
198        // Wait with timeout
199        let timeout = Duration::from_millis(timeout_ms);
200        let output = match wait_with_timeout(child, timeout) {
201            Ok(output) => output,
202            Err(e) => {
203                let _ = std::fs::remove_file(&source_file);
204                let _ = std::fs::remove_file(&binary_file);
205                return Err(e);
206            }
207        };
208
209        // Cleanup
210        let _ = std::fs::remove_file(&source_file);
211        let _ = std::fs::remove_file(&binary_file);
212
213        Ok(ExecutionResult {
214            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
215            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
216            exit_code: output.status.code().unwrap_or(-1),
217            duration_ms: start.elapsed().as_millis() as u64,
218        })
219    }
220
221    fn language(&self) -> Language {
222        Language::Rust
223    }
224}
225
226/// Wait for a process with timeout using threaded output capture
227fn wait_with_timeout(
228    mut child: std::process::Child,
229    timeout: Duration,
230) -> Result<std::process::Output> {
231    use std::io::Read;
232    use std::sync::mpsc;
233    use std::thread;
234
235    // Take stdout and stderr handles before spawning threads
236    let stdout_handle = child.stdout.take();
237    let stderr_handle = child.stderr.take();
238
239    // Spawn threads to read stdout and stderr concurrently
240    let stdout_thread = thread::spawn(move || {
241        let mut buf = Vec::new();
242        if let Some(mut stdout) = stdout_handle {
243            let _ = stdout.read_to_end(&mut buf);
244        }
245        buf
246    });
247
248    let stderr_thread = thread::spawn(move || {
249        let mut buf = Vec::new();
250        if let Some(mut stderr) = stderr_handle {
251            let _ = stderr.read_to_end(&mut buf);
252        }
253        buf
254    });
255
256    // Wait for process with timeout using a channel
257    let (tx, rx) = mpsc::channel();
258    let wait_thread = thread::spawn(move || {
259        let result = child.wait();
260        let _ = tx.send(result);
261        child
262    });
263
264    // Wait with timeout
265    match rx.recv_timeout(timeout) {
266        Ok(Ok(status)) => {
267            // Process finished, collect output
268            // The wait_thread returns the child after wait() completes
269            // We join the thread and let the child go out of scope (already waited)
270            let _ = wait_thread.join();
271
272            let stdout = stdout_thread.join().unwrap_or_default();
273            let stderr = stderr_thread.join().unwrap_or_default();
274
275            Ok(std::process::Output {
276                status,
277                stdout,
278                stderr,
279            })
280        }
281        Ok(Err(e)) => {
282            let _ = wait_thread.join();
283            let _ = stdout_thread.join();
284            let _ = stderr_thread.join();
285            Err(Error::Verification(format!("Wait error: {e}")))
286        }
287        Err(mpsc::RecvTimeoutError::Timeout) => {
288            // Timeout - kill the process and wait to avoid zombie
289            if let Ok(mut child) = wait_thread.join() {
290                let _ = child.kill();
291                let _ = child.wait(); // Reap the zombie process
292            }
293            // Still collect any output that was produced
294            let _ = stdout_thread.join();
295            let _ = stderr_thread.join();
296            Err(Error::Verification("Execution timed out".to_string()))
297        }
298        Err(mpsc::RecvTimeoutError::Disconnected) => {
299            let _ = wait_thread.join();
300            let _ = stdout_thread.join();
301            let _ = stderr_thread.join();
302            Err(Error::Verification(
303                "Process wait thread disconnected".to_string(),
304            ))
305        }
306    }
307}
308
309/// Get an executor for the specified language
310#[must_use]
311pub fn executor_for(language: Language) -> Option<Box<dyn Executor>> {
312    match language {
313        Language::Python => Some(Box::new(PythonExecutor::new())),
314        Language::Rust => Some(Box::new(RustExecutor::new())),
315        _ => None,
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn test_python_executor_simple() {
325        let executor = PythonExecutor::new();
326        if !executor.is_available() {
327            eprintln!("Python not available, skipping test");
328            return;
329        }
330
331        let result = executor
332            .execute("print('hello')", "", 5000)
333            .expect("execution should succeed");
334
335        assert_eq!(result.stdout.trim(), "hello");
336        assert_eq!(result.exit_code, 0);
337    }
338
339    #[test]
340    fn test_python_executor_with_input() {
341        let executor = PythonExecutor::new();
342        if !executor.is_available() {
343            eprintln!("Python not available, skipping test");
344            return;
345        }
346
347        let code = "x = input()\nprint(f'got: {x}')";
348        let result = executor
349            .execute(code, "test", 5000)
350            .expect("execution should succeed");
351
352        assert_eq!(result.stdout.trim(), "got: test");
353    }
354
355    #[test]
356    fn test_python_executor_error() {
357        let executor = PythonExecutor::new();
358        if !executor.is_available() {
359            eprintln!("Python not available, skipping test");
360            return;
361        }
362
363        let result = executor
364            .execute("raise ValueError('oops')", "", 5000)
365            .expect("execution should succeed");
366
367        assert_ne!(result.exit_code, 0);
368        assert!(result.stderr.contains("ValueError"));
369    }
370
371    #[test]
372    fn test_python_executor_arithmetic() {
373        let executor = PythonExecutor::new();
374        if !executor.is_available() {
375            eprintln!("Python not available, skipping test");
376            return;
377        }
378
379        let code = "print(1 + 2 * 3)";
380        let result = executor
381            .execute(code, "", 5000)
382            .expect("execution should succeed");
383
384        assert_eq!(result.stdout.trim(), "7");
385    }
386
387    #[test]
388    fn test_executor_for_python() {
389        let executor = executor_for(Language::Python);
390        assert!(executor.is_some());
391        assert_eq!(executor.unwrap().language(), Language::Python);
392    }
393
394    #[test]
395    fn test_executor_for_rust() {
396        let executor = executor_for(Language::Rust);
397        assert!(executor.is_some());
398        assert_eq!(executor.unwrap().language(), Language::Rust);
399    }
400
401    #[test]
402    fn test_executor_for_unsupported() {
403        let executor = executor_for(Language::Bash);
404        assert!(executor.is_none());
405    }
406
407    #[test]
408    fn test_python_executor_with_interpreter() {
409        let executor = PythonExecutor::with_interpreter("python3");
410        assert_eq!(executor.interpreter, "python3");
411    }
412
413    #[test]
414    fn test_python_executor_default() {
415        let executor = PythonExecutor::default();
416        assert!(executor.interpreter.is_empty() || executor.interpreter == "python3");
417    }
418
419    #[test]
420    fn test_python_executor_language() {
421        let executor = PythonExecutor::new();
422        assert_eq!(executor.language(), Language::Python);
423    }
424
425    #[test]
426    fn test_python_executor_debug() {
427        let executor = PythonExecutor::new();
428        let debug = format!("{:?}", executor);
429        assert!(debug.contains("PythonExecutor"));
430    }
431
432    #[test]
433    fn test_rust_executor_new() {
434        let executor = RustExecutor::new();
435        assert_eq!(executor.compiler, "rustc");
436    }
437
438    #[test]
439    fn test_rust_executor_default() {
440        let executor = RustExecutor::default();
441        assert!(executor.compiler.is_empty() || executor.compiler == "rustc");
442    }
443
444    #[test]
445    fn test_rust_executor_language() {
446        let executor = RustExecutor::new();
447        assert_eq!(executor.language(), Language::Rust);
448    }
449
450    #[test]
451    fn test_rust_executor_debug() {
452        let executor = RustExecutor::new();
453        let debug = format!("{:?}", executor);
454        assert!(debug.contains("RustExecutor"));
455    }
456
457    #[test]
458    fn test_rust_executor_is_available() {
459        let executor = RustExecutor::new();
460        // This may or may not be available depending on the system
461        let _ = executor.is_available();
462    }
463
464    #[test]
465    fn test_rust_executor_simple() {
466        let executor = RustExecutor::new();
467        if !executor.is_available() {
468            eprintln!("rustc not available, skipping test");
469            return;
470        }
471
472        let code = r#"fn main() { println!("hello from rust"); }"#;
473        let result = executor
474            .execute(code, "", 10000)
475            .expect("execution should succeed");
476
477        assert_eq!(result.stdout.trim(), "hello from rust");
478        assert_eq!(result.exit_code, 0);
479    }
480
481    #[test]
482    fn test_rust_executor_compile_error() {
483        let executor = RustExecutor::new();
484        if !executor.is_available() {
485            eprintln!("rustc not available, skipping test");
486            return;
487        }
488
489        let code = "fn main() { invalid syntax }";
490        let result = executor
491            .execute(code, "", 10000)
492            .expect("execution should return compile error");
493
494        assert_ne!(result.exit_code, 0);
495        assert!(!result.stderr.is_empty());
496    }
497
498    #[test]
499    fn test_rust_executor_with_input() {
500        let executor = RustExecutor::new();
501        if !executor.is_available() {
502            eprintln!("rustc not available, skipping test");
503            return;
504        }
505
506        let code = r#"
507use std::io::{self, BufRead};
508fn main() {
509    let stdin = io::stdin();
510    let line = stdin.lock().lines().next().unwrap().unwrap();
511    println!("got: {}", line);
512}
513"#;
514        let result = executor
515            .execute(code, "test input", 10000)
516            .expect("execution should succeed");
517
518        assert!(result.stdout.contains("got: test input"));
519    }
520
521    #[test]
522    fn test_python_executor_timeout() {
523        let executor = PythonExecutor::new();
524        if !executor.is_available() {
525            eprintln!("Python not available, skipping test");
526            return;
527        }
528
529        // Code that takes too long
530        let code = "import time; time.sleep(10)";
531        let result = executor.execute(code, "", 100); // 100ms timeout
532
533        assert!(result.is_err());
534        let err = result.unwrap_err();
535        let err_str = err.to_string();
536        assert!(err_str.contains("timeout") || err_str.contains("timed out"));
537    }
538
539    #[test]
540    fn test_python_executor_multiple_lines_output() {
541        let executor = PythonExecutor::new();
542        if !executor.is_available() {
543            eprintln!("Python not available, skipping test");
544            return;
545        }
546
547        let code = "for i in range(3): print(i)";
548        let result = executor
549            .execute(code, "", 5000)
550            .expect("execution should succeed");
551
552        assert!(result.stdout.contains("0"));
553        assert!(result.stdout.contains("1"));
554        assert!(result.stdout.contains("2"));
555    }
556
557    #[test]
558    fn test_python_executor_syntax_error() {
559        let executor = PythonExecutor::new();
560        if !executor.is_available() {
561            eprintln!("Python not available, skipping test");
562            return;
563        }
564
565        let code = "def f(: pass"; // syntax error
566        let result = executor
567            .execute(code, "", 5000)
568            .expect("execution should succeed");
569
570        assert_ne!(result.exit_code, 0);
571        assert!(result.stderr.contains("SyntaxError"));
572    }
573}