eggress_system_proxy/
command_runner.rs1use std::process::Output;
2
3pub trait CommandRunner {
8 fn run(&self, program: &str, args: &[&str]) -> Result<Output, std::io::Error>;
10}
11
12pub struct RealCommandRunner;
14
15impl CommandRunner for RealCommandRunner {
16 fn run(&self, program: &str, args: &[&str]) -> Result<Output, std::io::Error> {
17 std::process::Command::new(program).args(args).output()
18 }
19}
20
21pub struct MockCommandRunner {
23 responses: Vec<(String, Vec<String>, Result<Output, std::io::Error>)>,
24 calls: std::sync::Mutex<Vec<(String, Vec<String>)>>,
25}
26
27impl MockCommandRunner {
28 pub fn new() -> Self {
30 Self {
31 responses: Vec::new(),
32 calls: std::sync::Mutex::new(Vec::new()),
33 }
34 }
35
36 pub fn add_response(
38 mut self,
39 program: &str,
40 args: Vec<String>,
41 result: Result<Output, std::io::Error>,
42 ) -> Self {
43 self.responses.push((program.to_string(), args, result));
44 self
45 }
46
47 pub fn add_always(self, program: &str, result: Result<Output, std::io::Error>) -> Self {
49 self.add_response(program, Vec::new(), result)
50 }
51
52 pub fn calls(&self) -> Vec<(String, Vec<String>)> {
54 self.calls.lock().unwrap().clone()
55 }
56}
57
58impl Default for MockCommandRunner {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl CommandRunner for MockCommandRunner {
65 fn run(&self, program: &str, args: &[&str]) -> Result<Output, std::io::Error> {
66 let args_vec: Vec<String> = args.iter().map(|s| s.to_string()).collect();
67 self.calls
68 .lock()
69 .unwrap()
70 .push((program.to_string(), args_vec.clone()));
71
72 for (resp_prog, resp_args, resp_result) in &self.responses {
73 if resp_prog == program && (resp_args.is_empty() || resp_args == &args_vec) {
74 return match resp_result {
75 Ok(output) => Ok(Output {
76 status: output.status,
77 stdout: output.stdout.clone(),
78 stderr: output.stderr.clone(),
79 }),
80 Err(e) => Err(std::io::Error::new(e.kind(), e.to_string())),
81 };
82 }
83 }
84
85 Err(std::io::Error::new(
86 std::io::ErrorKind::NotFound,
87 format!("no mock response for {program}"),
88 ))
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 fn success_exit_status() -> std::process::ExitStatus {
97 #[cfg(unix)]
98 {
99 use std::os::unix::process::ExitStatusExt;
100 std::process::ExitStatus::from_raw(0)
101 }
102 #[cfg(not(unix))]
103 {
104 std::process::ExitStatus::default()
105 }
106 }
107
108 #[test]
109 fn mock_runner_returns_predefined_response() {
110 let runner = MockCommandRunner::new().add_always(
111 "echo",
112 Ok(Output {
113 status: success_exit_status(),
114 stdout: b"hello\n".to_vec(),
115 stderr: Vec::new(),
116 }),
117 );
118
119 let output = runner.run("echo", &["test"]).unwrap();
120 assert_eq!(output.stdout, b"hello\n");
121 }
122
123 #[test]
124 fn mock_runner_records_calls() {
125 let runner = MockCommandRunner::new().add_always(
126 "ls",
127 Ok(Output {
128 status: success_exit_status(),
129 stdout: Vec::new(),
130 stderr: Vec::new(),
131 }),
132 );
133
134 let _ = runner.run("ls", &["-la"]);
135 let _ = runner.run("ls", &["/tmp"]);
136
137 let calls = runner.calls();
138 assert_eq!(calls.len(), 2);
139 assert_eq!(calls[0].0, "ls");
140 assert_eq!(calls[1].1, vec!["/tmp".to_string()]);
141 }
142
143 #[test]
144 fn mock_runner_error_for_unknown_command() {
145 let runner = MockCommandRunner::new();
146 let result = runner.run("nonexistent", &[]);
147 assert!(result.is_err());
148 }
149
150 #[test]
151 fn real_runner_executes_command() {
152 let runner = RealCommandRunner;
153 let output = runner.run("echo", &["test"]).unwrap();
154 assert!(output.status.success());
155 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "test");
156 }
157}