1use std::path::PathBuf;
2use std::process::Command;
3use std::str;
4
5#[cfg(test)]
6use mockall::automock;
7
8use crate::config::Config;
9use crate::default_error::DefaultError;
10
11#[derive(Debug)]
12pub struct CommandOutput {
13 pub status: i32,
14 pub stdout: String,
15 pub stderr: String,
16}
17
18impl CommandOutput {
19 pub fn new(code: i32, stdout: String, stderr: String) -> Self {
20 CommandOutput { status: code, stdout, stderr }
21 }
22}
23
24impl Default for CommandOutput {
25 fn default() -> Self {
26 CommandOutput::new(0, "".to_string(), "".to_string())
27 }
28}
29
30#[cfg_attr(test, automock)]
31pub trait Shell {
32 fn execute(&self, command: &str, current_dir: &PathBuf) -> Result<CommandOutput, DefaultError>;
37
38 fn execute_in_repo(&self, command: &str) -> Result<CommandOutput, DefaultError>;
43
44 fn execute_interactive(&self, command: &str, current_dir: &PathBuf) -> Result<CommandOutput, DefaultError>;
49
50 fn execute_interactive_in_repo(&self, command: &str) -> Result<CommandOutput, DefaultError>;
55}
56
57#[derive(Clone)]
58pub struct ShellImpl<'a> {
59 config: &'a Config,
60 executor: fn(command: &str, current_dir: &PathBuf) -> Result<CommandOutput, DefaultError>,
61 interactive_executor: fn(command: &str, current_dir: &PathBuf) -> Result<CommandOutput, DefaultError>,
62}
63
64impl<'a> ShellImpl<'a> {
65 pub fn new(config: &'a Config) -> ShellImpl {
66 ShellImpl {
67 config,
68 executor: command,
69 interactive_executor: command_interactive,
70 }
71 }
72}
73
74impl<'a> Shell for ShellImpl<'a> {
75 fn execute(&self, command: &str, current_dir: &PathBuf) -> Result<CommandOutput, DefaultError> {
76 match (self.executor)(command, current_dir) {
77 Ok(o) if o.status == 0 => Ok(o),
78 Ok(o) if o.status != 0 => Err(DefaultError::new(format!(
79 "Command failed: '{}'\n\n\tExit code: {}\n\tstdout: {}\n\tstderr: {}",
80 command, o.status, o.stdout, o.stderr
81 ))),
82 Ok(_) => Err(DefaultError::new(String::from("Unexpected return value"))),
83 Err(e) => Err(e),
84 }
85 }
86
87 fn execute_in_repo(&self, command: &str) -> Result<CommandOutput, DefaultError> {
88 self.execute(command, &self.config.storage_directory)
89 }
90
91 fn execute_interactive(&self, command: &str, current_dir: &PathBuf) -> Result<CommandOutput, DefaultError> {
92 match (self.interactive_executor)(command, current_dir) {
93 Ok(o) if o.status == 0 => Ok(o),
94 Ok(o) if o.status != 0 => Err(DefaultError::new(format!("Command failed: '{}'\n\n\tExit code: {}\n", command, o.status))),
95 Ok(_) => Err(DefaultError::new(String::from("Unexpected return value"))),
96 Err(e) => Err(e),
97 }
98 }
99
100 fn execute_interactive_in_repo(&self, command: &str) -> Result<CommandOutput, DefaultError> {
101 self.execute_interactive(command, &self.config.storage_directory)
102 }
103}
104
105pub fn command(command: &str, current_dir: &PathBuf) -> Result<CommandOutput, DefaultError> {
110 let mut s_comm = Command::new("sh");
111 s_comm.args(&["-c", command]);
112 s_comm.current_dir(current_dir);
113
114 if let Ok(out) = s_comm.output() {
117 let stderr = str::from_utf8(&out.stderr[..]).unwrap_or_else(|_| "Bad stderr");
118 let stdout = str::from_utf8(&out.stdout[..]).unwrap_or_else(|_| "Bad stdout");
119 Ok(CommandOutput::new(
120 out.status.code().unwrap_or_else(|| -1),
121 String::from(stdout),
122 String::from(stderr),
123 ))
124 } else {
125 Err(DefaultError::new(format!("Cannot run command '{}'", command)))
126 }
127}
128
129pub fn command_interactive(command: &str, current_dir: &PathBuf) -> Result<CommandOutput, DefaultError> {
134 let mut s_comm = Command::new("sh");
135 s_comm.args(&["-c", command]);
136 s_comm.current_dir(current_dir);
137
138 if let Ok(out) = s_comm.status() {
141 Ok(CommandOutput::new(out.code().unwrap_or_else(|| -1), "".to_string(), "".to_string()))
142 } else {
143 Err(DefaultError::new(format!("Cannot run command '{}'", command)))
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 pub fn new_command_output() {
153 let res = CommandOutput::new(5, "out".to_string(), "err".to_string());
154 assert_eq!(res.status, 5);
155 assert_eq!(res.stdout, "out");
156 assert_eq!(res.stderr, "err");
157 }
158
159 #[test]
160 pub fn correct_command() {
161 let out = command("ls", &PathBuf::from("/")).unwrap();
162 assert_eq!(out.status, 0);
163 assert!(!out.stdout.is_empty());
164 assert!(out.stderr.is_empty());
165 }
166
167 #[test]
168 pub fn incorrect_command() {
169 let out = command("aaaaa", &PathBuf::from("/")).unwrap();
170 assert_ne!(out.status, 0);
171 assert!(out.stdout.is_empty());
172 assert!(!out.stderr.is_empty());
173 }
174
175 #[test]
176 pub fn interactive_correct_command() {
177 let out = command_interactive("ls", &PathBuf::from("/")).unwrap();
178 assert_eq!(out.status, 0);
179 assert!(out.stdout.is_empty());
180 assert!(out.stderr.is_empty());
181 }
182
183 #[test]
184 pub fn interactive_incorrect_command() {
185 let out = command_interactive("aaaaa", &PathBuf::from("/")).unwrap();
186 assert_ne!(out.status, 0);
187 assert!(out.stdout.is_empty());
188 assert!(out.stderr.is_empty());
189 }
190
191 #[test]
192 pub fn shell_impl_execute_correct_command() {
193 let config = Config {
194 storage_directory: PathBuf::from("/storage"),
195 template_path: PathBuf::from("/template.md"),
196 };
197 fn executor(c: &str, p: &PathBuf) -> Result<CommandOutput, DefaultError> {
198 assert_eq!(p, &PathBuf::from("/storage"));
199 assert_eq!(c, "test-command");
200 Ok(CommandOutput {
201 status: 0,
202 stderr: "err".to_string(),
203 stdout: "out".to_string(),
204 })
205 }
206 let shell = ShellImpl {
207 config: &config,
208 executor,
209 interactive_executor: command_interactive,
210 };
211
212 let res = shell.execute_in_repo("test-command").unwrap();
213 assert_eq!(res.status, 0);
214 assert_eq!(res.stderr, "err");
215 assert_eq!(res.stdout, "out");
216 }
217
218 #[test]
219 pub fn shell_impl_execute_bad_command() {
220 let config = Config {
221 storage_directory: PathBuf::from("/storage"),
222 template_path: PathBuf::from("/template.md"),
223 };
224 fn executor(c: &str, p: &PathBuf) -> Result<CommandOutput, DefaultError> {
225 assert_eq!(p, &PathBuf::from("/storage"));
226 assert_eq!(c, "test-command");
227 Ok(CommandOutput {
228 status: 1,
229 stderr: "err".to_string(),
230 stdout: "out".to_string(),
231 })
232 }
233 let shell = ShellImpl {
234 config: &config,
235 executor,
236 interactive_executor: command_interactive,
237 };
238
239 let res = shell.execute_in_repo("test-command").unwrap_err();
240 assert_eq!(res.message, "Command failed: \'test-command\'\n\n\tExit code: 1\n\tstdout: out\n\tstderr: err");
241 print!("{}", res.message);
242 }
243
244 #[test]
245 pub fn shell_impl_execute_error() {
246 let config = Config {
247 storage_directory: PathBuf::from("/storage"),
248 template_path: PathBuf::from("/template.md"),
249 };
250 fn executor(c: &str, p: &PathBuf) -> Result<CommandOutput, DefaultError> {
251 assert_eq!(p, &PathBuf::from("/storage"));
252 assert_eq!(c, "test-command");
253 Err(DefaultError::new("test error".to_string()))
254 }
255 let shell = ShellImpl {
256 config: &config,
257 executor,
258 interactive_executor: command_interactive,
259 };
260
261 let res = shell.execute_in_repo("test-command").unwrap_err();
262 assert_eq!(res.message, "test error");
263 }
264
265 #[test]
266 pub fn shell_impl_execute_interactive_correct_command() {
267 let config = Config {
268 storage_directory: PathBuf::from("/storage"),
269 template_path: PathBuf::from("/template.md"),
270 };
271 fn executor(c: &str, p: &PathBuf) -> Result<CommandOutput, DefaultError> {
272 assert_eq!(p, &PathBuf::from("/storage"));
273 assert_eq!(c, "test-command");
274 Ok(CommandOutput {
275 status: 0,
276 stderr: "".to_string(),
277 stdout: "".to_string(),
278 })
279 }
280 let shell = ShellImpl {
281 config: &config,
282 executor: command,
283 interactive_executor: executor,
284 };
285
286 let res = shell.execute_interactive_in_repo("test-command").unwrap();
287 assert_eq!(res.status, 0);
288 assert!(res.stderr.is_empty());
289 assert!(res.stdout.is_empty());
290 }
291
292 #[test]
293 pub fn shell_impl_execute_interactive_bad_command() {
294 let config = Config {
295 storage_directory: PathBuf::from("/storage"),
296 template_path: PathBuf::from("/template.md"),
297 };
298 fn executor(c: &str, p: &PathBuf) -> Result<CommandOutput, DefaultError> {
299 assert_eq!(p, &PathBuf::from("/storage"));
300 assert_eq!(c, "test-command");
301 Ok(CommandOutput {
302 status: 1,
303 stderr: "".to_string(),
304 stdout: "".to_string(),
305 })
306 }
307 let shell = ShellImpl {
308 config: &config,
309 executor: command,
310 interactive_executor: executor,
311 };
312
313 let res = shell.execute_interactive_in_repo("test-command").unwrap_err();
314 assert_eq!(res.message, "Command failed: \'test-command\'\n\n\tExit code: 1\n");
315 }
316
317 #[test]
318 pub fn shell_impl_execute_interactive_error() {
319 let config = Config {
320 storage_directory: PathBuf::from("/storage"),
321 template_path: PathBuf::from("/template.md"),
322 };
323 fn executor(c: &str, p: &PathBuf) -> Result<CommandOutput, DefaultError> {
324 assert_eq!(p, &PathBuf::from("/storage"));
325 assert_eq!(c, "test-command");
326 Err(DefaultError::new("test error".to_string()))
327 }
328 let shell = ShellImpl {
329 config: &config,
330 executor: command,
331 interactive_executor: executor,
332 };
333
334 let res = shell.execute_interactive_in_repo("test-command").unwrap_err();
335 assert_eq!(res.message, "test error");
336 }
337}