use std::path::Path;
use anyhow::{anyhow, Context, Result};
#[derive(Debug, Clone)]
pub struct CmdOutput {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
pub trait CmdRunner: Send + Sync {
fn run(&self, program: &str, args: &[&str], cwd: &Path) -> Result<CmdOutput>;
}
pub struct RealRunner;
impl CmdRunner for RealRunner {
fn run(&self, program: &str, args: &[&str], cwd: &Path) -> Result<CmdOutput> {
let out = std::process::Command::new(program)
.args(args)
.current_dir(cwd)
.output()
.with_context(|| format!("invoke `{program}`"))?;
let stdout = String::from_utf8(out.stdout)
.map_err(|e| anyhow!("`{program}` stdout was not UTF-8: {e}"))?;
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
Ok(CmdOutput {
stdout,
stderr,
exit_code: out.status.code().unwrap_or(-1),
})
}
}
pub mod testing {
use super::*;
use std::collections::HashMap;
use std::sync::Mutex;
pub struct CannedRunner {
responses: Mutex<HashMap<String, CmdOutput>>,
}
impl Default for CannedRunner {
fn default() -> Self {
Self::new()
}
}
impl CannedRunner {
pub fn new() -> Self {
Self {
responses: Mutex::new(HashMap::new()),
}
}
pub fn register(&self, program: &str, args: &[&str], out: CmdOutput) {
let key = format!("{program} {}", args.join(" "));
self.responses.lock().unwrap().insert(key, out);
}
}
impl CmdRunner for CannedRunner {
fn run(&self, program: &str, args: &[&str], _cwd: &Path) -> Result<CmdOutput> {
let key = format!("{program} {}", args.join(" "));
self.responses
.lock()
.unwrap()
.get(&key)
.cloned()
.ok_or_else(|| anyhow!("CannedRunner: no response registered for `{key}`"))
}
}
}