use std::collections::HashMap;
use std::process::{Command, Output};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
pub struct CommandResult {
pub success: bool,
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
}
impl CommandResult {
pub fn success(stdout: &str) -> Self {
Self {
success: true,
exit_code: 0,
stdout: stdout.to_string(),
stderr: String::new(),
}
}
pub fn failure(stderr: &str, exit_code: i32) -> Self {
Self {
success: false,
exit_code,
stdout: String::new(),
stderr: stderr.to_string(),
}
}
pub fn with_output(success: bool, stdout: &str, stderr: &str) -> Self {
Self {
success,
exit_code: i32::from(!success),
stdout: stdout.to_string(),
stderr: stderr.to_string(),
}
}
}
impl From<Output> for CommandResult {
fn from(output: Output) -> Self {
Self {
success: output.status.success(),
exit_code: output.status.code().unwrap_or(1),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
}
}
}
pub trait CommandExecutor: Send + Sync {
fn execute(&self, program: &str, args: &[&str]) -> Result<CommandResult, String>;
}
#[derive(Debug, Default)]
pub struct RealCommandExecutor;
impl CommandExecutor for RealCommandExecutor {
fn execute(&self, program: &str, args: &[&str]) -> Result<CommandResult, String> {
Command::new(program)
.args(args)
.output()
.map(CommandResult::from)
.map_err(|e| format!("Failed to execute {}: {}", program, e))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CommandKey {
pub program: String,
pub args: Vec<String>,
}
impl CommandKey {
pub fn new(program: &str, args: &[&str]) -> Self {
Self {
program: program.to_string(),
args: args.iter().map(|s| s.to_string()).collect(),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct MockCommandExecutor {
results: Arc<Mutex<HashMap<CommandKey, CommandResult>>>,
fallback: Option<CommandResult>,
executions: Arc<Mutex<Vec<CommandKey>>>,
}
impl MockCommandExecutor {
pub fn new() -> Self {
Self::default()
}
pub fn with_fallback(result: CommandResult) -> Self {
Self {
fallback: Some(result),
..Default::default()
}
}
pub fn register(&mut self, program: &str, args: &[&str], result: CommandResult) {
let key = CommandKey::new(program, args);
if let Ok(mut guard) = self.results.lock() {
guard.insert(key, result);
}
}
pub fn register_program(&mut self, program: &str, result: CommandResult) {
let key = CommandKey::new(program, &[]);
if let Ok(mut guard) = self.results.lock() {
guard.insert(key, result);
}
}
pub fn get_executions(&self) -> Vec<CommandKey> {
self.executions
.lock()
.map(|guard| guard.clone())
.unwrap_or_default()
}
pub fn was_executed(&self, program: &str, args: &[&str]) -> bool {
let key = CommandKey::new(program, args);
self.executions
.lock()
.map(|guard| guard.contains(&key))
.unwrap_or(false)
}
}
impl CommandExecutor for MockCommandExecutor {
fn execute(&self, program: &str, args: &[&str]) -> Result<CommandResult, String> {
let key = CommandKey::new(program, args);
if let Ok(mut guard) = self.executions.lock() {
guard.push(key.clone());
}
let results = match self.results.lock() {
Ok(guard) => guard,
Err(_) => return Err("Failed to acquire lock".to_string()),
};
if let Some(result) = results.get(&key) {
return Ok(result.clone());
}
let program_key = CommandKey::new(program, &[]);
if let Some(result) = results.get(&program_key) {
return Ok(result.clone());
}
if let Some(ref fallback) = self.fallback {
return Ok(fallback.clone());
}
Err(format!("No mock registered for: {} {:?}", program, args))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_command_result_success() {
let result = CommandResult::success("output");
assert!(result.success);
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "output");
assert!(result.stderr.is_empty());
}
#[test]
fn test_command_result_failure() {
let result = CommandResult::failure("error message", 1);
assert!(!result.success);
assert_eq!(result.exit_code, 1);
assert!(result.stdout.is_empty());
assert_eq!(result.stderr, "error message");
}
#[test]
fn test_command_result_with_output() {
let result = CommandResult::with_output(true, "stdout", "stderr");
assert!(result.success);
assert_eq!(result.stdout, "stdout");
assert_eq!(result.stderr, "stderr");
}
#[test]
fn test_mock_executor_register_and_execute() {
let mut mock = MockCommandExecutor::new();
mock.register("echo", &["hello"], CommandResult::success("hello\n"));
let result = mock.execute("echo", &["hello"]).expect("should succeed");
assert!(result.success);
assert_eq!(result.stdout, "hello\n");
}
#[test]
fn test_mock_executor_unregistered_command() {
let mock = MockCommandExecutor::new();
let result = mock.execute("unknown", &["arg"]);
assert!(result.is_err());
assert!(result.unwrap_err().contains("No mock registered"));
}
#[test]
fn test_mock_executor_fallback() {
let mock = MockCommandExecutor::with_fallback(CommandResult::success("fallback"));
let result = mock.execute("any", &["command"]).expect("should succeed");
assert!(result.success);
assert_eq!(result.stdout, "fallback");
}
#[test]
fn test_mock_executor_execution_tracking() {
let mock = MockCommandExecutor::with_fallback(CommandResult::success("ok"));
mock.execute("cmd1", &["a", "b"]).expect("should succeed");
mock.execute("cmd2", &["c"]).expect("should succeed");
assert!(mock.was_executed("cmd1", &["a", "b"]));
assert!(mock.was_executed("cmd2", &["c"]));
assert!(!mock.was_executed("cmd3", &[]));
}
#[test]
fn test_mock_executor_program_wildcard() {
let mut mock = MockCommandExecutor::new();
mock.register_program("cargo", CommandResult::success("cargo output"));
let result = mock.execute("cargo", &["build"]).expect("should succeed");
assert!(result.success);
let result = mock.execute("cargo", &["test"]).expect("should succeed");
assert!(result.success);
}
#[test]
fn test_command_key_equality() {
let key1 = CommandKey::new("cargo", &["test"]);
let key2 = CommandKey::new("cargo", &["test"]);
let key3 = CommandKey::new("cargo", &["build"]);
assert_eq!(key1, key2);
assert_ne!(key1, key3);
}
#[test]
fn test_real_executor_echo() {
let executor = RealCommandExecutor;
let result = executor.execute("echo", &["test"]);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.success);
assert!(result.stdout.contains("test"));
}
}