Skip to main content

prodigy/env/
mock.rs

1//! Mock environment implementations for testing
2//!
3//! These implementations use in-memory data structures and provide controlled,
4//! predictable behavior for testing without actual I/O operations.
5
6use super::traits::{DbEnv, FileEnv, GitEnv, ProcessEnv};
7use anyhow::Result;
8use std::collections::HashMap;
9use std::fs::Metadata;
10use std::path::{Path, PathBuf};
11use std::process::{Child, Command, Output};
12use std::sync::{Arc, Mutex};
13
14/// Mock file system for testing
15///
16/// Stores files in memory and provides controlled file system operations.
17///
18/// # Examples
19///
20/// ```
21/// use prodigy::env::{MockFileEnv, FileEnv};
22/// use std::path::Path;
23///
24/// let env = MockFileEnv::new();
25/// env.add_file("config.yml", "name: test");
26///
27/// let content = env.read_to_string(Path::new("config.yml")).unwrap();
28/// assert_eq!(content, "name: test");
29/// ```
30#[derive(Debug, Clone)]
31pub struct MockFileEnv {
32    files: Arc<Mutex<HashMap<PathBuf, String>>>,
33}
34
35impl MockFileEnv {
36    pub fn new() -> Self {
37        Self {
38            files: Arc::new(Mutex::new(HashMap::new())),
39        }
40    }
41
42    /// Add a file to the mock file system
43    pub fn add_file(&self, path: impl Into<PathBuf>, content: impl Into<String>) {
44        self.files
45            .lock()
46            .unwrap()
47            .insert(path.into(), content.into());
48    }
49
50    /// Get all files in the mock file system
51    pub fn files(&self) -> HashMap<PathBuf, String> {
52        self.files.lock().unwrap().clone()
53    }
54
55    /// Clear all files from the mock file system
56    pub fn clear(&self) {
57        self.files.lock().unwrap().clear();
58    }
59}
60
61impl Default for MockFileEnv {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl FileEnv for MockFileEnv {
68    fn read_to_string(&self, path: &Path) -> std::io::Result<String> {
69        self.files
70            .lock()
71            .unwrap()
72            .get(path)
73            .cloned()
74            .ok_or_else(|| {
75                std::io::Error::new(
76                    std::io::ErrorKind::NotFound,
77                    format!("File not found: {}", path.display()),
78                )
79            })
80    }
81
82    fn write(&self, path: &Path, content: &str) -> std::io::Result<()> {
83        self.files
84            .lock()
85            .unwrap()
86            .insert(path.to_path_buf(), content.to_string());
87        Ok(())
88    }
89
90    fn exists(&self, path: &Path) -> bool {
91        self.files.lock().unwrap().contains_key(path)
92    }
93
94    fn metadata(&self, _path: &Path) -> std::io::Result<Metadata> {
95        // For mock purposes, we don't need actual metadata
96        // Tests that need metadata should use real file system
97        Err(std::io::Error::new(
98            std::io::ErrorKind::Unsupported,
99            "Metadata not supported in mock file system",
100        ))
101    }
102
103    fn create_dir_all(&self, _path: &Path) -> std::io::Result<()> {
104        // In mock, directories are implicit
105        Ok(())
106    }
107
108    fn remove_file(&self, path: &Path) -> std::io::Result<()> {
109        self.files
110            .lock()
111            .unwrap()
112            .remove(path)
113            .ok_or_else(|| {
114                std::io::Error::new(
115                    std::io::ErrorKind::NotFound,
116                    format!("File not found: {}", path.display()),
117                )
118            })
119            .map(|_| ())
120    }
121
122    fn remove_dir_all(&self, path: &Path) -> std::io::Result<()> {
123        // Remove all files that start with this path
124        let mut files = self.files.lock().unwrap();
125        files.retain(|p, _| !p.starts_with(path));
126        Ok(())
127    }
128}
129
130/// Mock process environment for testing
131///
132/// Records command executions and returns predefined outputs.
133#[derive(Debug, Clone)]
134pub struct MockProcessEnv {
135    commands: Arc<Mutex<Vec<String>>>,
136    outputs: Arc<Mutex<HashMap<String, Output>>>,
137}
138
139impl MockProcessEnv {
140    pub fn new() -> Self {
141        Self {
142            commands: Arc::new(Mutex::new(Vec::new())),
143            outputs: Arc::new(Mutex::new(HashMap::new())),
144        }
145    }
146
147    /// Record that a command was executed
148    fn record_command(&self, cmd: &Command) {
149        let cmd_str = format!("{:?}", cmd);
150        self.commands.lock().unwrap().push(cmd_str);
151    }
152
153    /// Get all commands that were executed
154    pub fn commands(&self) -> Vec<String> {
155        self.commands.lock().unwrap().clone()
156    }
157
158    /// Set a predefined output for a command
159    pub fn set_output(&self, cmd_pattern: impl Into<String>, output: Output) {
160        self.outputs
161            .lock()
162            .unwrap()
163            .insert(cmd_pattern.into(), output);
164    }
165}
166
167impl Default for MockProcessEnv {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl ProcessEnv for MockProcessEnv {
174    fn spawn(&self, cmd: &mut Command) -> std::io::Result<Child> {
175        self.record_command(cmd);
176        // Mock spawn is not supported - use run() instead
177        Err(std::io::Error::new(
178            std::io::ErrorKind::Unsupported,
179            "Spawn not supported in mock environment, use run() instead",
180        ))
181    }
182
183    fn run(&self, cmd: &mut Command) -> std::io::Result<Output> {
184        self.record_command(cmd);
185
186        // Return predefined output if available
187        let cmd_str = format!("{:?}", cmd);
188        if let Some(output) = self.outputs.lock().unwrap().get(&cmd_str) {
189            return Ok(output.clone());
190        }
191
192        // Default: successful empty output
193        Ok(Output {
194            status: std::process::ExitStatus::default(),
195            stdout: Vec::new(),
196            stderr: Vec::new(),
197        })
198    }
199}
200
201/// Mock git environment for testing
202///
203/// Simulates git operations without actual repository manipulation.
204#[derive(Debug, Clone)]
205pub struct MockGitEnv {
206    operations: Arc<Mutex<Vec<String>>>,
207    branches: Arc<Mutex<Vec<String>>>,
208    current_branch: Arc<Mutex<String>>,
209    head_sha: Arc<Mutex<String>>,
210    is_clean: Arc<Mutex<bool>>,
211}
212
213impl MockGitEnv {
214    pub fn new() -> Self {
215        Self {
216            operations: Arc::new(Mutex::new(Vec::new())),
217            branches: Arc::new(Mutex::new(vec!["main".to_string()])),
218            current_branch: Arc::new(Mutex::new("main".to_string())),
219            head_sha: Arc::new(Mutex::new("abc123".to_string())),
220            is_clean: Arc::new(Mutex::new(true)),
221        }
222    }
223
224    /// Record a git operation
225    fn record_operation(&self, operation: String) {
226        self.operations.lock().unwrap().push(operation);
227    }
228
229    /// Get all recorded operations
230    pub fn operations(&self) -> Vec<String> {
231        self.operations.lock().unwrap().clone()
232    }
233
234    /// Set the current branch
235    pub fn set_current_branch(&self, branch: impl Into<String>) {
236        *self.current_branch.lock().unwrap() = branch.into();
237    }
238
239    /// Set the HEAD SHA
240    pub fn set_head_sha(&self, sha: impl Into<String>) {
241        *self.head_sha.lock().unwrap() = sha.into();
242    }
243
244    /// Set whether working directory is clean
245    pub fn set_is_clean(&self, clean: bool) {
246        *self.is_clean.lock().unwrap() = clean;
247    }
248}
249
250impl Default for MockGitEnv {
251    fn default() -> Self {
252        Self::new()
253    }
254}
255
256impl GitEnv for MockGitEnv {
257    fn worktree_add(&self, path: &Path, branch: &str) -> Result<()> {
258        self.record_operation(format!("worktree add {} {}", path.display(), branch));
259        self.branches.lock().unwrap().push(branch.to_string());
260        Ok(())
261    }
262
263    fn worktree_remove(&self, path: &Path) -> Result<()> {
264        self.record_operation(format!("worktree remove {}", path.display()));
265        Ok(())
266    }
267
268    fn worktree_list(&self) -> Result<Vec<String>> {
269        self.record_operation("worktree list".to_string());
270        Ok(self.branches.lock().unwrap().clone())
271    }
272
273    fn merge(&self, branch: &str) -> Result<()> {
274        self.record_operation(format!("merge {}", branch));
275        Ok(())
276    }
277
278    fn commit(&self, message: &str) -> Result<String> {
279        self.record_operation(format!("commit: {}", message));
280        let sha = self.head_sha.lock().unwrap().clone();
281        Ok(sha)
282    }
283
284    fn head_sha(&self) -> Result<String> {
285        Ok(self.head_sha.lock().unwrap().clone())
286    }
287
288    fn create_branch(&self, name: &str) -> Result<()> {
289        self.record_operation(format!("create branch {}", name));
290        self.branches.lock().unwrap().push(name.to_string());
291        Ok(())
292    }
293
294    fn checkout(&self, branch: &str) -> Result<()> {
295        self.record_operation(format!("checkout {}", branch));
296        *self.current_branch.lock().unwrap() = branch.to_string();
297        Ok(())
298    }
299
300    fn current_branch(&self) -> Result<String> {
301        Ok(self.current_branch.lock().unwrap().clone())
302    }
303
304    fn is_clean(&self) -> Result<bool> {
305        Ok(*self.is_clean.lock().unwrap())
306    }
307}
308
309/// Mock database environment (placeholder)
310#[derive(Debug, Clone, Default)]
311pub struct MockDbEnv;
312
313impl MockDbEnv {
314    pub fn new() -> Self {
315        Self
316    }
317}
318
319impl DbEnv for MockDbEnv {
320    // Placeholder implementation
321}