1use std::{process::Command, thread::sleep, time::Duration};
2
3use crate::models::FileInfo;
4
5use eyre::{Context, Result, eyre};
6
7pub trait CommandExecutor {
8 fn execute_command(&self, work_dir: &FileInfo, command: &str) -> Result<()>;
9}
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
12pub struct MockCommandExecutor;
13
14impl CommandExecutor for MockCommandExecutor {
15 fn execute_command(&self, _work_dir: &FileInfo, _command: &str) -> Result<()> {
16 sleep(Duration::from_secs(2));
17 Ok(())
18 }
19}
20
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
22pub struct RealCommandExecutor;
23
24impl CommandExecutor for RealCommandExecutor {
25 fn execute_command(&self, work_dir: &FileInfo, command: &str) -> Result<()> {
26 let mut parts = command.split_ascii_whitespace();
27 let program = parts
28 .next()
29 .ok_or_else(|| eyre!("refusing to run an empty command"))?;
30
31 let status = Command::new(program)
32 .current_dir(&work_dir.path)
33 .args(parts)
34 .status()
35 .with_context(|| format!("failed to spawn `{command}`"))?;
36
37 if status.success() {
38 Ok(())
39 } else {
40 Err(eyre!("`{command}` failed: {status}"))
41 }
42 }
43}
44
45#[cfg(test)]
46mod tests;