1use std::{error::Error, process::Command};
2
3pub struct CmdOutput {
4 pub stdout: Vec<u8>,
5 pub stderr: Vec<u8>,
6 pub status_code: Option<i32>,
7}
8
9#[cfg(test)]
10use mockall::{automock, predicate::*};
11#[cfg_attr(test, automock)]
12pub trait CommandRunner {
13 #[allow(clippy::needless_lifetimes)] fn execute<'a>(&self, program: &str, args: &[&'a str]) -> Result<CmdOutput, Box<dyn Error>>;
15}
16
17pub struct StdCommandRunner;
18
19impl CommandRunner for StdCommandRunner {
20 fn execute(&self, program: &str, args: &[&str]) -> Result<CmdOutput, Box<dyn Error>> {
21 let output = Command::new(program).args(args).output()?;
22
23 Ok(CmdOutput {
24 stdout: output.stdout,
25 stderr: output.stderr,
26 status_code: output.status.code(),
27 })
28 }
29}
30
31#[cfg(test)]
32mod tests {
33 use super::*;
34
35 #[test]
36 fn test_execute_success() -> Result<(), Box<dyn Error>> {
37 let result = StdCommandRunner.execute("git", &["--version"])?;
38
39 assert_eq!(result.status_code, Some(0));
40 assert!(result.stdout.starts_with(b"git version "));
41 assert!(result.stderr.is_empty());
42
43 Ok(())
44 }
45
46 #[test]
47 fn test_execute_failure() -> Result<(), Box<dyn Error>> {
48 let result = StdCommandRunner.execute("git", &["--invalid_option"])?;
49
50 assert!(result.status_code.is_some_and(|x| x != 0));
51 assert!(result.stdout.is_empty());
52 assert!(result
53 .stderr
54 .starts_with(b"unknown option: --invalid_option"));
55
56 Ok(())
57 }
58
59 #[test]
60 fn test_execute_error() -> Result<(), Box<dyn Error>> {
61 let result = StdCommandRunner.execute("non_existent_program", &[]);
62
63 assert!(result.is_err());
64
65 Ok(())
66 }
67}