1use std::collections::BTreeMap;
8use std::path::{Component, Path, PathBuf};
9use std::sync::Mutex;
10
11use termesh_filesystem::{
12 sort_entries, DirEntryInfo, EntryKind, FileSystemService, FsError, FsResult,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17enum Node {
18 Dir,
19 File(Vec<u8>),
20 Symlink(PathBuf),
22}
23
24#[derive(Debug, Default)]
31pub struct FakeFileSystem {
32 inner: Mutex<Inner>,
33}
34
35#[derive(Debug, Default)]
36struct Inner {
37 nodes: BTreeMap<PathBuf, Node>,
38 failures: BTreeMap<PathBuf, FsError>,
40}
41
42impl FakeFileSystem {
43 pub fn new() -> Self {
44 let mut inner = Inner::default();
45 inner.nodes.insert(PathBuf::from("/"), Node::Dir);
46 Self { inner: Mutex::new(inner) }
47 }
48
49 pub fn with_paths(paths: &[&str]) -> Self {
55 let fs = Self::new();
56 for p in paths {
57 if let Some(dir) = p.strip_suffix('/') {
58 fs.add_dir(dir);
59 } else {
60 fs.add_file(p, b"");
61 }
62 }
63 fs
64 }
65
66 pub fn add_dir(&self, path: impl AsRef<Path>) -> &Self {
68 let path = normalize(path.as_ref());
69 let mut inner = self.inner.lock().unwrap();
70 insert_ancestors(&mut inner.nodes, &path);
71 inner.nodes.insert(path, Node::Dir);
72 drop(inner);
73 self
74 }
75
76 pub fn add_file(&self, path: impl AsRef<Path>, contents: &[u8]) -> &Self {
78 let path = normalize(path.as_ref());
79 let mut inner = self.inner.lock().unwrap();
80 insert_ancestors(&mut inner.nodes, &path);
81 inner.nodes.insert(path, Node::File(contents.to_vec()));
82 drop(inner);
83 self
84 }
85
86 pub fn add_symlink(&self, path: impl AsRef<Path>, target: impl AsRef<Path>) -> &Self {
88 let path = normalize(path.as_ref());
89 let mut inner = self.inner.lock().unwrap();
90 insert_ancestors(&mut inner.nodes, &path);
91 inner.nodes.insert(path, Node::Symlink(target.as_ref().to_path_buf()));
92 drop(inner);
93 self
94 }
95
96 pub fn fail(&self, path: impl AsRef<Path>, error: FsError) -> &Self {
99 let path = normalize(path.as_ref());
100 self.inner.lock().unwrap().failures.insert(path, error);
101 self
102 }
103
104 pub fn paths(&self) -> Vec<PathBuf> {
106 self.inner.lock().unwrap().nodes.keys().cloned().collect()
107 }
108}
109
110impl Inner {
111 fn check_failure(&self, path: &Path) -> FsResult<()> {
112 match self.failures.get(path) {
113 Some(e) => Err(e.clone()),
114 None => Ok(()),
115 }
116 }
117}
118
119fn normalize(path: &Path) -> PathBuf {
121 let mut out = PathBuf::new();
122 for c in path.components() {
123 match c {
124 Component::ParentDir => {
125 out.pop();
126 }
127 Component::CurDir => {}
128 other => out.push(other.as_os_str()),
129 }
130 }
131 if out.as_os_str().is_empty() {
132 PathBuf::from("/")
133 } else {
134 out
135 }
136}
137
138fn insert_ancestors(nodes: &mut BTreeMap<PathBuf, Node>, path: &Path) {
139 let mut cur = PathBuf::new();
140 for c in path.components() {
141 cur.push(c.as_os_str());
142 if cur != path {
143 nodes.entry(cur.clone()).or_insert(Node::Dir);
144 }
145 }
146}
147
148fn kind_of(node: &Node) -> EntryKind {
149 match node {
150 Node::Dir => EntryKind::Dir,
151 Node::File(_) => EntryKind::File,
152 Node::Symlink(_) => EntryKind::Symlink,
153 }
154}
155
156impl FileSystemService for FakeFileSystem {
157 fn read_dir(&self, path: &Path) -> FsResult<Vec<DirEntryInfo>> {
158 let path = normalize(path);
159 let inner = self.inner.lock().unwrap();
160 inner.check_failure(&path)?;
161
162 match inner.nodes.get(&path) {
163 None => return Err(FsError::NotFound(path)),
164 Some(Node::Dir) => {}
165 Some(_) => return Err(FsError::NotADirectory(path)),
166 }
167
168 let mut out: Vec<DirEntryInfo> = inner
170 .nodes
171 .iter()
172 .filter(|(p, _)| p.parent() == Some(path.as_path()))
173 .map(|(p, node)| DirEntryInfo {
174 name: p.file_name().unwrap_or_default().to_os_string(),
175 path: p.clone(),
176 kind: kind_of(node),
177 })
178 .collect();
179 sort_entries(&mut out);
180 Ok(out)
181 }
182
183 fn read_file(&self, path: &Path) -> FsResult<Vec<u8>> {
184 let path = normalize(path);
185 let inner = self.inner.lock().unwrap();
186 inner.check_failure(&path)?;
187 match inner.nodes.get(&path) {
188 Some(Node::File(bytes)) => Ok(bytes.clone()),
189 Some(_) => Err(FsError::Other { path, message: "not a regular file".to_string() }),
190 None => Err(FsError::NotFound(path)),
191 }
192 }
193
194 fn create_file(&self, path: &Path) -> FsResult<()> {
195 let path = normalize(path);
196 let mut inner = self.inner.lock().unwrap();
197 inner.check_failure(&path)?;
198 if inner.nodes.contains_key(&path) {
199 return Err(FsError::AlreadyExists(path));
200 }
201 insert_ancestors(&mut inner.nodes, &path);
202 inner.nodes.insert(path, Node::File(Vec::new()));
203 Ok(())
204 }
205
206 fn write_file(&self, path: &Path, contents: &[u8]) -> FsResult<()> {
207 let path = normalize(path);
208 let mut inner = self.inner.lock().unwrap();
209 inner.check_failure(&path)?;
210 insert_ancestors(&mut inner.nodes, &path);
211 inner.nodes.insert(path, Node::File(contents.to_vec()));
212 Ok(())
213 }
214
215 fn create_dir(&self, path: &Path) -> FsResult<()> {
216 let path = normalize(path);
217 let mut inner = self.inner.lock().unwrap();
218 inner.check_failure(&path)?;
219 insert_ancestors(&mut inner.nodes, &path);
220 inner.nodes.entry(path).or_insert(Node::Dir);
221 Ok(())
222 }
223
224 fn rename(&self, from: &Path, to: &Path) -> FsResult<()> {
225 let (from, to) = (normalize(from), normalize(to));
226 let mut inner = self.inner.lock().unwrap();
227 inner.check_failure(&from)?;
228 if !inner.nodes.contains_key(&from) {
229 return Err(FsError::NotFound(from));
230 }
231 if inner.nodes.contains_key(&to) {
232 return Err(FsError::AlreadyExists(to));
233 }
234 let moving: Vec<PathBuf> =
236 inner.nodes.keys().filter(|p| p.starts_with(&from)).cloned().collect();
237 for old in moving {
238 let node = inner.nodes.remove(&old).expect("key came from this map");
239 let suffix = old.strip_prefix(&from).expect("filtered by starts_with");
240 inner.nodes.insert(to.join(suffix), node);
241 }
242 insert_ancestors(&mut inner.nodes, &to);
243 Ok(())
244 }
245
246 fn remove_file(&self, path: &Path) -> FsResult<()> {
247 let path = normalize(path);
248 let mut inner = self.inner.lock().unwrap();
249 inner.check_failure(&path)?;
250 match inner.nodes.get(&path) {
251 Some(Node::Dir) => Err(FsError::Other { path, message: "is a directory".to_string() }),
252 Some(_) => {
253 inner.nodes.remove(&path);
254 Ok(())
255 }
256 None => Err(FsError::NotFound(path)),
257 }
258 }
259
260 fn remove_dir_all(&self, path: &Path) -> FsResult<()> {
261 let path = normalize(path);
262 let mut inner = self.inner.lock().unwrap();
263 inner.check_failure(&path)?;
264 if !inner.nodes.contains_key(&path) {
265 return Err(FsError::NotFound(path));
266 }
267 inner.nodes.retain(|p, _| !p.starts_with(&path));
268 Ok(())
269 }
270
271 fn canonicalize(&self, path: &Path) -> FsResult<PathBuf> {
272 let path = normalize(path);
273 let inner = self.inner.lock().unwrap();
274 inner.check_failure(&path)?;
275 if inner.nodes.contains_key(&path) {
276 Ok(path)
277 } else {
278 Err(FsError::NotFound(path))
279 }
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 fn sample() -> FakeFileSystem {
288 FakeFileSystem::with_paths(&[
289 "/proj/src/main.rs",
290 "/proj/src/model.rs",
291 "/proj/Cargo.toml",
292 "/proj/target/debug/",
293 ])
294 }
295
296 fn names(fs: &FakeFileSystem, at: &str) -> Vec<String> {
297 fs.read_dir(Path::new(at))
298 .unwrap()
299 .iter()
300 .map(|e| e.name.to_string_lossy().into_owned())
301 .collect()
302 }
303
304 #[test]
305 fn read_dir_returns_direct_children_only_dirs_first() {
306 assert_eq!(names(&sample(), "/proj"), ["src", "target", "Cargo.toml"]);
307 }
308
309 #[test]
310 fn read_dir_does_not_recurse() {
311 assert!(!names(&sample(), "/proj").contains(&"main.rs".to_string()));
313 assert_eq!(names(&sample(), "/proj/src"), ["main.rs", "model.rs"]);
314 }
315
316 #[test]
317 fn ordering_matches_the_real_service_contract() {
318 let fs = FakeFileSystem::with_paths(&["/r/README.md", "/r/assets/", "/r/Cargo.toml"]);
319 assert_eq!(names(&fs, "/r"), ["assets", "Cargo.toml", "README.md"]);
320 }
321
322 #[test]
323 fn injected_failures_surface_on_read() {
324 let fs = sample();
325 let denied = PathBuf::from("/proj/src");
326 fs.fail(&denied, FsError::PermissionDenied(denied.clone()));
327 assert_eq!(fs.read_dir(&denied), Err(FsError::PermissionDenied(denied)));
328 assert!(fs.read_dir(Path::new("/proj")).is_ok());
330 }
331
332 #[test]
333 fn missing_and_wrong_kind_are_distinguishable() {
334 let fs = sample();
335 assert_eq!(
336 fs.read_dir(Path::new("/proj/nope")),
337 Err(FsError::NotFound(PathBuf::from("/proj/nope")))
338 );
339 assert_eq!(
340 fs.read_dir(Path::new("/proj/Cargo.toml")),
341 Err(FsError::NotADirectory(PathBuf::from("/proj/Cargo.toml")))
342 );
343 }
344
345 #[test]
346 fn create_file_refuses_to_clobber() {
347 let fs = sample();
348 fs.add_file("/proj/keep.txt", b"precious");
349 assert_eq!(
350 fs.create_file(Path::new("/proj/keep.txt")),
351 Err(FsError::AlreadyExists(PathBuf::from("/proj/keep.txt")))
352 );
353 assert_eq!(fs.read_file(Path::new("/proj/keep.txt")).unwrap(), b"precious");
354 }
355
356 #[test]
357 fn rename_moves_the_whole_subtree() {
358 let fs = sample();
359 fs.rename(Path::new("/proj/src"), Path::new("/proj/lib")).unwrap();
360 assert_eq!(names(&fs, "/proj/lib"), ["main.rs", "model.rs"]);
361 assert_eq!(
362 fs.read_dir(Path::new("/proj/src")),
363 Err(FsError::NotFound(PathBuf::from("/proj/src")))
364 );
365 }
366
367 #[test]
368 fn remove_dir_all_takes_descendants_but_not_siblings() {
369 let fs = sample();
370 fs.remove_dir_all(Path::new("/proj/src")).unwrap();
371 assert!(!fs.paths().iter().any(|p| p.starts_with("/proj/src")));
372 assert!(fs.paths().contains(&PathBuf::from("/proj/Cargo.toml")));
373 }
374
375 #[test]
376 fn remove_file_refuses_directories() {
377 let fs = sample();
378 assert!(fs.remove_file(Path::new("/proj/src")).is_err());
379 assert!(fs.read_dir(Path::new("/proj/src")).is_ok(), "directory survives");
380 }
381
382 #[test]
383 fn symlinks_are_listed_but_typed_as_symlinks() {
384 let fs = sample();
385 fs.add_symlink("/proj/link", "/proj/src");
386 let entries = fs.read_dir(Path::new("/proj")).unwrap();
387 let link = entries.iter().find(|e| e.name == "link").unwrap();
388 assert_eq!(link.kind, EntryKind::Symlink);
389 }
390
391 #[test]
392 fn paths_are_normalized_before_lookup() {
393 let fs = sample();
394 assert_eq!(names(&fs, "/proj/src/../src"), ["main.rs", "model.rs"]);
395 assert_eq!(names(&fs, "/proj/./src"), ["main.rs", "model.rs"]);
396 }
397}