turtle_build/infrastructure/command_runner/
os.rs1use crate::infrastructure::{CommandError, CommandRunner};
2use async_trait::async_trait;
3use std::{
4 io,
5 process::{ExitStatus, Output, Stdio},
6};
7use tokio::{process::Command, sync::Semaphore};
8
9#[cfg(any(windows, test))]
10const BLANK_CHARACTERS: [char; 2] = [' ', '\t'];
11
12#[derive(Debug)]
14pub struct OsCommandRunner {
15 semaphore: Semaphore,
16}
17
18impl OsCommandRunner {
19 pub fn new(job_limit: usize) -> Self {
21 Self {
22 semaphore: Semaphore::new(job_limit),
23 }
24 }
25
26 fn create_command(command: &str) -> Command {
27 cfg_select! {
28 windows => {
29 let (program, arguments) = split_program(command);
30 let mut process = Command::new(program);
31
32 process.raw_arg(arguments);
33 process
34 }
35 _ => {
36 let mut process = Command::new("sh");
37
38 process.arg("-ec").arg(command);
39 process
40 }
41 }
42 }
43
44 fn error(error: io::Error, process: &Command) -> CommandError {
45 CommandError::new(format!(
46 "{}: {}",
47 error,
48 process.as_std().get_program().display()
49 ))
50 }
51}
52
53#[async_trait]
54impl CommandRunner for OsCommandRunner {
55 async fn run(&self, command: &str) -> Result<Output, CommandError> {
56 let _permit = self.semaphore.acquire().await?;
57 let mut process = Self::create_command(command);
58
59 process
60 .stdin(Stdio::null())
61 .output()
62 .await
63 .map_err(|error| Self::error(error, &process))
64 }
65
66 async fn run_with_console(&self, command: &str) -> Result<ExitStatus, CommandError> {
67 let _permit = self.semaphore.acquire().await?;
68 let mut process = Self::create_command(command);
69
70 process
72 .status()
73 .await
74 .map_err(|error| Self::error(error, &process))
75 }
76}
77
78#[cfg(any(windows, test))]
79fn split_program(command: &str) -> (String, &str) {
80 let command = command.trim_start_matches(BLANK_CHARACTERS);
81 let (program, arguments) = command.split_at(
82 command
83 .char_indices()
84 .scan(false, |quoted, (index, character)| {
85 *quoted ^= character == '"';
86
87 Some((index, !*quoted && BLANK_CHARACTERS.contains(&character)))
88 })
89 .find(|&(_, end)| end)
90 .map_or(command.len(), |(index, _)| index),
91 );
92
93 (
94 program.replace('"', ""),
95 arguments.trim_start_matches(BLANK_CHARACTERS),
96 )
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 mod run {
104 use super::*;
105 use pretty_assertions::assert_eq;
106
107 #[tokio::test]
108 async fn run_command() {
109 let output = OsCommandRunner::new(1)
110 .run(cfg_select! {
111 windows => "cmd /c echo foo",
112 _ => "echo foo",
113 })
114 .await
115 .unwrap();
116
117 assert!(output.status.success());
118 assert_eq!(String::from_utf8(output.stdout).unwrap().trim_end(), "foo");
119 }
120
121 #[tokio::test]
122 async fn run_command_with_quoted_argument() {
123 let output = OsCommandRunner::new(1)
124 .run(cfg_select! {
125 windows => "cmd /c \"echo foo bar\"",
126 _ => "echo 'foo bar'",
127 })
128 .await
129 .unwrap();
130
131 assert!(output.status.success());
132 assert_eq!(
133 String::from_utf8(output.stdout).unwrap().trim_end(),
134 "foo bar"
135 );
136 }
137
138 #[tokio::test]
139 async fn run_failing_command() {
140 assert_eq!(
141 OsCommandRunner::new(1)
142 .run(cfg_select! {
143 windows => "cmd /c exit 42",
144 _ => "exit 42",
145 })
146 .await
147 .unwrap()
148 .status
149 .code(),
150 Some(42)
151 );
152 }
153
154 #[cfg(windows)]
155 #[tokio::test]
156 async fn fail_to_run_missing_program() {
157 assert!(
158 OsCommandRunner::new(1)
159 .run("missing-program foo")
160 .await
161 .unwrap_err()
162 .to_string()
163 .ends_with(": missing-program")
164 );
165 }
166 }
167
168 mod run_with_console {
169 use super::*;
170 use pretty_assertions::assert_eq;
171
172 #[tokio::test]
173 async fn run_command() {
174 assert!(
175 OsCommandRunner::new(1)
176 .run_with_console(cfg_select! {
177 windows => "cmd /c exit 0",
178 _ => "exit 0",
179 })
180 .await
181 .unwrap()
182 .success()
183 );
184 }
185
186 #[tokio::test]
187 async fn run_failing_command() {
188 assert_eq!(
189 OsCommandRunner::new(1)
190 .run_with_console(cfg_select! {
191 windows => "cmd /c exit 42",
192 _ => "exit 42",
193 })
194 .await
195 .unwrap()
196 .code(),
197 Some(42)
198 );
199 }
200
201 #[cfg(windows)]
202 #[tokio::test]
203 async fn fail_to_run_missing_program() {
204 assert!(
205 OsCommandRunner::new(1)
206 .run_with_console("missing-program foo")
207 .await
208 .unwrap_err()
209 .to_string()
210 .ends_with(": missing-program")
211 );
212 }
213 }
214
215 mod split_program {
216 use super::*;
217 use pretty_assertions::assert_eq;
218
219 #[test]
220 fn split_no_argument() {
221 assert_eq!(split_program("foo"), ("foo".into(), ""));
222 }
223
224 #[test]
225 fn split_arguments() {
226 assert_eq!(split_program("foo bar baz"), ("foo".into(), "bar baz"));
227 assert_eq!(split_program("foo\tbar"), ("foo".into(), "bar"));
228 assert_eq!(split_program("foo bar baz "), ("foo".into(), "bar baz "));
229 }
230
231 #[test]
232 fn split_quoted_arguments() {
233 assert_eq!(
234 split_program("foo \"bar baz\" \\\"qux"),
235 ("foo".into(), "\"bar baz\" \\\"qux")
236 );
237 }
238
239 #[test]
240 fn split_leading_blanks() {
241 assert_eq!(split_program(" \tfoo bar"), ("foo".into(), "bar"));
242 }
243
244 #[test]
245 fn split_quoted_program() {
246 assert_eq!(
247 split_program("\"C:\\Program Files\\foo.exe\" bar"),
248 ("C:\\Program Files\\foo.exe".into(), "bar")
249 );
250 assert_eq!(split_program("\"foo bar\""), ("foo bar".into(), ""));
251 }
252
253 #[test]
254 fn split_partially_quoted_program() {
255 assert_eq!(
256 split_program("\"C:\\Program Files\"\\foo.exe bar"),
257 ("C:\\Program Files\\foo.exe".into(), "bar")
258 );
259 }
260
261 #[test]
262 fn split_program_with_unclosed_quote() {
263 assert_eq!(split_program("\"foo bar"), ("foo bar".into(), ""));
264 }
265
266 #[test]
267 fn split_empty_command() {
268 assert_eq!(split_program(""), ("".into(), ""));
269 assert_eq!(split_program(" "), ("".into(), ""));
270 }
271 }
272}