use std::collections::HashMap;
use std::process::Command;
use std::sync::Mutex;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PackageError {
#[error("io error running package manager: {0}")]
Io(#[from] std::io::Error),
#[error("package manager not available on PATH")]
NotAvailable,
#[error("package manager exited with status {status}: {stderr}")]
ExitFailure {
status: i32,
stderr: String,
},
}
pub struct RunOutcome {
pub status: i32,
pub stdout: String,
pub stderr: String,
}
pub trait Runner: Send + Sync {
fn run(&self, cmd: &str, args: &[&str]) -> Result<RunOutcome, std::io::Error>;
}
pub struct RealRunner;
impl Runner for RealRunner {
fn run(&self, cmd: &str, args: &[&str]) -> Result<RunOutcome, std::io::Error> {
let out = Command::new(cmd).args(args).output()?;
Ok(RunOutcome {
status: out.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
})
}
}
type CallKey = (String, Vec<String>);
#[derive(Clone)]
pub struct MockResponse {
pub status: i32,
pub stdout: String,
pub stderr: String,
}
impl MockResponse {
pub fn success() -> Self {
Self {
status: 0,
stdout: String::new(),
stderr: String::new(),
}
}
pub fn failure() -> Self {
Self {
status: 1,
stdout: String::new(),
stderr: String::new(),
}
}
}
pub struct MockRunner {
responses: HashMap<CallKey, MockResponse>,
calls: Mutex<Vec<(String, Vec<String>)>>,
}
impl MockRunner {
pub fn new() -> Self {
Self {
responses: HashMap::new(),
calls: Mutex::new(Vec::new()),
}
}
#[must_use]
pub fn with(mut self, cmd: &str, args: &[&str], resp: MockResponse) -> Self {
let key = (cmd.to_owned(), args.iter().map(|s| s.to_string()).collect());
self.responses.insert(key, resp);
self
}
pub fn calls(&self) -> Vec<(String, Vec<String>)> {
self.calls.lock().unwrap().clone()
}
}
impl Default for MockRunner {
fn default() -> Self {
Self::new()
}
}
impl Runner for MockRunner {
fn run(&self, cmd: &str, args: &[&str]) -> Result<RunOutcome, std::io::Error> {
let key: CallKey = (cmd.to_owned(), args.iter().map(|s| s.to_string()).collect());
self.calls.lock().unwrap().push(key.clone());
let resp = self
.responses
.get(&key)
.cloned()
.unwrap_or(MockResponse::success());
Ok(RunOutcome {
status: resp.status,
stdout: resp.stdout,
stderr: resp.stderr,
})
}
}
pub trait PackageManager: Send + Sync {
fn name(&self) -> &'static str;
fn is_available(&self) -> bool;
fn is_installed(&self, runner: &dyn Runner, pkg: &str) -> Result<bool, PackageError>;
fn install(&self, runner: &dyn Runner, packages: &[String]) -> Result<(), PackageError>;
}