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