use super::runner::TokioProcessRunner;
use super::{ProcessCommand, ProcessError, ProcessRunner};
use async_trait::async_trait;
use std::collections::HashMap;
use std::path::Path;
use std::process::Output;
use std::time::Duration;
pub type SubprocessError = ProcessError;
#[async_trait]
pub trait SubprocessExecutor: Send + Sync {
async fn execute(
&self,
command: &str,
args: &[&str],
working_dir: Option<&Path>,
env: Option<HashMap<String, String>>,
timeout: Option<Duration>,
) -> Result<Output, SubprocessError>;
}
pub struct RealSubprocessExecutor;
#[async_trait]
impl SubprocessExecutor for RealSubprocessExecutor {
async fn execute(
&self,
command: &str,
args: &[&str],
working_dir: Option<&Path>,
env: Option<HashMap<String, String>>,
timeout: Option<Duration>,
) -> Result<Output, SubprocessError> {
let runner = TokioProcessRunner;
let cmd = ProcessCommand {
program: command.to_string(),
args: args.iter().map(|s| s.to_string()).collect(),
env: env.unwrap_or_default(),
working_dir: working_dir.map(|p| p.to_path_buf()),
timeout,
stdin: None,
suppress_stderr: false,
};
let output = runner.run(cmd).await?;
Ok(create_output(
output.status.code().unwrap_or(0),
output.stdout.into_bytes(),
output.stderr.into_bytes(),
))
}
}
fn create_output(exit_code: i32, stdout: Vec<u8>, stderr: Vec<u8>) -> Output {
Output {
status: create_exit_status(exit_code),
stdout,
stderr,
}
}
#[cfg(unix)]
fn create_exit_status(code: i32) -> std::process::ExitStatus {
use std::os::unix::process::ExitStatusExt;
std::process::ExitStatus::from_raw(code)
}
#[cfg(windows)]
fn create_exit_status(code: i32) -> std::process::ExitStatus {
use std::os::windows::process::ExitStatusExt;
std::process::ExitStatus::from_raw(code as u32)
}
#[cfg(test)]
type MockResponse = (
String,
Vec<String>,
Option<std::path::PathBuf>,
Option<Duration>,
Output,
);
#[cfg(test)]
pub struct MockSubprocessExecutor {
responses: std::sync::Mutex<Vec<MockResponse>>,
}
#[cfg(test)]
impl Default for MockSubprocessExecutor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
impl MockSubprocessExecutor {
pub fn new() -> Self {
Self {
responses: std::sync::Mutex::new(Vec::new()),
}
}
pub fn expect_execute(
&mut self,
command: &str,
args: Vec<&str>,
working_dir: Option<std::path::PathBuf>,
_env: Option<HashMap<String, String>>,
timeout: Option<Duration>,
output: Output,
) {
let mut responses = self.responses.lock().unwrap();
responses.push((
command.to_string(),
args.iter().map(|s| s.to_string()).collect(),
working_dir,
timeout,
output,
));
}
}
#[cfg(test)]
#[async_trait]
impl SubprocessExecutor for MockSubprocessExecutor {
async fn execute(
&self,
command: &str,
args: &[&str],
working_dir: Option<&Path>,
_env: Option<HashMap<String, String>>,
timeout: Option<Duration>,
) -> Result<Output, SubprocessError> {
let mut responses = self.responses.lock().unwrap();
for i in 0..responses.len() {
let (ref cmd, ref expected_args, ref expected_dir, ref expected_timeout, _) =
responses[i];
let args_vec: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let working_dir_path = working_dir.map(|p| p.to_path_buf());
if cmd == command
&& expected_args == &args_vec
&& expected_dir == &working_dir_path
&& expected_timeout == &timeout
{
let (_, _, _, _, output) = responses.remove(i);
return Ok(output);
}
}
Err(SubprocessError::MockExpectationNotMet(format!(
"Unexpected command: {command} {args:?}"
)))
}
}