Skip to main content

ruff_db/system/
memory_fs.rs

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/// File system that stores all content in memory.
21///
22/// The file system supports files and directories. Paths are case-sensitive.
23///
24/// The implementation doesn't aim at fully capturing the behavior of a real file system.
25/// The implementation intentionally doesn't support:
26/// * symlinks
27/// * hardlinks
28/// * permissions: All files and directories have the permission 0755.
29///
30/// Use a tempdir with the real file system to test these advanced file system features and behavior.
31#[derive(Clone)]
32pub struct MemoryFileSystem {
33    inner: Arc<MemoryFileSystemInner>,
34}
35
36impl MemoryFileSystem {
37    /// Permission used by all files and directories
38    const PERMISSION: u32 = 0o755;
39
40    pub fn new() -> Self {
41        Self::with_current_directory("/")
42    }
43
44    /// Creates a new, empty in memory file system with the given current working directory.
45    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        // Mimic the behavior of a real FS where canonicalize errors if the `path` doesn't exist
99        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        // Since the permissions of every file are 755,
119        // it follows that every file is executable.
120        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    /// Stores a new file in the file system.
190    ///
191    /// The operation overrides the content for an existing file with the same normalized `path`.
192    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    /// Writes the files to the file system.
208    ///
209    /// The operation overrides existing files with the same normalized path.
210    ///
211    /// Enclosing directories are automatically created if they don't exist.
212    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    /// Stores a new file in the file system.
225    ///
226    /// The operation overrides the content for an existing file with the same normalized `path`.
227    ///
228    /// Enclosing directories are automatically created if they don't exist.
229    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    /// Stores a new virtual file in the file system.
244    ///
245    /// The operation overrides the content for an existing virtual file with the same `path`.
246    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    /// Returns a builder for walking the directory tree of `path`.
269    ///
270    /// The only files that are ignored when setting `WalkDirectoryBuilder::standard_filters`
271    /// are hidden files (files with a name starting with a `.`).
272    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    /// Sets the last modified timestamp of the file stored at `path` to now.
312    ///
313    /// Creates a new file if the file at `path` doesn't exist.
314    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    /// Creates a directory at `path`. All enclosing directories are created if they don't exist.
324    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    /// Deletes the directory at `path`.
332    ///
333    /// ## Errors
334    /// * If the directory is not empty
335    /// * The `path` is not a directory
336    /// * The `path` does not exist
337    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            // Test if the directory is empty
343            // Skip the directory path itself
344            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        // Collect the entries into a vector to avoid deadlocks when the
385        // consumer calls into other file system methods while iterating over the
386        // directory entries.
387        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    /// Removes all files and directories except the current working directory.
407    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        // This matches the semantics of the in-memory recursive
574        // directory traversal. That is, the only thing we care
575        // about filtering is hidden files. We let everything else
576        // through.
577        self.ignore_hidden && !is_directory && is_hidden(path)
578    }
579}
580
581/// Recursively walks a directory in the memory file system.
582#[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            // Use `read_directory` here instead of locking `by_path` to avoid deadlocks
669            // when the `visitor` calls any file system operations.
670            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    /// An entry path that was directly provided to the walker. Always has depth 0.
727    Start { path: SystemPathBuf },
728
729    /// Traverse into the directory with the given path at the given depth.
730    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    /// Creates a file system with the given files.
751    ///
752    /// The content of all files will be empty.
753    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        // The default permissions match the default on Linux: 0755
804        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        // Creates a file if it doesn't exist
818        fs.touch(path)?;
819
820        assert!(fs.exists(path));
821
822        let timestamp1 = fs.metadata(path)?.revision();
823
824        // Sleep to ensure that the timestamp changes
825        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        // Sleep to ensure that the timestamp changes
844        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        // Should not fail if the directory already exists
865        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        // It doesn't delete the enclosing directories
979        assert!(fs.exists(SystemPath::new("a")));
980
981        // It doesn't delete unrelated files.
982        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        // It doesn't delete unrelated files.
1029        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}