1use std::io;
2use std::process::{Command, Stdio};
3use std::sync::{Arc, OnceLock};
4
5#[cfg(test)]
6use std::collections::VecDeque;
7#[cfg(test)]
8use std::sync::Mutex;
9
10#[derive(Debug, Clone, Copy, Eq, PartialEq)]
11pub struct CommandStatus {
12 pub success: bool,
13 pub code: Option<i32>,
14}
15
16#[derive(Debug, Clone, Eq, PartialEq)]
17pub struct CommandOutput {
18 pub status: CommandStatus,
19 pub stdout: Vec<u8>,
20 pub stderr: Vec<u8>,
21}
22
23pub trait CommandRunner: Send + Sync {
24 fn run_capture(&self, program: &str, args: &[String]) -> io::Result<CommandOutput>;
25 fn run_capture_with_input(
26 &self,
27 program: &str,
28 args: &[String],
29 input: &str,
30 ) -> io::Result<CommandOutput>;
31 fn run_inherit(&self, program: &str, args: &[String]) -> io::Result<CommandStatus>;
32}
33
34pub fn default_runner() -> Arc<dyn CommandRunner> {
35 static RUNNER: OnceLock<Arc<dyn CommandRunner>> = OnceLock::new();
36 RUNNER.get_or_init(|| Arc::new(RealCommandRunner)).clone()
37}
38
39#[derive(Debug)]
40struct RealCommandRunner;
41
42impl CommandRunner for RealCommandRunner {
43 fn run_capture(&self, program: &str, args: &[String]) -> io::Result<CommandOutput> {
44 let output = Command::new(program).args(args).output()?;
45 Ok(CommandOutput {
46 status: CommandStatus {
47 success: output.status.success(),
48 code: output.status.code(),
49 },
50 stdout: output.stdout,
51 stderr: output.stderr,
52 })
53 }
54
55 fn run_capture_with_input(
56 &self,
57 program: &str,
58 args: &[String],
59 input: &str,
60 ) -> io::Result<CommandOutput> {
61 use std::io::Write;
62
63 let mut child = Command::new(program)
64 .args(args)
65 .stdin(Stdio::piped())
66 .stdout(Stdio::piped())
67 .stderr(Stdio::piped())
68 .spawn()?;
69
70 if let Some(mut stdin) = child.stdin.take() {
71 stdin.write_all(input.as_bytes())?;
72 }
73
74 let output = child.wait_with_output()?;
75 Ok(CommandOutput {
76 status: CommandStatus {
77 success: output.status.success(),
78 code: output.status.code(),
79 },
80 stdout: output.stdout,
81 stderr: output.stderr,
82 })
83 }
84
85 fn run_inherit(&self, program: &str, args: &[String]) -> io::Result<CommandStatus> {
86 let status = Command::new(program)
87 .args(args)
88 .stdin(Stdio::inherit())
89 .stdout(Stdio::inherit())
90 .stderr(Stdio::inherit())
91 .status()?;
92
93 Ok(CommandStatus {
94 success: status.success(),
95 code: status.code(),
96 })
97 }
98}
99
100#[cfg(test)]
101#[derive(Debug, Clone, Copy, Eq, PartialEq)]
102pub enum IoMode {
103 Capture,
104 Inherit,
105}
106
107#[cfg(test)]
108#[derive(Debug, Clone, Eq, PartialEq)]
109pub struct RecordedCommand {
110 pub program: String,
111 pub args: Vec<String>,
112 pub stdin: Option<String>,
113 pub io_mode: IoMode,
114}
115
116#[cfg(test)]
117#[derive(Debug)]
118enum PlannedResponse {
119 Capture(io::Result<CommandOutput>),
120 Inherit(io::Result<CommandStatus>),
121}
122
123#[cfg(test)]
124#[derive(Debug)]
125pub struct FakeCommandRunner {
126 planned: Mutex<VecDeque<PlannedResponse>>,
127 recorded: Mutex<Vec<RecordedCommand>>,
128}
129
130#[cfg(test)]
131impl FakeCommandRunner {
132 pub fn new() -> Self {
133 Self {
134 planned: Mutex::new(VecDeque::new()),
135 recorded: Mutex::new(Vec::new()),
136 }
137 }
138
139 pub fn push_capture(&self, result: io::Result<CommandOutput>) {
140 self.planned
141 .lock()
142 .expect("planned queue should lock")
143 .push_back(PlannedResponse::Capture(result));
144 }
145
146 pub fn push_inherit(&self, result: io::Result<CommandStatus>) {
147 self.planned
148 .lock()
149 .expect("planned queue should lock")
150 .push_back(PlannedResponse::Inherit(result));
151 }
152
153 pub fn recorded(&self) -> Vec<RecordedCommand> {
154 self.recorded
155 .lock()
156 .expect("recorded queue should lock")
157 .clone()
158 }
159}
160
161#[cfg(test)]
162impl Default for FakeCommandRunner {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168#[cfg(test)]
169impl CommandRunner for FakeCommandRunner {
170 fn run_capture(&self, program: &str, args: &[String]) -> io::Result<CommandOutput> {
171 self.recorded
172 .lock()
173 .expect("recorded queue should lock")
174 .push(RecordedCommand {
175 program: program.to_owned(),
176 args: args.to_vec(),
177 stdin: None,
178 io_mode: IoMode::Capture,
179 });
180
181 match self
182 .planned
183 .lock()
184 .expect("planned queue should lock")
185 .pop_front()
186 .expect("missing planned capture response")
187 {
188 PlannedResponse::Capture(result) => result,
189 PlannedResponse::Inherit(_) => {
190 panic!("expected capture response but inherit response was queued")
191 }
192 }
193 }
194
195 fn run_capture_with_input(
196 &self,
197 program: &str,
198 args: &[String],
199 input: &str,
200 ) -> io::Result<CommandOutput> {
201 self.recorded
202 .lock()
203 .expect("recorded queue should lock")
204 .push(RecordedCommand {
205 program: program.to_owned(),
206 args: args.to_vec(),
207 stdin: Some(input.to_owned()),
208 io_mode: IoMode::Capture,
209 });
210
211 match self
212 .planned
213 .lock()
214 .expect("planned queue should lock")
215 .pop_front()
216 .expect("missing planned capture-with-input response")
217 {
218 PlannedResponse::Capture(result) => result,
219 PlannedResponse::Inherit(_) => {
220 panic!("expected capture response but inherit response was queued")
221 }
222 }
223 }
224
225 fn run_inherit(&self, program: &str, args: &[String]) -> io::Result<CommandStatus> {
226 self.recorded
227 .lock()
228 .expect("recorded queue should lock")
229 .push(RecordedCommand {
230 program: program.to_owned(),
231 args: args.to_vec(),
232 stdin: None,
233 io_mode: IoMode::Inherit,
234 });
235
236 match self
237 .planned
238 .lock()
239 .expect("planned queue should lock")
240 .pop_front()
241 .expect("missing planned inherit response")
242 {
243 PlannedResponse::Inherit(result) => result,
244 PlannedResponse::Capture(_) => {
245 panic!("expected inherit response but capture response was queued")
246 }
247 }
248 }
249}