Skip to main content

perl_subprocess_runtime/
mock.rs

1use crate::{SubprocessError, SubprocessOutput, SubprocessRuntime};
2use std::sync::{Arc, Mutex, MutexGuard};
3
4fn lock<'a, T>(mutex: &'a Mutex<T>) -> MutexGuard<'a, T> {
5    match mutex.lock() {
6        Ok(guard) => guard,
7        Err(poisoned) => poisoned.into_inner(),
8    }
9}
10
11/// A recorded command invocation.
12#[derive(Debug, Clone)]
13pub struct CommandInvocation {
14    /// The program that was called.
15    pub program: String,
16    /// The arguments passed.
17    pub args: Vec<String>,
18    /// The stdin data provided.
19    pub stdin: Option<Vec<u8>>,
20}
21
22/// Builder for mock responses.
23#[derive(Debug, Clone)]
24pub struct MockResponse {
25    /// Stdout to return.
26    pub stdout: Vec<u8>,
27    /// Stderr to return.
28    pub stderr: Vec<u8>,
29    /// Status code to return.
30    pub status_code: i32,
31}
32
33impl MockResponse {
34    /// Create a successful mock response with the given stdout.
35    pub fn success(stdout: impl Into<Vec<u8>>) -> Self {
36        Self { stdout: stdout.into(), stderr: Vec::new(), status_code: 0 }
37    }
38
39    /// Create a failed mock response with the given stderr.
40    pub fn failure(stderr: impl Into<Vec<u8>>, status_code: i32) -> Self {
41        Self { stdout: Vec::new(), stderr: stderr.into(), status_code }
42    }
43}
44
45/// Mock subprocess runtime for testing.
46pub struct MockSubprocessRuntime {
47    invocations: Arc<Mutex<Vec<CommandInvocation>>>,
48    responses: Arc<Mutex<Vec<MockResponse>>>,
49    default_response: MockResponse,
50}
51
52impl MockSubprocessRuntime {
53    /// Create a new mock runtime with a default successful response.
54    pub fn new() -> Self {
55        Self {
56            invocations: Arc::new(Mutex::new(Vec::new())),
57            responses: Arc::new(Mutex::new(Vec::new())),
58            default_response: MockResponse::success(Vec::new()),
59        }
60    }
61
62    /// Add a response to be returned for the next command.
63    pub fn add_response(&self, response: MockResponse) {
64        lock(&self.responses).push(response);
65    }
66
67    /// Set the default response when no queued responses remain.
68    pub fn set_default_response(&mut self, response: MockResponse) {
69        self.default_response = response;
70    }
71
72    /// Get all recorded invocations.
73    pub fn invocations(&self) -> Vec<CommandInvocation> {
74        lock(&self.invocations).clone()
75    }
76
77    /// Clear recorded invocations.
78    pub fn clear_invocations(&self) {
79        lock(&self.invocations).clear();
80    }
81}
82
83impl Default for MockSubprocessRuntime {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl SubprocessRuntime for MockSubprocessRuntime {
90    fn run_command(
91        &self,
92        program: &str,
93        args: &[&str],
94        stdin: Option<&[u8]>,
95    ) -> Result<SubprocessOutput, SubprocessError> {
96        lock(&self.invocations).push(CommandInvocation {
97            program: program.to_string(),
98            args: args.iter().map(|s| s.to_string()).collect(),
99            stdin: stdin.map(|s| s.to_vec()),
100        });
101
102        let response = {
103            let mut responses = lock(&self.responses);
104            if responses.is_empty() { self.default_response.clone() } else { responses.remove(0) }
105        };
106
107        Ok(SubprocessOutput {
108            stdout: response.stdout,
109            stderr: response.stderr,
110            status_code: response.status_code,
111        })
112    }
113}