Skip to main content

double_o/
exec.rs

1use std::process::Command;
2use std::time::{Duration, Instant};
3
4use crate::error::Error;
5
6#[allow(dead_code)]
7pub struct CommandOutput {
8    pub stdout: Vec<u8>,
9    pub stderr: Vec<u8>,
10    pub exit_code: i32,
11    pub duration: Duration,
12}
13
14impl CommandOutput {
15    /// Merged output: stdout followed by stderr.
16    pub fn merged(&self) -> Vec<u8> {
17        let mut out = self.stdout.clone();
18        out.extend_from_slice(&self.stderr);
19        out
20    }
21
22    /// Merged output as a lossy UTF-8 string.
23    pub fn merged_lossy(&self) -> String {
24        String::from_utf8_lossy(&self.merged()).into_owned()
25    }
26}
27
28pub fn run(args: &[String]) -> Result<CommandOutput, Error> {
29    let start = Instant::now();
30
31    let output = Command::new(&args[0]).args(&args[1..]).output()?;
32
33    let duration = start.elapsed();
34    let exit_code = output.status.code().unwrap_or(128);
35
36    Ok(CommandOutput {
37        stdout: output.stdout,
38        stderr: output.stderr,
39        exit_code,
40        duration,
41    })
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn test_successful_command() {
50        let result = run(&["echo".into(), "hello".into()]).unwrap();
51        assert_eq!(result.exit_code, 0);
52        assert_eq!(String::from_utf8_lossy(&result.stdout), "hello\n");
53    }
54
55    #[test]
56    fn test_failing_command() {
57        let result = run(&["false".into()]).unwrap();
58        assert_ne!(result.exit_code, 0);
59    }
60
61    #[test]
62    fn test_stderr_captured() {
63        let result = run(&["sh".into(), "-c".into(), "echo err >&2".into()]).unwrap();
64        assert_eq!(result.exit_code, 0);
65        assert_eq!(String::from_utf8_lossy(&result.stderr), "err\n");
66    }
67
68    #[test]
69    fn test_exit_code_preserved() {
70        let result = run(&["sh".into(), "-c".into(), "exit 42".into()]).unwrap();
71        assert_eq!(result.exit_code, 42);
72    }
73
74    #[test]
75    fn test_merged_output() {
76        let result = run(&["sh".into(), "-c".into(), "echo out; echo err >&2".into()]).unwrap();
77        let merged = result.merged_lossy();
78        assert!(merged.contains("out"));
79        assert!(merged.contains("err"));
80    }
81
82    #[test]
83    fn test_duration_measured() {
84        let result = run(&["sleep".into(), "0.01".into()]).unwrap();
85        assert!(result.duration.as_millis() >= 5);
86    }
87}