use std::path::PathBuf;
use std::sync::{Mutex, PoisonError};
use std::time::Duration;
use super::{Exit, OnLine, ProcessError, ProcessRunner, ProcessSpec};
use crate::BoxFuture;
use crate::events::Stream;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Call {
pub program: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
pub cwd: Option<PathBuf>,
pub stdin: Option<PathBuf>,
}
impl Call {
#[must_use]
pub fn line(&self) -> String {
super::command_line(&self.program, &self.args)
}
#[must_use]
pub fn env(&self, name: &str) -> Option<&str> {
self.env.iter().rev().find(|(k, _)| k == name).map(|(_, v)| v.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct Reply {
pub stdout: Vec<String>,
pub stderr: Vec<String>,
pub code: i32,
pub missing: bool,
pub delay: Duration,
}
impl Reply {
pub fn ok(stdout: &str) -> Reply {
Reply {
stdout: stdout.lines().map(str::to_owned).collect(),
..Reply::default()
}
}
pub fn fail(code: i32, stderr: &str) -> Reply {
Reply {
stderr: stderr.lines().map(str::to_owned).collect(),
code,
..Reply::default()
}
}
#[must_use]
pub fn missing() -> Reply {
Reply {
missing: true,
..Reply::default()
}
}
#[must_use]
pub fn after(mut self, delay: Duration) -> Reply {
self.delay = delay;
self
}
}
type Script = Box<dyn Fn(&Call) -> Reply + Send + Sync>;
pub struct FakeProcesses {
script: Script,
calls: Mutex<Vec<Call>>,
}
impl std::fmt::Debug for FakeProcesses {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FakeProcesses").field("calls", &self.lines()).finish_non_exhaustive()
}
}
impl FakeProcesses {
pub fn new(script: impl Fn(&Call) -> Reply + Send + Sync + 'static) -> FakeProcesses {
FakeProcesses {
script: Box::new(script),
calls: Mutex::new(Vec::new()),
}
}
#[must_use]
pub fn rules(rules: &[(&str, Reply)]) -> FakeProcesses {
let rules: Vec<(String, Reply)> = rules.iter().map(|(k, r)| ((*k).to_owned(), r.clone())).collect();
FakeProcesses::new(move |call| {
let line = call.line();
rules
.iter()
.find(|(k, _)| line.contains(k.as_str()))
.map_or_else(|| Reply::ok(""), |(_, r)| r.clone())
})
}
pub fn calls(&self) -> Vec<Call> {
self.calls.lock().unwrap_or_else(PoisonError::into_inner).clone()
}
pub fn lines(&self) -> Vec<String> {
self.calls().iter().map(Call::line).collect()
}
pub fn last(&self, text: &str) -> Option<Call> {
self.calls().into_iter().rev().find(|c| c.line().contains(text))
}
async fn answer(&self, spec: &ProcessSpec, on_line: OnLine<'_>) -> Result<Exit, ProcessError> {
let call = Call {
program: spec.program.clone(),
args: spec.args.clone(),
env: spec.env.clone(),
cwd: spec.cwd.clone(),
stdin: spec.stdin.clone(),
};
let reply = (self.script)(&call);
self.calls.lock().unwrap_or_else(PoisonError::into_inner).push(call);
if !reply.delay.is_zero() {
tokio::time::sleep(reply.delay).await;
}
if reply.missing {
return Err(ProcessError::Missing { program: spec.program.clone() });
}
reply.stdout.iter().for_each(|l| on_line(Stream::Stdout, l));
reply.stderr.iter().for_each(|l| on_line(Stream::Stderr, l));
Ok(Exit { code: Some(reply.code) })
}
}
impl ProcessRunner for FakeProcesses {
fn run<'a>(&'a self, spec: &'a ProcessSpec, on_line: OnLine<'a>) -> BoxFuture<'a, Result<Exit, ProcessError>> {
Box::pin(self.answer(spec, on_line))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::process::capture;
#[tokio::test(start_paused = true)]
async fn calls_are_recorded_and_answered_by_rule() {
let fake = FakeProcesses::rules(&[
("compose ps", Reply::ok("[]")),
("vagrant", Reply::missing()),
("slow", Reply::fail(2, "boom").after(Duration::from_secs(600))),
]);
assert_eq!(capture(&fake, &ProcessSpec::new("docker", ["compose", "ps"])).await.unwrap().stdout, ["[]"]);
assert!(matches!(
capture(&fake, &ProcessSpec::new("vagrant", ["up"])).await,
Err(ProcessError::Missing { .. })
));
let slow = capture(&fake, &ProcessSpec::new("slow", Vec::<String>::new())).await.unwrap();
assert_eq!((slow.exit, slow.stderr), (Some(Exit { code: Some(2) }), vec!["boom".to_owned()]));
assert_eq!(fake.lines(), ["docker compose ps", "vagrant up", "slow"]);
assert!(fake.last("vagrant").is_some() && fake.last("terraform").is_none());
}
}