1use std::process::Command;
2
3use crate::error::Error;
4
5pub struct CommandOutput {
10 pub stdout: Vec<u8>,
12
13 pub stderr: Vec<u8>,
15
16 pub exit_code: i32,
18}
19
20impl CommandOutput {
21 pub fn merged(&self) -> Vec<u8> {
23 let mut out = self.stdout.clone();
24 out.extend_from_slice(&self.stderr);
25 out
26 }
27
28 pub fn merged_lossy(&self) -> String {
30 String::from_utf8_lossy(&self.merged()).into_owned()
31 }
32}
33
34pub fn run(args: &[String]) -> Result<CommandOutput, Error> {
61 let output = Command::new(&args[0]).args(&args[1..]).output()?;
62
63 let exit_code = output.status.code().unwrap_or(128);
64
65 Ok(CommandOutput {
66 stdout: output.stdout,
67 stderr: output.stderr,
68 exit_code,
69 })
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn test_successful_command() {
78 let result = run(&["echo".into(), "hello".into()]).unwrap();
79 assert_eq!(result.exit_code, 0);
80 assert_eq!(String::from_utf8_lossy(&result.stdout), "hello\n");
81 }
82
83 #[test]
84 fn test_failing_command() {
85 let result = run(&["false".into()]).unwrap();
86 assert_ne!(result.exit_code, 0);
87 }
88
89 #[test]
90 fn test_stderr_captured() {
91 let result = run(&["sh".into(), "-c".into(), "echo err >&2".into()]).unwrap();
92 assert_eq!(result.exit_code, 0);
93 assert_eq!(String::from_utf8_lossy(&result.stderr), "err\n");
94 }
95
96 #[test]
97 fn test_exit_code_preserved() {
98 let result = run(&["sh".into(), "-c".into(), "exit 42".into()]).unwrap();
99 assert_eq!(result.exit_code, 42);
100 }
101
102 #[test]
103 fn test_merged_output() {
104 let result = run(&["sh".into(), "-c".into(), "echo out; echo err >&2".into()]).unwrap();
105 let merged = result.merged_lossy();
106 assert!(merged.contains("out"));
107 assert!(merged.contains("err"));
108 }
109}