1use std::fs::{Metadata, ReadDir};
7use std::io;
8use std::path::Path;
9use std::process::{Command, ExitStatus, Output};
10
11pub trait FileSystem {
17 fn copy(&self, from: &Path, to: &Path) -> io::Result<u64>;
19
20 fn create_dir_all(&self, path: &Path) -> io::Result<()>;
22
23 fn read_dir(&self, path: &Path) -> io::Result<ReadDir>;
25
26 fn metadata(&self, path: &Path) -> io::Result<Metadata>;
28
29 fn read_to_string(&self, path: &Path) -> io::Result<String>;
31
32 fn write(&self, path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()>;
34}
35
36#[derive(Clone, Copy)]
38pub struct RealFileSystem;
39
40impl FileSystem for RealFileSystem {
41 fn copy(&self, from: &Path, to: &Path) -> io::Result<u64> {
42 std::fs::copy(from, to)
43 }
44
45 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
46 std::fs::create_dir_all(path)
47 }
48
49 fn read_dir(&self, path: &Path) -> io::Result<ReadDir> {
50 std::fs::read_dir(path)
51 }
52
53 fn metadata(&self, path: &Path) -> io::Result<Metadata> {
54 std::fs::metadata(path)
55 }
56
57 fn read_to_string(&self, path: &Path) -> io::Result<String> {
58 std::fs::read_to_string(path)
59 }
60
61 fn write(&self, path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> {
62 std::fs::write(path, contents)
63 }
64}
65
66pub trait CommandExecutor {
72 fn status(&self, cmd: &mut Command) -> io::Result<ExitStatus>;
75
76 fn output(&self, cmd: &mut Command) -> io::Result<Output>;
79
80 fn execute<F>(&self, builder: F, program: &str) -> io::Result<Output>
98 where
99 F: FnOnce(&mut Command) -> &mut Command,
100 {
101 let mut cmd = Command::new(program);
102 builder(&mut cmd);
103 self.output(&mut cmd)
104 }
105
106 fn run<F>(&self, builder: F, program: &str) -> io::Result<ExitStatus>
124 where
125 F: FnOnce(&mut Command) -> &mut Command,
126 {
127 let mut cmd = Command::new(program);
128 builder(&mut cmd);
129 self.status(&mut cmd)
130 }
131}
132
133#[derive(Debug, Clone, Copy)]
135pub struct RealCommandExecutor;
136
137impl CommandExecutor for RealCommandExecutor {
138 fn status(&self, cmd: &mut Command) -> io::Result<ExitStatus> {
139 cmd.status()
140 }
141
142 fn output(&self, cmd: &mut Command) -> io::Result<Output> {
143 cmd.output()
144 }
145}
146
147#[cfg(all(test, unix))]
151pub fn mock_exit_status(code: i32) -> ExitStatus {
152 use std::os::unix::process::ExitStatusExt;
153 ExitStatus::from_raw(code << 8) }
155
156#[cfg(all(test, windows))]
157pub fn mock_exit_status(code: i32) -> ExitStatus {
158 use std::os::windows::process::ExitStatusExt;
159 ExitStatus::from_raw(code as u32)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 use tempfile::TempDir;
167
168 #[test]
171 fn test_real_filesystem_write_and_read() {
172 let temp_dir = TempDir::new().unwrap();
173 let file_path = temp_dir.path().join("test.txt");
174
175 let fs = RealFileSystem;
176
177 let content = b"Hello, World!";
179 fs.write(&file_path, content).unwrap();
180
181 let read_content = fs.read_to_string(&file_path).unwrap();
183 assert_eq!(read_content, "Hello, World!");
184 }
185
186 #[test]
187 fn test_real_filesystem_copy() {
188 let temp_dir = TempDir::new().unwrap();
189 let source = temp_dir.path().join("source.txt");
190 let dest = temp_dir.path().join("dest.txt");
191
192 let fs = RealFileSystem;
193
194 fs.write(&source, b"test content").unwrap();
196
197 let bytes_copied = fs.copy(&source, &dest).unwrap();
199 assert_eq!(bytes_copied, 12); let dest_content = fs.read_to_string(&dest).unwrap();
203 assert_eq!(dest_content, "test content");
204 }
205
206 #[test]
207 fn test_real_filesystem_create_dir_all() {
208 let temp_dir = TempDir::new().unwrap();
209 let nested_path = temp_dir.path().join("a").join("b").join("c");
210
211 let fs = RealFileSystem;
212
213 fs.create_dir_all(&nested_path).unwrap();
215
216 assert!(nested_path.exists());
218 assert!(nested_path.is_dir());
219 }
220
221 #[test]
222 fn test_real_filesystem_metadata() {
223 let temp_dir = TempDir::new().unwrap();
224 let file_path = temp_dir.path().join("test.txt");
225
226 let fs = RealFileSystem;
227
228 fs.write(&file_path, b"content").unwrap();
230
231 let metadata = fs.metadata(&file_path).unwrap();
233 assert!(metadata.is_file());
234 assert_eq!(metadata.len(), 7); }
236
237 #[test]
238 fn test_real_filesystem_read_dir() {
239 let temp_dir = TempDir::new().unwrap();
240 let fs = RealFileSystem;
241
242 fs.write(&temp_dir.path().join("file1.txt"), b"test1")
244 .unwrap();
245 fs.write(&temp_dir.path().join("file2.txt"), b"test2")
246 .unwrap();
247 fs.write(&temp_dir.path().join("file3.txt"), b"test3")
248 .unwrap();
249
250 let entries: Vec<_> = fs
252 .read_dir(temp_dir.path())
253 .unwrap()
254 .collect::<Result<Vec<_>, _>>()
255 .unwrap();
256
257 assert_eq!(entries.len(), 3);
258 }
259
260 #[test]
261 fn test_real_filesystem_read_nonexistent_file_returns_error() {
262 let fs = RealFileSystem;
263 let result = fs.read_to_string(Path::new("/nonexistent/file.txt"));
264 assert!(result.is_err());
265 }
266
267 #[test]
268 fn test_real_filesystem_copy_nonexistent_file_returns_error() {
269 let temp_dir = TempDir::new().unwrap();
270 let fs = RealFileSystem;
271
272 let result = fs.copy(
273 Path::new("/nonexistent.txt"),
274 &temp_dir.path().join("dest.txt"),
275 );
276 assert!(result.is_err());
277 }
278
279 #[test]
282 fn test_real_command_executor_status_success() {
283 let executor = RealCommandExecutor;
284 let mut cmd = Command::new("echo");
285 cmd.arg("test");
286
287 let status = executor.status(&mut cmd).unwrap();
288 assert!(status.success());
289 }
290
291 #[test]
292 fn test_real_command_executor_output_captures_stdout() {
293 let executor = RealCommandExecutor;
294 let mut cmd = Command::new("echo");
295 cmd.arg("hello");
296
297 let output = executor.output(&mut cmd).unwrap();
298 assert!(output.status.success());
299
300 let stdout = String::from_utf8_lossy(&output.stdout);
301 assert!(stdout.contains("hello"));
302 }
303
304 #[test]
305 fn test_real_command_executor_execute_with_builder() {
306 let executor = RealCommandExecutor;
307
308 let output = executor
309 .execute(|cmd| cmd.arg("test_output"), "echo")
310 .unwrap();
311
312 assert!(output.status.success());
313 let stdout = String::from_utf8_lossy(&output.stdout);
314 assert!(stdout.contains("test_output"));
315 }
316
317 #[test]
318 fn test_real_command_executor_run_with_builder() {
319 let executor = RealCommandExecutor;
320
321 let status = executor.run(|cmd| cmd.arg("test_arg"), "echo").unwrap();
322
323 assert!(status.success());
324 }
325
326 #[test]
327 fn test_real_command_executor_nonexistent_command_returns_error() {
328 let executor = RealCommandExecutor;
329 let mut cmd = Command::new("nonexistent_command_xyz_123");
330
331 let result = executor.output(&mut cmd);
332 assert!(result.is_err());
333 }
334
335 #[test]
336 fn test_real_command_executor_failed_command_returns_non_success() {
337 let executor = RealCommandExecutor;
338 let mut cmd = Command::new("cat");
340 cmd.arg("/nonexistent/file/that/does/not/exist.txt");
341
342 let output = executor.output(&mut cmd).unwrap();
343 assert!(!output.status.success());
344 }
345
346 #[test]
347 fn test_real_filesystem_clone() {
348 let fs1 = RealFileSystem;
349 let fs2 = fs1;
350
351 let temp_dir = TempDir::new().unwrap();
353 let path = temp_dir.path().join("test.txt");
354
355 fs1.write(&path, b"test1").unwrap();
356 let content = fs2.read_to_string(&path).unwrap();
357 assert_eq!(content, "test1");
358 }
359
360 #[test]
361 fn test_real_command_executor_clone() {
362 let exec1 = RealCommandExecutor;
363 let exec2 = exec1;
364
365 let mut cmd = Command::new("echo");
367 cmd.arg("test");
368
369 let status1 = exec1.status(&mut cmd).unwrap();
370 assert!(status1.success());
371
372 let mut cmd2 = Command::new("echo");
373 cmd2.arg("test");
374 let status2 = exec2.status(&mut cmd2).unwrap();
375 assert!(status2.success());
376 }
377}