xerv_core/testing/providers/
fs.rs1use parking_lot::RwLock;
7use std::collections::HashMap;
8use std::io;
9use std::path::{Path, PathBuf};
10
11pub trait FsProvider: Send + Sync {
13 fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
15
16 fn write(&self, path: &Path, contents: &[u8]) -> io::Result<()>;
18
19 fn exists(&self, path: &Path) -> bool;
21
22 fn is_file(&self, path: &Path) -> bool;
24
25 fn is_dir(&self, path: &Path) -> bool;
27
28 fn create_dir_all(&self, path: &Path) -> io::Result<()>;
30
31 fn remove_file(&self, path: &Path) -> io::Result<()>;
33
34 fn remove_dir_all(&self, path: &Path) -> io::Result<()>;
36
37 fn read_dir(&self, path: &Path) -> io::Result<Vec<PathBuf>>;
39
40 fn metadata(&self, path: &Path) -> io::Result<FsMetadata>;
42
43 fn is_mock(&self) -> bool;
45}
46
47#[derive(Debug, Clone)]
49pub struct FsMetadata {
50 pub size: u64,
52 pub is_file: bool,
54 pub is_dir: bool,
56}
57
58pub struct RealFs;
60
61impl RealFs {
62 pub fn new() -> Self {
64 Self
65 }
66}
67
68impl Default for RealFs {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl FsProvider for RealFs {
75 fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
76 std::fs::read(path)
77 }
78
79 fn write(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
80 std::fs::write(path, contents)
81 }
82
83 fn exists(&self, path: &Path) -> bool {
84 path.exists()
85 }
86
87 fn is_file(&self, path: &Path) -> bool {
88 path.is_file()
89 }
90
91 fn is_dir(&self, path: &Path) -> bool {
92 path.is_dir()
93 }
94
95 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
96 std::fs::create_dir_all(path)
97 }
98
99 fn remove_file(&self, path: &Path) -> io::Result<()> {
100 std::fs::remove_file(path)
101 }
102
103 fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
104 std::fs::remove_dir_all(path)
105 }
106
107 fn read_dir(&self, path: &Path) -> io::Result<Vec<PathBuf>> {
108 std::fs::read_dir(path)?
109 .map(|entry| entry.map(|e| e.path()))
110 .collect()
111 }
112
113 fn metadata(&self, path: &Path) -> io::Result<FsMetadata> {
114 let meta = std::fs::metadata(path)?;
115 Ok(FsMetadata {
116 size: meta.len(),
117 is_file: meta.is_file(),
118 is_dir: meta.is_dir(),
119 })
120 }
121
122 fn is_mock(&self) -> bool {
123 false
124 }
125}
126
127pub struct MockFs {
143 files: RwLock<HashMap<PathBuf, Vec<u8>>>,
144 dirs: RwLock<HashMap<PathBuf, ()>>,
145}
146
147impl MockFs {
148 pub fn new() -> Self {
150 let mut dirs = HashMap::new();
151 dirs.insert(PathBuf::from("/"), ());
153 Self {
154 files: RwLock::new(HashMap::new()),
155 dirs: RwLock::new(dirs),
156 }
157 }
158
159 pub fn with_file(self, path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Self {
161 let path = path.as_ref().to_path_buf();
162
163 if let Some(parent) = path.parent() {
165 self.ensure_parent_dirs(parent);
166 }
167
168 self.files.write().insert(path, contents.as_ref().to_vec());
169 self
170 }
171
172 pub fn with_text_file(self, path: impl AsRef<Path>, contents: &str) -> Self {
174 self.with_file(path, contents.as_bytes())
175 }
176
177 pub fn with_dir(self, path: impl AsRef<Path>) -> Self {
179 self.ensure_parent_dirs(path.as_ref());
180 self.dirs.write().insert(path.as_ref().to_path_buf(), ());
181 self
182 }
183
184 fn ensure_parent_dirs(&self, path: &Path) {
186 let mut dirs = self.dirs.write();
187 let mut current = PathBuf::new();
188 for component in path.components() {
189 current.push(component);
190 dirs.entry(current.clone()).or_insert(());
191 }
192 }
193
194 pub fn all_files(&self) -> Vec<PathBuf> {
196 self.files.read().keys().cloned().collect()
197 }
198
199 pub fn all_dirs(&self) -> Vec<PathBuf> {
201 self.dirs.read().keys().cloned().collect()
202 }
203
204 pub fn clear(&self) {
206 self.files.write().clear();
207 let mut dirs = self.dirs.write();
208 dirs.clear();
209 dirs.insert(PathBuf::from("/"), ());
210 }
211
212 fn normalize_path(path: &Path) -> PathBuf {
214 let mut normalized = PathBuf::new();
215 for component in path.components() {
216 match component {
217 std::path::Component::ParentDir => {
218 normalized.pop();
219 }
220 std::path::Component::CurDir => {}
221 _ => normalized.push(component),
222 }
223 }
224 normalized
225 }
226}
227
228impl Default for MockFs {
229 fn default() -> Self {
230 Self::new()
231 }
232}
233
234impl FsProvider for MockFs {
235 fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
236 let path = Self::normalize_path(path);
237 self.files.read().get(&path).cloned().ok_or_else(|| {
238 io::Error::new(
239 io::ErrorKind::NotFound,
240 format!("File not found: {}", path.display()),
241 )
242 })
243 }
244
245 fn write(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
246 let path = Self::normalize_path(path);
247
248 if let Some(parent) = path.parent() {
250 if !parent.as_os_str().is_empty() && !self.dirs.read().contains_key(parent) {
251 return Err(io::Error::new(
252 io::ErrorKind::NotFound,
253 format!("Parent directory not found: {}", parent.display()),
254 ));
255 }
256 }
257
258 self.files.write().insert(path, contents.to_vec());
259 Ok(())
260 }
261
262 fn exists(&self, path: &Path) -> bool {
263 let path = Self::normalize_path(path);
264 self.files.read().contains_key(&path) || self.dirs.read().contains_key(&path)
265 }
266
267 fn is_file(&self, path: &Path) -> bool {
268 let path = Self::normalize_path(path);
269 self.files.read().contains_key(&path)
270 }
271
272 fn is_dir(&self, path: &Path) -> bool {
273 let path = Self::normalize_path(path);
274 self.dirs.read().contains_key(&path)
275 }
276
277 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
278 self.ensure_parent_dirs(&Self::normalize_path(path));
279 Ok(())
280 }
281
282 fn remove_file(&self, path: &Path) -> io::Result<()> {
283 let path = Self::normalize_path(path);
284 self.files
285 .write()
286 .remove(&path)
287 .map(|_| ())
288 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "File not found"))
289 }
290
291 fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
292 let path = Self::normalize_path(path);
293
294 self.files.write().retain(|p, _| !p.starts_with(&path));
296
297 self.dirs.write().retain(|p, _| !p.starts_with(&path));
299
300 Ok(())
301 }
302
303 fn read_dir(&self, path: &Path) -> io::Result<Vec<PathBuf>> {
304 let path = Self::normalize_path(path);
305
306 if !self.dirs.read().contains_key(&path) {
307 return Err(io::Error::new(
308 io::ErrorKind::NotFound,
309 "Directory not found",
310 ));
311 }
312
313 let mut entries = Vec::new();
314
315 for file_path in self.files.read().keys() {
317 if let Some(parent) = file_path.parent() {
318 if parent == path {
319 entries.push(file_path.clone());
320 }
321 }
322 }
323
324 for dir_path in self.dirs.read().keys() {
326 if let Some(parent) = dir_path.parent() {
327 if parent == path && dir_path != &path {
328 entries.push(dir_path.clone());
329 }
330 }
331 }
332
333 Ok(entries)
334 }
335
336 fn metadata(&self, path: &Path) -> io::Result<FsMetadata> {
337 let path = Self::normalize_path(path);
338
339 if let Some(contents) = self.files.read().get(&path) {
340 return Ok(FsMetadata {
341 size: contents.len() as u64,
342 is_file: true,
343 is_dir: false,
344 });
345 }
346
347 if self.dirs.read().contains_key(&path) {
348 return Ok(FsMetadata {
349 size: 0,
350 is_file: false,
351 is_dir: true,
352 });
353 }
354
355 Err(io::Error::new(io::ErrorKind::NotFound, "Path not found"))
356 }
357
358 fn is_mock(&self) -> bool {
359 true
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn mock_fs_basic_operations() {
369 let fs = MockFs::new()
370 .with_file("/test.txt", b"hello world")
371 .with_dir("/data");
372
373 assert!(fs.exists(Path::new("/test.txt")));
374 assert!(fs.is_file(Path::new("/test.txt")));
375 assert!(!fs.is_dir(Path::new("/test.txt")));
376
377 assert!(fs.exists(Path::new("/data")));
378 assert!(fs.is_dir(Path::new("/data")));
379 assert!(!fs.is_file(Path::new("/data")));
380
381 let contents = fs.read(Path::new("/test.txt")).unwrap();
382 assert_eq!(contents, b"hello world");
383 }
384
385 #[test]
386 fn mock_fs_write() {
387 let fs = MockFs::new().with_dir("/data");
388
389 fs.write(Path::new("/data/file.txt"), b"test content")
390 .unwrap();
391
392 assert!(fs.exists(Path::new("/data/file.txt")));
393 assert_eq!(
394 fs.read(Path::new("/data/file.txt")).unwrap(),
395 b"test content"
396 );
397 }
398
399 #[test]
400 fn mock_fs_remove() {
401 let fs = MockFs::new()
402 .with_file("/file.txt", b"test")
403 .with_file("/dir/nested.txt", b"nested")
404 .with_dir("/dir");
405
406 fs.remove_file(Path::new("/file.txt")).unwrap();
407 assert!(!fs.exists(Path::new("/file.txt")));
408
409 fs.remove_dir_all(Path::new("/dir")).unwrap();
410 assert!(!fs.exists(Path::new("/dir")));
411 assert!(!fs.exists(Path::new("/dir/nested.txt")));
412 }
413
414 #[test]
415 fn mock_fs_read_dir() {
416 let fs = MockFs::new()
417 .with_file("/dir/a.txt", b"a")
418 .with_file("/dir/b.txt", b"b")
419 .with_dir("/dir/subdir");
420
421 let entries = fs.read_dir(Path::new("/dir")).unwrap();
422 assert_eq!(entries.len(), 3);
423 }
424
425 #[test]
426 fn mock_fs_metadata() {
427 let fs = MockFs::new()
428 .with_file("/file.txt", b"12345")
429 .with_dir("/dir");
430
431 let file_meta = fs.metadata(Path::new("/file.txt")).unwrap();
432 assert_eq!(file_meta.size, 5);
433 assert!(file_meta.is_file);
434 assert!(!file_meta.is_dir);
435
436 let dir_meta = fs.metadata(Path::new("/dir")).unwrap();
437 assert!(dir_meta.is_dir);
438 assert!(!dir_meta.is_file);
439 }
440
441 #[test]
442 fn mock_fs_auto_creates_parent_dirs() {
443 let fs = MockFs::new().with_file("/a/b/c/file.txt", b"deep");
444
445 assert!(fs.is_dir(Path::new("/a")));
446 assert!(fs.is_dir(Path::new("/a/b")));
447 assert!(fs.is_dir(Path::new("/a/b/c")));
448 assert!(fs.is_file(Path::new("/a/b/c/file.txt")));
449 }
450}