notes/
shell.rs

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    /// Execute specified command in user shell, and capture outputs
33    /// If command succeed, return a CommandOutput
34    /// If command fail, return an error
35    /// If command cannot be run, return an error
36    fn execute(&self, command: &str, current_dir: &PathBuf) -> Result<CommandOutput, DefaultError>;
37
38    /// Execute specified command in user shell, and capture outputs
39    /// If command succeed, return a CommandOutput
40    /// If command fail, return an error
41    /// If command cannot be run, return an error
42    fn execute_in_repo(&self, command: &str) -> Result<CommandOutput, DefaultError>;
43
44    /// Execute specified command in user shell, and capture outputs
45    /// If command succeed, return a CommandOutput
46    /// If command fail, return an error
47    /// If command cannot be run, return an error
48    fn execute_interactive(&self, command: &str, current_dir: &PathBuf) -> Result<CommandOutput, DefaultError>;
49
50    /// Execute specified command in user shell, and capture outputs
51    /// If command succeed, return a CommandOutput
52    /// If command fail, return an error
53    /// If command cannot be run, return an error
54    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
105/// Execute specified command in user shell, and capture outputs
106/// If command succeed, return a CommandOutput
107/// If command fail, return a CommandOutput
108/// If command cannot be run, return an error
109pub 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    // println!("{}", command);
115
116    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
129/// Execute specified command in user shell
130/// If command succeed, return a CommandOutput
131/// If command fail, return a CommandOutput
132/// If command cannot be run, return an error
133pub 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    // println!("{}", command);
139
140    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}