1use std::collections::{BTreeMap, btree_map};
2use std::io;
3use std::iter::FusedIterator;
4use std::sync::{Arc, RwLock, RwLockWriteGuard};
5
6use camino::{Utf8Path, Utf8PathBuf};
7use filetime::FileTime;
8use rustc_hash::FxHashMap;
9
10use crate::system::{
11 DirectoryEntry, FileType, Metadata, Result, SystemPath, SystemPathBuf, SystemVirtualPath,
12 SystemVirtualPathBuf, file_time_now, walk_directory,
13};
14
15use super::walk_directory::{
16 DirectoryWalker, IgnoreIncremental, WalkDirectoryBuilder, WalkDirectoryConfiguration,
17 WalkDirectoryVisitor, WalkDirectoryVisitorBuilder, WalkState,
18};
19
20#[derive(Clone)]
32pub struct MemoryFileSystem {
33 inner: Arc<MemoryFileSystemInner>,
34}
35
36impl MemoryFileSystem {
37 const PERMISSION: u32 = 0o755;
39
40 pub fn new() -> Self {
41 Self::with_current_directory("/")
42 }
43
44 pub fn with_current_directory(cwd: impl AsRef<SystemPath>) -> Self {
46 let cwd = cwd.as_ref().to_path_buf();
47
48 assert!(
49 cwd.starts_with("/"),
50 "The current working directory must be an absolute path."
51 );
52
53 let fs = Self {
54 inner: Arc::new(MemoryFileSystemInner {
55 by_path: RwLock::new(BTreeMap::default()),
56 virtual_files: RwLock::new(FxHashMap::default()),
57 cwd: cwd.clone(),
58 }),
59 };
60
61 fs.create_directory_all(&cwd).unwrap();
62
63 fs
64 }
65
66 pub fn current_directory(&self) -> &SystemPath {
67 &self.inner.cwd
68 }
69
70 pub fn metadata(&self, path: impl AsRef<SystemPath>) -> Result<Metadata> {
71 fn metadata(fs: &MemoryFileSystem, path: &SystemPath) -> Result<Metadata> {
72 let by_path = fs.inner.by_path.read().unwrap();
73 let normalized = fs.normalize_path(path);
74
75 let entry = by_path.get(&normalized).ok_or_else(not_found)?;
76
77 let metadata = match entry {
78 Entry::File(file) => Metadata {
79 revision: file.last_modified.into(),
80 permissions: Some(MemoryFileSystem::PERMISSION),
81 file_type: FileType::File,
82 },
83 Entry::Directory(directory) => Metadata {
84 revision: directory.last_modified.into(),
85 permissions: Some(MemoryFileSystem::PERMISSION),
86 file_type: FileType::Directory,
87 },
88 };
89
90 Ok(metadata)
91 }
92
93 metadata(self, path.as_ref())
94 }
95
96 pub fn canonicalize(&self, path: impl AsRef<SystemPath>) -> Result<SystemPathBuf> {
97 let path = path.as_ref();
98 self.metadata(path)?;
100 Ok(SystemPathBuf::from_utf8_path_buf(self.normalize_path(path)))
101 }
102
103 pub fn is_file(&self, path: impl AsRef<SystemPath>) -> bool {
104 let by_path = self.inner.by_path.read().unwrap();
105 let normalized = self.normalize_path(path.as_ref());
106
107 matches!(by_path.get(&normalized), Some(Entry::File(_)))
108 }
109
110 pub fn is_directory(&self, path: impl AsRef<SystemPath>) -> bool {
111 let by_path = self.inner.by_path.read().unwrap();
112 let normalized = self.normalize_path(path.as_ref());
113
114 matches!(by_path.get(&normalized), Some(Entry::Directory(_)))
115 }
116
117 pub fn is_executable(&self, path: &SystemPath) -> bool {
118 self.is_file(path)
121 }
122
123 pub fn read_to_string(&self, path: impl AsRef<SystemPath>) -> Result<String> {
124 fn read_to_string(fs: &MemoryFileSystem, path: &SystemPath) -> Result<String> {
125 let by_path = fs.inner.by_path.read().unwrap();
126 let normalized = fs.normalize_path(path);
127
128 let entry = by_path.get(&normalized).ok_or_else(not_found)?;
129
130 match entry {
131 Entry::File(file) => {
132 String::from_utf8(file.content.to_vec()).map_err(|_| invalid_utf8())
133 }
134 Entry::Directory(_) => Err(io::Error::from(io::ErrorKind::IsADirectory)),
135 }
136 }
137
138 read_to_string(self, path.as_ref())
139 }
140
141 pub(crate) fn read_virtual_path_to_string(
142 &self,
143 path: impl AsRef<SystemVirtualPath>,
144 ) -> Result<String> {
145 let virtual_files = self.inner.virtual_files.read().unwrap();
146 let file = virtual_files
147 .get(&path.as_ref().to_path_buf())
148 .ok_or_else(not_found)?;
149
150 String::from_utf8(file.content.to_vec()).map_err(|_| invalid_utf8())
151 }
152
153 pub fn exists(&self, path: &SystemPath) -> bool {
154 let by_path = self.inner.by_path.read().unwrap();
155 let normalized = self.normalize_path(path);
156
157 by_path.contains_key(&normalized)
158 }
159
160 pub fn virtual_path_exists(&self, path: &SystemVirtualPath) -> bool {
161 let virtual_files = self.inner.virtual_files.read().unwrap();
162 virtual_files.contains_key(&path.to_path_buf())
163 }
164
165 pub(crate) fn create_new_file(&self, path: &SystemPath) -> Result<()> {
166 let normalized = self.normalize_path(path);
167
168 let mut by_path = self.inner.by_path.write().unwrap();
169 match by_path.entry(normalized) {
170 btree_map::Entry::Vacant(entry) => {
171 let parent = entry.key().parent().map(Utf8Path::to_path_buf);
172 entry.insert(Entry::File(File {
173 content: Box::default(),
174 last_modified: file_time_now(),
175 }));
176 if let Some(parent) = parent {
177 touch_directory(&mut by_path, &parent);
178 }
179
180 Ok(())
181 }
182 btree_map::Entry::Occupied(_) => Err(io::Error::new(
183 io::ErrorKind::AlreadyExists,
184 "File already exists",
185 )),
186 }
187 }
188
189 pub fn write_file(
193 &self,
194 path: impl AsRef<SystemPath>,
195 content: impl AsRef<[u8]>,
196 ) -> Result<()> {
197 let mut by_path = self.inner.by_path.write().unwrap();
198
199 let normalized = self.normalize_path(path.as_ref());
200 let file = get_or_create_file(&mut by_path, &normalized)?;
201 file.content = content.as_ref().to_vec().into_boxed_slice();
202 file.last_modified = file_time_now();
203
204 Ok(())
205 }
206
207 pub fn write_files_all<P, C>(&self, files: impl IntoIterator<Item = (P, C)>) -> Result<()>
213 where
214 P: AsRef<SystemPath>,
215 C: ToString,
216 {
217 for (path, content) in files {
218 self.write_file_all(path.as_ref(), content.to_string())?;
219 }
220
221 Ok(())
222 }
223
224 pub fn write_file_all(
230 &self,
231 path: impl AsRef<SystemPath>,
232 content: impl AsRef<[u8]>,
233 ) -> Result<()> {
234 let path = path.as_ref();
235
236 if let Some(parent) = path.parent() {
237 self.create_directory_all(parent)?;
238 }
239
240 self.write_file(path, content)
241 }
242
243 pub fn write_virtual_file(
247 &self,
248 path: impl AsRef<SystemVirtualPath>,
249 content: impl AsRef<[u8]>,
250 ) {
251 let path = path.as_ref();
252 let mut virtual_files = self.inner.virtual_files.write().unwrap();
253 let content = content.as_ref().to_vec().into_boxed_slice();
254
255 match virtual_files.entry(path.to_path_buf()) {
256 std::collections::hash_map::Entry::Vacant(entry) => {
257 entry.insert(File {
258 content,
259 last_modified: file_time_now(),
260 });
261 }
262 std::collections::hash_map::Entry::Occupied(mut entry) => {
263 entry.get_mut().content = content;
264 }
265 }
266 }
267
268 pub fn walk_directory(&self, path: impl AsRef<SystemPath>) -> WalkDirectoryBuilder {
273 WalkDirectoryBuilder::new(path, MemoryWalker { fs: self.clone() })
274 }
275
276 pub fn remove_file(&self, path: impl AsRef<SystemPath>) -> Result<()> {
277 fn remove_file(fs: &MemoryFileSystem, path: &SystemPath) -> Result<()> {
278 let mut by_path = fs.inner.by_path.write().unwrap();
279 let normalized = fs.normalize_path(path);
280
281 match by_path.entry(normalized) {
282 btree_map::Entry::Occupied(entry) => match entry.get() {
283 Entry::File(_) => {
284 let parent = entry.key().parent().map(Utf8Path::to_path_buf);
285 entry.remove();
286 if let Some(parent) = parent {
287 touch_directory(&mut by_path, &parent);
288 }
289 Ok(())
290 }
291 Entry::Directory(_) => Err(io::Error::from(io::ErrorKind::IsADirectory)),
292 },
293 btree_map::Entry::Vacant(_) => Err(not_found()),
294 }
295 }
296
297 remove_file(self, path.as_ref())
298 }
299
300 pub fn remove_virtual_file(&self, path: impl AsRef<SystemVirtualPath>) -> Result<()> {
301 let mut virtual_files = self.inner.virtual_files.write().unwrap();
302 match virtual_files.entry(path.as_ref().to_path_buf()) {
303 std::collections::hash_map::Entry::Occupied(entry) => {
304 entry.remove();
305 Ok(())
306 }
307 std::collections::hash_map::Entry::Vacant(_) => Err(not_found()),
308 }
309 }
310
311 pub fn touch(&self, path: impl AsRef<SystemPath>) -> Result<()> {
315 let mut by_path = self.inner.by_path.write().unwrap();
316 let normalized = self.normalize_path(path.as_ref());
317
318 get_or_create_file(&mut by_path, &normalized)?.last_modified = file_time_now();
319
320 Ok(())
321 }
322
323 pub fn create_directory_all(&self, path: impl AsRef<SystemPath>) -> Result<()> {
325 let mut by_path = self.inner.by_path.write().unwrap();
326 let normalized = self.normalize_path(path.as_ref());
327
328 create_dir_all(&mut by_path, &normalized)
329 }
330
331 pub fn remove_directory(&self, path: impl AsRef<SystemPath>) -> Result<()> {
338 fn remove_directory(fs: &MemoryFileSystem, path: &SystemPath) -> Result<()> {
339 let mut by_path = fs.inner.by_path.write().unwrap();
340 let normalized = fs.normalize_path(path);
341
342 for (maybe_child, _) in by_path.range(normalized.clone()..).skip(1) {
345 if maybe_child.starts_with(&normalized) {
346 return Err(io::Error::from(io::ErrorKind::DirectoryNotEmpty));
347 } else if !maybe_child.as_str().starts_with(normalized.as_str()) {
348 break;
349 }
350 }
351
352 match by_path.entry(normalized.clone()) {
353 btree_map::Entry::Occupied(entry) => match entry.get() {
354 Entry::Directory(_) => {
355 let parent = entry.key().parent().map(Utf8Path::to_path_buf);
356 entry.remove();
357 if let Some(parent) = parent {
358 touch_directory(&mut by_path, &parent);
359 }
360 Ok(())
361 }
362 Entry::File(_) => Err(io::Error::from(io::ErrorKind::NotADirectory)),
363 },
364 btree_map::Entry::Vacant(_) => Err(not_found()),
365 }
366 }
367
368 remove_directory(self, path.as_ref())
369 }
370
371 fn normalize_path(&self, path: impl AsRef<SystemPath>) -> Utf8PathBuf {
372 let normalized = SystemPath::absolute(path, &self.inner.cwd);
373 normalized.into_utf8_path_buf()
374 }
375
376 pub fn read_directory(&self, path: impl AsRef<SystemPath>) -> Result<ReadDirectory> {
377 let by_path = self.inner.by_path.read().unwrap();
378 let normalized = self.normalize_path(path.as_ref());
379 let entry = by_path.get(&normalized).ok_or_else(not_found)?;
380 if entry.is_file() {
381 return Err(io::Error::from(io::ErrorKind::NotADirectory));
382 };
383
384 let collected = by_path
388 .range(normalized.clone()..)
389 .skip(1)
390 .take_while(|(path, _)| path.starts_with(&normalized))
391 .filter_map(|(path, entry)| {
392 if path.parent()? == normalized {
393 Some(Ok(DirectoryEntry {
394 path: SystemPathBuf::from_utf8_path_buf(path.to_owned()),
395 file_type: entry.file_type(),
396 }))
397 } else {
398 None
399 }
400 })
401 .collect();
402
403 Ok(ReadDirectory::new(collected))
404 }
405
406 pub fn remove_all(&self) {
408 self.inner.virtual_files.write().unwrap().clear();
409
410 self.inner
411 .by_path
412 .write()
413 .unwrap()
414 .retain(|key, _| key == self.inner.cwd.as_utf8_path());
415 }
416}
417
418impl Default for MemoryFileSystem {
419 fn default() -> Self {
420 MemoryFileSystem::new()
421 }
422}
423
424impl std::fmt::Debug for MemoryFileSystem {
425 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 let paths = self.inner.by_path.read().unwrap();
427
428 f.debug_map().entries(paths.iter()).finish()
429 }
430}
431
432struct MemoryFileSystemInner {
433 by_path: RwLock<BTreeMap<Utf8PathBuf, Entry>>,
434 virtual_files: RwLock<FxHashMap<SystemVirtualPathBuf, File>>,
435 cwd: SystemPathBuf,
436}
437
438#[derive(Debug)]
439enum Entry {
440 File(File),
441 Directory(Directory),
442}
443
444impl Entry {
445 const fn is_file(&self) -> bool {
446 matches!(self, Entry::File(_))
447 }
448
449 const fn file_type(&self) -> FileType {
450 match self {
451 Self::File(_) => FileType::File,
452 Self::Directory(_) => FileType::Directory,
453 }
454 }
455}
456
457#[derive(Debug)]
458struct File {
459 content: Box<[u8]>,
460 last_modified: FileTime,
461}
462
463#[derive(Debug)]
464struct Directory {
465 last_modified: FileTime,
466}
467
468fn not_found() -> std::io::Error {
469 std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory")
470}
471
472fn invalid_utf8() -> std::io::Error {
473 std::io::Error::new(
474 std::io::ErrorKind::InvalidData,
475 "stream did not contain valid UTF-8",
476 )
477}
478
479fn create_dir_all(
480 paths: &mut RwLockWriteGuard<BTreeMap<Utf8PathBuf, Entry>>,
481 normalized: &Utf8Path,
482) -> Result<()> {
483 let mut path = Utf8PathBuf::new();
484
485 for component in normalized.components() {
486 path.push(component);
487 let mut inserted = false;
488 let entry = paths.entry(path.clone()).or_insert_with(|| {
489 inserted = true;
490 Entry::Directory(Directory {
491 last_modified: file_time_now(),
492 })
493 });
494
495 if entry.is_file() {
496 return Err(io::Error::from(io::ErrorKind::NotADirectory));
497 }
498
499 if inserted && let Some(parent) = path.parent().map(Utf8Path::to_path_buf) {
500 touch_directory(paths, &parent);
501 }
502 }
503
504 Ok(())
505}
506
507fn touch_directory(paths: &mut RwLockWriteGuard<BTreeMap<Utf8PathBuf, Entry>>, path: &Utf8Path) {
508 if let Some(Entry::Directory(directory)) = paths.get_mut(path) {
509 directory.last_modified = file_time_now();
510 }
511}
512
513fn get_or_create_file<'a>(
514 paths: &'a mut RwLockWriteGuard<BTreeMap<Utf8PathBuf, Entry>>,
515 normalized: &Utf8Path,
516) -> Result<&'a mut File> {
517 if let Some(parent) = normalized.parent() {
518 let parent_entry = paths.get(parent).ok_or_else(not_found)?;
519
520 if parent_entry.is_file() {
521 return Err(io::Error::from(io::ErrorKind::NotADirectory));
522 }
523 }
524
525 if let Some(parent) = normalized.parent().map(Utf8Path::to_path_buf)
526 && !paths.contains_key(normalized)
527 {
528 touch_directory(paths, &parent);
529 }
530
531 let entry = paths.entry(normalized.to_path_buf()).or_insert_with(|| {
532 Entry::File(File {
533 content: Box::default(),
534 last_modified: file_time_now(),
535 })
536 });
537
538 match entry {
539 Entry::File(file) => Ok(file),
540 Entry::Directory(_) => Err(io::Error::from(io::ErrorKind::IsADirectory)),
541 }
542}
543
544#[derive(Debug)]
545pub struct ReadDirectory {
546 entries: std::vec::IntoIter<Result<DirectoryEntry>>,
547}
548
549impl ReadDirectory {
550 fn new(entries: Vec<Result<DirectoryEntry>>) -> Self {
551 Self {
552 entries: entries.into_iter(),
553 }
554 }
555}
556
557impl Iterator for ReadDirectory {
558 type Item = std::io::Result<DirectoryEntry>;
559
560 fn next(&mut self) -> Option<Self::Item> {
561 self.entries.next()
562 }
563}
564
565impl FusedIterator for ReadDirectory {}
566
567struct MemoryIgnoreIncremental {
568 ignore_hidden: bool,
569}
570
571impl IgnoreIncremental for MemoryIgnoreIncremental {
572 fn is_ignored(&mut self, path: &SystemPath, is_directory: bool) -> bool {
573 self.ignore_hidden && !is_directory && is_hidden(path)
578 }
579}
580
581#[derive(Debug)]
583struct MemoryWalker {
584 fs: MemoryFileSystem,
585}
586
587impl MemoryWalker {
588 fn visit_entry(
589 &self,
590 visitor: &mut dyn WalkDirectoryVisitor,
591 entry: walk_directory::DirectoryEntry,
592 queue: &mut Vec<WalkerState>,
593 ignore_hidden: bool,
594 ) -> WalkState {
595 if entry.file_type().is_directory() {
596 let path = entry.path.clone();
597 let depth = entry.depth;
598
599 let state = visitor.visit(Ok(entry));
600
601 if matches!(state, WalkState::Continue) {
602 queue.push(WalkerState::Nested {
603 path,
604 depth: depth + 1,
605 });
606 }
607
608 state
609 } else if ignore_hidden && is_hidden(&entry.path) {
610 WalkState::Skip
611 } else {
612 visitor.visit(Ok(entry))
613 }
614 }
615}
616
617impl DirectoryWalker for MemoryWalker {
618 fn walk(
619 &self,
620 builder: &mut dyn WalkDirectoryVisitorBuilder,
621 configuration: WalkDirectoryConfiguration,
622 ) {
623 let WalkDirectoryConfiguration {
624 paths,
625 ignore_hidden,
626 standard_filters: _,
627 } = configuration;
628
629 let mut visitor = builder.build();
630 let mut queue: Vec<_> = paths
631 .into_iter()
632 .map(|path| WalkerState::Start { path })
633 .collect();
634
635 while let Some(state) = queue.pop() {
636 let (path, depth) = match state {
637 WalkerState::Start { path } => {
638 match self.fs.metadata(&path) {
639 Ok(metadata) => {
640 let entry = walk_directory::DirectoryEntry {
641 file_type: metadata.file_type,
642 depth: 0,
643 path,
644 };
645
646 if self.visit_entry(&mut *visitor, entry, &mut queue, ignore_hidden)
647 == WalkState::Quit
648 {
649 return;
650 }
651 }
652 Err(error) => {
653 visitor.visit(Err(walk_directory::Error {
654 depth: Some(0),
655 kind: walk_directory::ErrorKind::Io {
656 path: Some(path),
657 err: error,
658 },
659 }));
660 }
661 }
662
663 continue;
664 }
665 WalkerState::Nested { path, depth } => (path, depth),
666 };
667
668 let entries = match self.fs.read_directory(&path) {
671 Ok(entries) => entries,
672 Err(error) => {
673 visitor.visit(Err(walk_directory::Error {
674 depth: Some(depth),
675 kind: walk_directory::ErrorKind::Io {
676 path: Some(path),
677 err: error,
678 },
679 }));
680
681 continue;
682 }
683 };
684
685 for entry in entries {
686 match entry {
687 Ok(entry) => {
688 let entry = walk_directory::DirectoryEntry {
689 file_type: entry.file_type,
690 depth,
691 path: entry.path,
692 };
693
694 if self.visit_entry(&mut *visitor, entry, &mut queue, ignore_hidden)
695 == WalkState::Quit
696 {
697 return;
698 }
699 }
700
701 Err(error) => {
702 visitor.visit(Err(walk_directory::Error {
703 depth: Some(depth),
704 kind: walk_directory::ErrorKind::Io {
705 path: Some(path.clone()),
706 err: error,
707 },
708 }));
709 }
710 }
711 }
712 }
713 }
714
715 fn incremental_matcher(
716 &self,
717 configuration: WalkDirectoryConfiguration,
718 ) -> Box<dyn IgnoreIncremental> {
719 let WalkDirectoryConfiguration { ignore_hidden, .. } = configuration;
720 Box::new(MemoryIgnoreIncremental { ignore_hidden })
721 }
722}
723
724#[derive(Debug)]
725enum WalkerState {
726 Start { path: SystemPathBuf },
728
729 Nested { path: SystemPathBuf, depth: usize },
731}
732
733fn is_hidden(path: &SystemPath) -> bool {
734 path.file_name().is_some_and(|name| name.starts_with('.'))
735}
736
737#[cfg(test)]
738mod tests {
739 use std::io::ErrorKind;
740
741 use std::time::Duration;
742
743 use crate::system::walk_directory::WalkState;
744 use crate::system::walk_directory::tests::DirectoryEntryToString;
745 use crate::system::{
746 DirectoryEntry, FileType, MemoryFileSystem, Result, SystemPath, SystemPathBuf,
747 SystemVirtualPath,
748 };
749
750 fn with_files<P>(files: impl IntoIterator<Item = P>) -> super::MemoryFileSystem
754 where
755 P: AsRef<SystemPath>,
756 {
757 let fs = MemoryFileSystem::new();
758 fs.write_files_all(files.into_iter().map(|path| (path, "")))
759 .unwrap();
760
761 fs
762 }
763
764 #[test]
765 fn is_file() {
766 let path = SystemPath::new("a.py");
767 let fs = with_files([path]);
768
769 assert!(fs.is_file(path));
770 assert!(!fs.is_directory(path));
771 }
772
773 #[test]
774 fn exists() {
775 let fs = with_files(["a.py"]);
776
777 assert!(fs.exists(SystemPath::new("a.py")));
778 assert!(!fs.exists(SystemPath::new("b.py")));
779 }
780
781 #[test]
782 fn exists_directories() {
783 let fs = with_files(["a/b/c.py"]);
784
785 assert!(fs.exists(SystemPath::new("a")));
786 assert!(fs.exists(SystemPath::new("a/b")));
787 assert!(fs.exists(SystemPath::new("a/b/c.py")));
788 }
789
790 #[test]
791 fn path_normalization() {
792 let fs = with_files(["a.py"]);
793
794 assert!(fs.exists(SystemPath::new("a.py")));
795 assert!(fs.exists(SystemPath::new("/a.py")));
796 assert!(fs.exists(SystemPath::new("/b/./../a.py")));
797 }
798
799 #[test]
800 fn permissions() -> Result<()> {
801 let fs = with_files(["a.py"]);
802
803 assert_eq!(
805 fs.metadata(SystemPath::new("a.py"))?.permissions(),
806 Some(MemoryFileSystem::PERMISSION)
807 );
808
809 Ok(())
810 }
811
812 #[test]
813 fn touch() -> Result<()> {
814 let fs = MemoryFileSystem::new();
815 let path = SystemPath::new("a.py");
816
817 fs.touch(path)?;
819
820 assert!(fs.exists(path));
821
822 let timestamp1 = fs.metadata(path)?.revision();
823
824 std::thread::sleep(Duration::from_millis(1));
826
827 fs.touch(path)?;
828
829 let timestamp2 = fs.metadata(path)?.revision();
830
831 assert_ne!(timestamp1, timestamp2);
832
833 Ok(())
834 }
835
836 #[test]
837 fn touch_new_file_updates_parent_directory() -> Result<()> {
838 let fs = MemoryFileSystem::new();
839 let directory = SystemPath::new("src");
840 fs.create_directory_all(directory)?;
841 let before = fs.metadata(directory)?.revision();
842
843 std::thread::sleep(Duration::from_millis(1));
845
846 fs.touch(SystemPath::new("src/new.py"))?;
847 let after = fs.metadata(directory)?.revision();
848
849 assert_ne!(before, after);
850
851 Ok(())
852 }
853
854 #[test]
855 fn create_dir_all() {
856 let fs = MemoryFileSystem::new();
857
858 fs.create_directory_all(SystemPath::new("a/b/c")).unwrap();
859
860 assert!(fs.is_directory(SystemPath::new("a")));
861 assert!(fs.is_directory(SystemPath::new("a/b")));
862 assert!(fs.is_directory(SystemPath::new("a/b/c")));
863
864 fs.create_directory_all(SystemPath::new("a/b/c")).unwrap();
866 }
867
868 #[test]
869 fn create_dir_all_fails_if_a_component_is_a_file() {
870 let fs = with_files(["a/b.py"]);
871
872 let error = fs
873 .create_directory_all(SystemPath::new("a/b.py/c"))
874 .unwrap_err();
875 assert_eq!(error.kind(), ErrorKind::NotADirectory);
876 }
877
878 #[test]
879 fn write_file_fails_if_a_parent_directory_is_missing() {
880 let fs = with_files(["c.py"]);
881
882 let error = fs
883 .write_file(SystemPath::new("a/b.py"), "content")
884 .unwrap_err();
885
886 assert_eq!(error.kind(), ErrorKind::NotFound);
887 }
888
889 #[test]
890 fn write_file_all_fails_if_a_component_is_a_file() {
891 let fs = with_files(["a/b.py"]);
892
893 let error = fs
894 .write_file_all(SystemPath::new("a/b.py/c"), "content")
895 .unwrap_err();
896
897 assert_eq!(error.kind(), ErrorKind::NotADirectory);
898 }
899
900 #[test]
901 fn write_virtual_file() {
902 let fs = MemoryFileSystem::new();
903
904 fs.write_virtual_file("a", "content");
905
906 let error = fs.read_to_string("a").unwrap_err();
907 assert_eq!(error.kind(), ErrorKind::NotFound);
908
909 assert_eq!(fs.read_virtual_path_to_string("a").unwrap(), "content");
910 }
911
912 #[test]
913 fn read() -> Result<()> {
914 let fs = MemoryFileSystem::new();
915 let path = SystemPath::new("a.py");
916
917 fs.write_file_all(path, "Test content")?;
918
919 assert_eq!(fs.read_to_string(path)?, "Test content");
920
921 Ok(())
922 }
923
924 #[test]
925 fn read_fails_if_path_is_a_directory() -> Result<()> {
926 let fs = MemoryFileSystem::new();
927
928 fs.create_directory_all("a")?;
929
930 let error = fs.read_to_string(SystemPath::new("a")).unwrap_err();
931
932 assert_eq!(error.kind(), ErrorKind::IsADirectory);
933
934 Ok(())
935 }
936
937 #[test]
938 fn read_fails_if_path_doesnt_exist() -> Result<()> {
939 let fs = MemoryFileSystem::new();
940
941 let error = fs.read_to_string(SystemPath::new("a")).unwrap_err();
942
943 assert_eq!(error.kind(), ErrorKind::NotFound);
944
945 Ok(())
946 }
947
948 #[test]
949 fn write_file_fails_if_path_points_to_a_directory() -> Result<()> {
950 let fs = MemoryFileSystem::new();
951
952 fs.create_directory_all("a")?;
953
954 let error = fs.write_file(SystemPath::new("a"), "content").unwrap_err();
955
956 assert_eq!(error.kind(), ErrorKind::IsADirectory);
957
958 Ok(())
959 }
960
961 #[test]
962 fn read_fails_if_virtual_path_doesnt_exit() {
963 let fs = MemoryFileSystem::new();
964
965 let error = fs.read_virtual_path_to_string("a").unwrap_err();
966
967 assert_eq!(error.kind(), ErrorKind::NotFound);
968 }
969
970 #[test]
971 fn remove_file() -> Result<()> {
972 let fs = with_files(["a/a.py", "b.py"]);
973
974 fs.remove_file("a/a.py")?;
975
976 assert!(!fs.exists(SystemPath::new("a/a.py")));
977
978 assert!(fs.exists(SystemPath::new("a")));
980
981 assert!(fs.exists(SystemPath::new("b.py")));
983
984 Ok(())
985 }
986
987 #[test]
988 fn remove_virtual_file() {
989 let fs = MemoryFileSystem::new();
990 fs.write_virtual_file("a", "content");
991 fs.write_virtual_file("b", "content");
992
993 fs.remove_virtual_file("a").unwrap();
994
995 assert!(!fs.virtual_path_exists(SystemVirtualPath::new("a")));
996 assert!(fs.virtual_path_exists(SystemVirtualPath::new("b")));
997 }
998
999 #[test]
1000 fn remove_non_existing_file() {
1001 let fs = with_files(["b.py"]);
1002
1003 let error = fs.remove_file("a.py").unwrap_err();
1004
1005 assert_eq!(error.kind(), ErrorKind::NotFound);
1006 }
1007
1008 #[test]
1009 fn remove_file_that_is_a_directory() -> Result<()> {
1010 let fs = MemoryFileSystem::new();
1011 fs.create_directory_all("a")?;
1012
1013 let error = fs.remove_file("a").unwrap_err();
1014 assert_eq!(error.kind(), ErrorKind::IsADirectory);
1015
1016 Ok(())
1017 }
1018
1019 #[test]
1020 fn remove_directory() -> Result<()> {
1021 let fs = with_files(["b.py"]);
1022 fs.create_directory_all("a")?;
1023
1024 fs.remove_directory("a")?;
1025
1026 assert!(!fs.exists(SystemPath::new("a")));
1027
1028 assert!(fs.exists(SystemPath::new("b.py")));
1030
1031 Ok(())
1032 }
1033
1034 #[test]
1035 fn remove_non_empty_directory() {
1036 let fs = with_files(["a/a.py"]);
1037
1038 let error = fs.remove_directory("a").unwrap_err();
1039 assert_eq!(error.kind(), ErrorKind::DirectoryNotEmpty);
1040 }
1041
1042 #[test]
1043 fn remove_directory_with_files_that_start_with_the_same_string() -> Result<()> {
1044 let fs = with_files(["foo_bar.py", "foob.py"]);
1045 fs.create_directory_all("foo")?;
1046
1047 fs.remove_directory("foo").unwrap();
1048
1049 assert!(!fs.exists(SystemPath::new("foo")));
1050 assert!(fs.exists(SystemPath::new("foo_bar.py")));
1051 assert!(fs.exists(SystemPath::new("foob.py")));
1052
1053 Ok(())
1054 }
1055
1056 #[test]
1057 fn remove_non_existing_directory() {
1058 let fs = MemoryFileSystem::new();
1059
1060 let error = fs.remove_directory("a").unwrap_err();
1061 assert_eq!(error.kind(), ErrorKind::NotFound);
1062 }
1063
1064 #[test]
1065 fn remove_directory_that_is_a_file() {
1066 let fs = with_files(["a"]);
1067
1068 let error = fs.remove_directory("a").unwrap_err();
1069 assert_eq!(error.kind(), ErrorKind::NotADirectory);
1070 }
1071
1072 #[test]
1073 fn read_directory() {
1074 let fs = with_files(["b.ts", "a/bar.py", "d.rs", "a/foo/bar.py", "a/baz.pyi"]);
1075 let contents: Vec<DirectoryEntry> = fs
1076 .read_directory("a")
1077 .unwrap()
1078 .map(Result::unwrap)
1079 .collect();
1080 let expected_contents = vec![
1081 DirectoryEntry::new(SystemPathBuf::from("/a/bar.py"), FileType::File),
1082 DirectoryEntry::new(SystemPathBuf::from("/a/baz.pyi"), FileType::File),
1083 DirectoryEntry::new(SystemPathBuf::from("/a/foo"), FileType::Directory),
1084 ];
1085 assert_eq!(contents, expected_contents)
1086 }
1087
1088 #[test]
1089 fn read_directory_nonexistent() {
1090 let fs = MemoryFileSystem::new();
1091 let Err(error) = fs.read_directory("doesnt_exist") else {
1092 panic!("Expected this to fail");
1093 };
1094 assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
1095 }
1096
1097 #[test]
1098 fn read_directory_on_file() {
1099 let fs = with_files(["a.py"]);
1100 let Err(error) = fs.read_directory("a.py") else {
1101 panic!("Expected this to fail");
1102 };
1103 assert_eq!(error.kind(), std::io::ErrorKind::NotADirectory);
1104 }
1105
1106 #[test]
1107 fn walk_directory() -> std::io::Result<()> {
1108 let root = SystemPath::new("/src");
1109 let system = MemoryFileSystem::with_current_directory(root);
1110
1111 system.write_files_all([
1112 (root.join("foo.py"), "print('foo')"),
1113 (root.join("a/bar.py"), "print('bar')"),
1114 (root.join("a/baz.py"), "print('baz')"),
1115 (root.join("a/b/c.py"), "print('c')"),
1116 ])?;
1117
1118 let writer = DirectoryEntryToString::new(root.to_path_buf());
1119
1120 system.walk_directory(root).run(|| {
1121 Box::new(|entry| {
1122 writer.write_entry(entry);
1123
1124 WalkState::Continue
1125 })
1126 });
1127
1128 assert_eq!(
1129 writer.to_string(),
1130 r#"{
1131 "": (
1132 Directory,
1133 0,
1134 ),
1135 "a": (
1136 Directory,
1137 1,
1138 ),
1139 "a/b": (
1140 Directory,
1141 2,
1142 ),
1143 "a/b/c.py": (
1144 File,
1145 3,
1146 ),
1147 "a/bar.py": (
1148 File,
1149 2,
1150 ),
1151 "a/baz.py": (
1152 File,
1153 2,
1154 ),
1155 "foo.py": (
1156 File,
1157 1,
1158 ),
1159}"#
1160 );
1161
1162 Ok(())
1163 }
1164
1165 #[test]
1166 fn walk_directory_hidden() -> std::io::Result<()> {
1167 let root = SystemPath::new("/src");
1168 let system = MemoryFileSystem::with_current_directory(root);
1169
1170 system.write_files_all([
1171 (root.join("foo.py"), "print('foo')"),
1172 (root.join("a/bar.py"), "print('bar')"),
1173 (root.join("a/.baz.py"), "print('baz')"),
1174 ])?;
1175
1176 let writer = DirectoryEntryToString::new(root.to_path_buf());
1177
1178 system.walk_directory(root).run(|| {
1179 Box::new(|entry| {
1180 writer.write_entry(entry);
1181
1182 WalkState::Continue
1183 })
1184 });
1185
1186 assert_eq!(
1187 writer.to_string(),
1188 r#"{
1189 "": (
1190 Directory,
1191 0,
1192 ),
1193 "a": (
1194 Directory,
1195 1,
1196 ),
1197 "a/bar.py": (
1198 File,
1199 2,
1200 ),
1201 "foo.py": (
1202 File,
1203 1,
1204 ),
1205}"#
1206 );
1207
1208 Ok(())
1209 }
1210
1211 #[test]
1212 fn walk_directory_file() -> std::io::Result<()> {
1213 let root = SystemPath::new("/src");
1214 let system = MemoryFileSystem::with_current_directory(root);
1215
1216 system.write_file_all(root.join("foo.py"), "print('foo')")?;
1217
1218 let writer = DirectoryEntryToString::new(root.to_path_buf());
1219
1220 system.walk_directory(root.join("foo.py")).run(|| {
1221 Box::new(|entry| {
1222 writer.write_entry(entry);
1223
1224 WalkState::Continue
1225 })
1226 });
1227
1228 assert_eq!(
1229 writer.to_string(),
1230 r#"{
1231 "foo.py": (
1232 File,
1233 0,
1234 ),
1235}"#
1236 );
1237
1238 Ok(())
1239 }
1240}