use crate::{AdapterError, CommandSpec, ProcessRunner, RunResult};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Debug, Default, Clone)]
pub struct FakeProcessRunner {
results: Arc<Mutex<HashMap<String, RunResult>>>,
fallback: Arc<Mutex<Option<RunResult>>>,
history: Arc<Mutex<Vec<CommandSpec>>>,
}
impl FakeProcessRunner {
pub fn new() -> Self {
Self::default()
}
pub fn set_result(&self, argv: &[&str], result: RunResult) {
let key = argv.join(" ");
self.results.lock().expect("lock").insert(key, result);
}
pub fn set_fallback(&self, result: RunResult) {
*self.fallback.lock().expect("lock") = Some(result);
}
pub fn history(&self) -> Vec<CommandSpec> {
self.history.lock().expect("lock").clone()
}
pub fn clear(&self) {
self.results.lock().expect("lock").clear();
*self.fallback.lock().expect("lock") = None;
self.history.lock().expect("lock").clear();
}
pub fn call_count(&self) -> usize {
self.history.lock().expect("lock").len()
}
pub fn was_run(&self, argv: &[&str]) -> bool {
let key = argv.join(" ");
self.history
.lock()
.expect("lock")
.iter()
.any(|spec| spec.argv.join(" ") == key)
}
pub fn nth_call(&self, n: usize) -> Option<CommandSpec> {
self.history.lock().expect("lock").get(n).cloned()
}
}
impl ProcessRunner for FakeProcessRunner {
fn run(&self, spec: &CommandSpec) -> Result<RunResult, AdapterError> {
self.history.lock().expect("lock").push(spec.clone());
let key = spec.argv.join(" ");
let results = self.results.lock().expect("lock");
if let Some(res) = results.get(&key) {
return Ok(res.clone());
}
let fallback = self.fallback.lock().expect("lock");
if let Some(res) = &*fallback {
return Ok(res.clone());
}
Err(AdapterError::Other(format!(
"FakeProcessRunner: no result configured for command: {:?}",
spec.argv
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_result(exit_code: i32, wall_ms: u64) -> RunResult {
RunResult {
wall_ms,
exit_code,
timed_out: false,
cpu_ms: None,
page_faults: None,
ctx_switches: None,
max_rss_kb: None,
io_read_bytes: None,
io_write_bytes: None,
network_packets: None,
energy_uj: None,
binary_bytes: None,
stdout: vec![],
stderr: vec![],
}
}
fn make_spec(argv: Vec<&str>) -> CommandSpec {
CommandSpec {
name: argv.first().unwrap_or(&"unknown").to_string(),
argv: argv.into_iter().map(String::from).collect(),
cwd: None,
env: vec![],
timeout: None,
output_cap_bytes: 1024,
}
}
#[test]
fn new_runner_is_empty() {
let runner = FakeProcessRunner::new();
assert!(runner.history().is_empty());
assert_eq!(runner.call_count(), 0);
}
#[test]
fn set_result_returns_configured() {
let runner = FakeProcessRunner::new();
runner.set_result(&["echo", "hello"], make_result(0, 50));
let result = runner.run(&make_spec(vec!["echo", "hello"])).unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.wall_ms, 50);
}
#[test]
fn fallback_is_used_when_no_match() {
let runner = FakeProcessRunner::new();
runner.set_fallback(make_result(42, 100));
let result = runner.run(&make_spec(vec!["unknown"])).unwrap();
assert_eq!(result.exit_code, 42);
assert_eq!(result.wall_ms, 100);
}
#[test]
fn specific_result_takes_precedence_over_fallback() {
let runner = FakeProcessRunner::new();
runner.set_result(&["echo"], make_result(0, 10));
runner.set_fallback(make_result(1, 999));
let result = runner.run(&make_spec(vec!["echo"])).unwrap();
assert_eq!(result.exit_code, 0);
}
#[test]
fn error_when_no_result_configured() {
let runner = FakeProcessRunner::new();
let result = runner.run(&make_spec(vec!["unknown"]));
assert!(result.is_err());
}
#[test]
fn history_records_commands() {
let runner = FakeProcessRunner::new();
runner.set_fallback(make_result(0, 0));
runner.run(&make_spec(vec!["cmd1"])).unwrap();
runner.run(&make_spec(vec!["cmd2", "arg"])).unwrap();
let history = runner.history();
assert_eq!(history.len(), 2);
assert_eq!(history[0].argv, vec!["cmd1"]);
assert_eq!(history[1].argv, vec!["cmd2", "arg"]);
}
#[test]
fn was_run_checks_history() {
let runner = FakeProcessRunner::new();
runner.set_fallback(make_result(0, 0));
assert!(!runner.was_run(&["echo"]));
runner.run(&make_spec(vec!["echo", "hello"])).unwrap();
assert!(runner.was_run(&["echo", "hello"]));
assert!(!runner.was_run(&["echo", "goodbye"]));
}
#[test]
fn nth_call_returns_correct_command() {
let runner = FakeProcessRunner::new();
runner.set_fallback(make_result(0, 0));
runner.run(&make_spec(vec!["first"])).unwrap();
runner.run(&make_spec(vec!["second"])).unwrap();
assert_eq!(runner.nth_call(0).unwrap().argv, vec!["first"]);
assert_eq!(runner.nth_call(1).unwrap().argv, vec!["second"]);
assert!(runner.nth_call(2).is_none());
}
#[test]
fn clear_resets_everything() {
let runner = FakeProcessRunner::new();
runner.set_result(&["cmd"], make_result(0, 0));
runner.set_fallback(make_result(1, 1));
runner.run(&make_spec(vec!["cmd"])).unwrap();
runner.clear();
assert!(runner.history().is_empty());
assert!(runner.run(&make_spec(vec!["cmd"])).is_err());
}
#[test]
fn thread_safe_sharing() {
use std::sync::Arc;
use std::thread;
let runner = Arc::new(FakeProcessRunner::new());
runner.set_fallback(make_result(0, 0));
let handles: Vec<_> = (0..4)
.map(|i| {
let r = runner.clone();
thread::spawn(move || {
r.run(&make_spec(vec!["cmd", &i.to_string()])).unwrap();
})
})
.collect();
for h in handles {
h.join().unwrap();
}
assert_eq!(runner.call_count(), 4);
}
}