about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/actions/create.rs110
-rw-r--r--src/filesystem.rs624
2 files changed, 299 insertions, 435 deletions
diff --git a/src/actions/create.rs b/src/actions/create.rs
index 184e249..bc7f7b0 100644
--- a/src/actions/create.rs
+++ b/src/actions/create.rs
@@ -21,113 +21,3 @@ pub fn create(command_options: ActionOptions, fs: &impl Fs, timestamp: u64) -> R
 
     Ok(())
 }
-
-#[cfg(test)]
-mod tests {
-    use std::{path::Path, vec};
-
-    use crate::{
-        actions::{create, ActionOptions},
-        diff::ContentChange,
-        filesystem::mock::{EntryMock, ExpectedCall as Call, ExpectedCallVariant as Type, FsMock},
-        history::{
-            FileChange, FileChangeVariant, FileHistory, RepositoryChange, RepositoryHistory,
-        },
-    };
-
-    #[test]
-    fn create_empty() {
-        let now = 0xC0FFEE;
-        let fs_mock = FsMock::new();
-        let options = ActionOptions::from_path(".");
-
-        let pwd = Path::new(".");
-        let working_file = Path::new("./test");
-        let history_file = Path::new("./.ka/files/test");
-
-        let ka = Path::new("./.ka");
-        let ka_files = Path::new("./.ka/files");
-        let ka_index = Path::new("./.ka/index");
-
-        let empty_index = RepositoryHistory::default().encode().unwrap();
-        let expected_index = {
-            let mut history = RepositoryHistory::default();
-            history.add_change(RepositoryChange {
-                affected_files: vec![working_file.to_path_buf()],
-                timestamp: now,
-            });
-            history.cursor = 1;
-            history.encode().unwrap()
-        };
-
-        let expected_file_history = {
-            let mut history = FileHistory::default();
-            let change = ContentChange::Inserted {
-                at: 0,
-                new_content: vec![1, 2, 3],
-            };
-            history.add_change(FileChange {
-                change_index: 1,
-                variant: FileChangeVariant::Updated(vec![change]),
-            });
-            history.encode().unwrap()
-        };
-
-        fs_mock.set_expected_calls(vec![
-            // Create calls
-            Call::new(ka, Type::PathExists(false)),
-            Call::new(ka, Type::CreateDirectory),
-            Call::new(ka_files, Type::CreateDirectory),
-            Call::new(ka_index, Type::CreateFile),
-            Call::new(
-                ka_index,
-                Type::WriteToFile {
-                    expected: empty_index.clone(),
-                },
-            ),
-            // Update calls
-            Call::new(ka_index, Type::OpenWritableFile),
-            Call::new(
-                ka_index,
-                Type::ReadFile {
-                    returned: empty_index,
-                },
-            ),
-            Call::new(
-                pwd,
-                Type::ReadDirectory {
-                    returned: vec![
-                        EntryMock::new(working_file, false),
-                        EntryMock::new(ka, true),
-                    ],
-                },
-            ),
-            Call::new(ka_files, Type::ReadDirectory { returned: vec![] }),
-            Call::new(history_file, Type::PathExists(false)),
-            Call::new(working_file, Type::OpenReadableFile),
-            Call::new(
-                working_file,
-                Type::ReadFile {
-                    returned: vec![1, 2, 3],
-                },
-            ),
-            Call::new(history_file, Type::CreateFile),
-            Call::new(
-                history_file,
-                Type::WriteToFile {
-                    expected: expected_file_history,
-                },
-            ),
-            Call::new(
-                ka_index,
-                Type::WriteToFile {
-                    expected: expected_index,
-                },
-            ),
-        ]);
-
-        create(options, &fs_mock, now).expect("Action failed.");
-
-        fs_mock.assert_calls();
-    }
-}
diff --git a/src/filesystem.rs b/src/filesystem.rs
index f1f3af8..e1ccd2d 100644
--- a/src/filesystem.rs
+++ b/src/filesystem.rs
@@ -121,34 +121,23 @@ impl FsEntry for DirEntry {
 #[allow(dead_code)]
 #[cfg(test)]
 pub mod mock {
-    use anyhow::Result;
+    use anyhow::{anyhow, Result};
     use std::{
+        collections::{hash_map, HashMap},
         path::{Path, PathBuf},
-        sync::{Arc, Mutex},
+        sync::{Arc, Mutex, MutexGuard},
     };
 
     use super::{Fs, FsEntry};
 
-    // TODO: This testing style is very imperative as we have to consider every single
-    // call that happens in an action. (See actions::create::tests)
-    // Could we instead emulate a fake in-memory file system for FsMock, requiring only
-    // an input state and an output state, with no knowledge what happens in between.
-    // That would greatly simplify making tests.
-
     pub struct FsMock {
-        state: Arc<Mutex<FsMockState>>,
-    }
-
-    struct FsMockState {
-        expected_calls: Vec<ExpectedCall>,
-        received_calls: Vec<ReceivedCall>,
+        state: Arc<Mutex<FsState>>,
     }
 
     impl FsMock {
         pub fn new() -> Self {
-            let state = FsMockState {
-                expected_calls: Vec::new(),
-                received_calls: Vec::new(),
+            let state = FsState {
+                entries: HashMap::new(),
             };
 
             FsMock {
@@ -156,393 +145,378 @@ pub mod mock {
             }
         }
 
-        pub fn set_expected_calls(&self, calls: Vec<ExpectedCall>) {
-            let mut state = self.state.lock().expect("File system mock lock poisoned.");
-            state.expected_calls = calls;
-        }
-
-        pub fn assert_calls(self) {
-            let state = self.state.lock().expect("File system lock poisoned.");
-
-            let longest_call_amount = state.received_calls.len().max(state.expected_calls.len());
-
-            for call_index in 0..longest_call_amount {
-                let expected_option = state.expected_calls.get(call_index);
-                let received_option = state.received_calls.get(call_index);
-
-                let expected_call = expected_option.unwrap_or_else(|| {
-                    panic!(
-                        "Received unexpected call: '{:?}'.",
-                        received_option.unwrap()
-                    )
-                });
-                let received_call = received_option.unwrap_or_else(|| {
-                    panic!(
-                        "Expected call: '{:?}', which was not received.",
-                        expected_option.unwrap()
-                    )
-                });
-
-                expected_call.assert_received(received_call);
-            }
+        pub fn set_state(&mut self, new_state: FsState) {
+            let mut state = self.state.lock().expect("FsMock state lock poisoned.");
+            *state = new_state;
         }
 
-        fn add_call(&self, call: ReceivedCall) {
-            let mut state = self.state.lock().expect("File system mock lock poisoned.");
-            state.received_calls.push(call);
+        pub fn assert_match(&self, _expected_state: FsState) {
+            todo!()
         }
 
-        fn get_expected_call(&self) -> Option<ExpectedCallVariant> {
-            let state = self.state.lock().expect("File system lock poisoned.");
-
-            state
-                .expected_calls
-                .get(state.received_calls.len())
-                .map(|e| e.variant.clone())
+        fn state(&self) -> MutexGuard<FsState> {
+            self.state.lock().expect("FsMock state lock poisoned.")
         }
     }
 
-    impl Fs for FsMock {
+    impl<'fs> Fs for FsMock {
         type File = FileMock;
+
         type Entry = EntryMock;
 
         fn create_file(&self, path: &Path) -> Result<Self::File> {
-            let call = ReceivedCall::new(path, ReceivedCallVariant::FileCreated);
-            self.add_call(call);
-
-            Ok(FileMock {
-                path: path.to_path_buf(),
-                writable: true,
-            })
+            let mut state = self.state();
+            if let Some(file) = state.get_or_create_file(path) {
+                Ok(file)
+            } else {
+                if state.is_directory(path) {
+                    Err(anyhow!(
+                        "The file '{}' can't be opened or created, because it is a directory.",
+                        path.display()
+                    ))
+                } else {
+                    Err(anyhow!(
+                        "The file '{}' can't be opened or created, because one of it's parent paths which have to be created is occupied.",
+                        path.display()
+                    ))
+                }
+            }
         }
 
         fn delete_file(&self, path: &Path) -> Result<()> {
-            let call = ReceivedCall::new(path, ReceivedCallVariant::FileDeleted);
-            self.add_call(call);
-
-            Ok(())
+            let mut state = self.state();
+            if state.delete_if_file(path) {
+                Ok(())
+            } else {
+                if state.is_directory(path) {
+                    Err(anyhow!(
+                        "The file '{}' can't be deleted because it is a directory.",
+                        path.display()
+                    ))
+                } else {
+                    Err(anyhow!(
+                        "The file '{}' can't be deleted because it doesn't exist.",
+                        path.display()
+                    ))
+                }
+            }
         }
 
         fn open_readable_file(&self, path: &Path) -> Result<Self::File> {
-            let call = ReceivedCall::new(path, ReceivedCallVariant::ReadableFileOpened);
-            self.add_call(call);
-
-            Ok(FileMock {
-                path: path.to_path_buf(),
-                writable: false,
-            })
+            let state = self.state();
+            if let Some(file) = state.get_file_for_reading(path) {
+                Ok(file)
+            } else {
+                if state.is_directory(path) {
+                    Err(anyhow!(
+                        "The file '{}' can't be opened for reading because it is a directory.",
+                        path.display()
+                    ))
+                } else {
+                    Err(anyhow!(
+                        "The file '{}' can't be opened for reading because it doesn't exist.",
+                        path.display()
+                    ))
+                }
+            }
         }
 
         fn open_writable_file(&self, path: &Path) -> Result<Self::File> {
-            let call = ReceivedCall::new(path, ReceivedCallVariant::WritableFileOpened);
-            self.add_call(call);
-
-            Ok(FileMock {
-                path: path.to_path_buf(),
-                writable: true,
-            })
+            let state = self.state();
+            if let Some(file) = state.get_file(path) {
+                Ok(file)
+            } else {
+                if state.is_directory(path) {
+                    Err(anyhow!("The file '{}' can't be opened for reading and writing because it is a directory.", path.display()))
+                } else {
+                    Err(anyhow!("The file '{}' can't be opened for reading and writing because it doesn't exist.", path.display()))
+                }
+            }
         }
 
         fn create_directory(&self, path: &Path) -> Result<()> {
-            let call = ReceivedCall::new(path, ReceivedCallVariant::DirectoryCreated);
-            self.add_call(call);
-
-            Ok(())
+            let mut state = self.state();
+            if state.create_directory(path) {
+                Ok(())
+            } else {
+                if state.is_directory(path) {
+                    Err(anyhow!(
+                        "The directory '{}' can't be created because it already exists.",
+                        path.display()
+                    ))
+                } else if state.is_file(path) {
+                    Err(anyhow!("The directory '{}' can't be created because there is a file with the same path.", path.display()))
+                } else {
+                    Err(anyhow!(
+                        "The directory '{}' can't be opened or created, because one of it's parent paths which have to be created is occupied.",
+                        path.display()
+                    ))
+                }
+            }
         }
 
         fn read_directory(&self, path: &Path) -> Result<Vec<Self::Entry>> {
-            let call = ReceivedCall::new(path, ReceivedCallVariant::DirectoryRead);
-
-            let return_value = if let Some(expected_call) = self.get_expected_call() {
-                if let ExpectedCallVariant::ReadDirectory { returned } = expected_call {
-                    Ok(returned.to_vec())
+            let state = self.state();
+            if let Some(entries) = state.get_entries_if_directory(path) {
+                Ok(entries)
+            } else {
+                if state.is_file(path) {
+                    Err(anyhow!(
+                        "The directory '{}' can't be read because it is a file.",
+                        path.display()
+                    ))
                 } else {
-                    panic!(
-                        "Received unexpected call: {:?}, expected call: {:?}.",
-                        call, expected_call
-                    );
+                    Err(anyhow!(
+                        "The directory '{}' can't be read because it doesn't exist.",
+                        path.display()
+                    ))
                 }
-            } else {
-                panic!(
-                    "Received unexpected call, with no expected call to compare it to: {:?}.",
-                    call
-                );
-            };
-
-            self.add_call(call);
-
-            return_value
+            }
         }
 
         fn delete_directory(&self, path: &Path) -> Result<()> {
-            let call = ReceivedCall::new(path, ReceivedCallVariant::DirectoryDeleted);
-            self.add_call(call);
-
-            Ok(())
+            let mut state = self.state();
+            if state.delete_if_directory(path) {
+                Ok(())
+            } else {
+                if state.is_file(path) {
+                    Err(anyhow!(
+                        "The directory '{}' can't be deleted because it is a file.",
+                        path.display()
+                    ))
+                } else {
+                    Err(anyhow!(
+                        "The directory '{}' can't be deleted because it doesn't exist.",
+                        path.display()
+                    ))
+                }
+            }
         }
 
         fn write_to_file(&self, file: &mut Self::File, buffer: Vec<u8>) -> Result<()> {
-            let call = ReceivedCall::new(
-                &file.path,
-                ReceivedCallVariant::FileWritten { received: buffer },
-            );
-            self.add_call(call);
-
-            // TODO: Check whether file was opened with write flag.
-
-            Ok(())
-        }
-
-        fn read_from_file(&self, file: &mut Self::File) -> Result<Vec<u8>> {
-            let call = ReceivedCall::new(&file.path, ReceivedCallVariant::FileRead);
-
-            let return_value = if let Some(expected_call) = self.get_expected_call() {
-                if let ExpectedCallVariant::ReadFile { returned } = expected_call {
-                    Ok(returned.clone())
+            let mut state = self.state();
+            if file.writable {
+                if state.write_to_if_file(&file.path, buffer) {
+                    Ok(())
                 } else {
-                    panic!(
-                        "Received unexpected call: {:?}, expected call: {:?}.",
-                        call, expected_call
-                    );
+                    if state.is_directory(&file.path) {
+                        Err(anyhow!(
+                            "The file '{}' can't be written to because it is a directory.",
+                            file.path.display()
+                        ))
+                    } else {
+                        Err(anyhow!(
+                            "The file '{}' can't be written to because it doesn't exist.",
+                            file.path.display()
+                        ))
+                    }
                 }
             } else {
-                panic!(
-                    "Received unexpected call, with no expected call to compare it to: {:?}.",
-                    call
-                );
-            };
-
-            self.add_call(call);
-
-            return_value
+                Err(anyhow!(
+                    "The file '{}' is not writable.",
+                    file.path.display()
+                ))
+            }
         }
 
-        fn path_exists(&self, path: &Path) -> bool {
-            let call = ReceivedCall::new(path, ReceivedCallVariant::DoesPathExist);
-
-            let return_value = if let Some(expected_call) = self.get_expected_call() {
-                if let ExpectedCallVariant::PathExists(answer) = expected_call {
-                    answer
+        fn read_from_file(&self, file: &mut Self::File) -> Result<Vec<u8>> {
+            let state = self.state();
+            if let Some(content) = state.get_content_if_file(&file.path) {
+                Ok(content)
+            } else {
+                if state.is_directory(&file.path) {
+                    Err(anyhow!(
+                        "The file '{}' can't be read from because it is a directory.",
+                        file.path.display()
+                    ))
                 } else {
-                    panic!(
-                        "Received unexpected call: {:?}, expected call: {:?}.",
-                        call, expected_call
-                    );
+                    Err(anyhow!(
+                        "The file '{}' can't be read from because it doesn't exist.",
+                        file.path.display()
+                    ))
                 }
-            } else {
-                panic!(
-                    "Received unexpected call, with no expected call to compare it to: {:?}.",
-                    call
-                );
-            };
-
-            self.add_call(call);
+            }
+        }
 
-            return_value
+        fn path_exists(&self, path: &Path) -> bool {
+            self.state().exists(path)
         }
     }
 
-    #[derive(Debug, Clone)]
-    pub struct EntryMock {
-        path: PathBuf,
-        is_directory: bool,
+    pub struct FsState {
+        entries: HashMap<PathBuf, EntryMock>,
     }
 
-    impl EntryMock {
-        pub fn new(path: &Path, is_directory: bool) -> Self {
-            EntryMock {
-                path: path.to_path_buf(),
-                is_directory,
+    impl FsState {
+        fn get_or_create_file(&mut self, path: &Path) -> Option<FileMock> {
+            if let Some(parent) = path.parent() {
+                if !self.is_directory(parent) && !self.create_directory(path) {
+                    return None;
+                }
             }
-        }
-    }
 
-    impl FsEntry for EntryMock {
-        fn path(&self) -> PathBuf {
-            self.path.clone()
+            let path_buf = path.to_path_buf();
+            match self.entries.entry(path_buf.clone()) {
+                hash_map::Entry::Occupied(occupied) => match occupied.get() {
+                    EntryMock::File(file) => Some(file.clone()),
+                    _ => None,
+                },
+                hash_map::Entry::Vacant(vacant) => {
+                    let file = FileMock {
+                        path: path_buf,
+                        writable: true,
+                        content: Vec::new(),
+                    };
+                    vacant.insert(EntryMock::File(file.clone()));
+                    Some(file)
+                }
+            }
         }
 
-        fn is_directory(&self) -> Result<bool> {
-            Ok(self.is_directory)
+        fn delete_if_file(&mut self, path: &Path) -> bool {
+            if self.is_file(path) {
+                self.entries.remove(path).is_some()
+            } else {
+                false
+            }
         }
-    }
 
-    pub struct FileMock {
-        path: PathBuf,
-        writable: bool,
-    }
+        fn get_file(&self, path: &Path) -> Option<FileMock> {
+            match self.entries.get(path) {
+                Some(entry) => match entry {
+                    EntryMock::File(file) => Some(file.clone()),
+                    _ => None,
+                },
+                _ => None,
+            }
+        }
 
-    // TODO: Do we need ways to return errors?
-    #[derive(Debug)]
-    pub struct ExpectedCall {
-        pub affected_path: PathBuf,
-        pub variant: ExpectedCallVariant,
-    }
+        fn get_file_for_reading(&self, path: &Path) -> Option<FileMock> {
+            self.get_file(path).map(|mut f| {
+                f.writable = false;
+                f
+            })
+        }
 
-    impl ExpectedCall {
-        pub fn new(path: &Path, variant: ExpectedCallVariant) -> Self {
-            ExpectedCall {
-                affected_path: path.to_path_buf(),
-                variant,
-            }
+        fn get_content_if_file(&self, path: &Path) -> Option<Vec<u8>> {
+            self.get_file(path).map(|f| f.content)
         }
 
-        fn assert_received(&self, rec: &ReceivedCall) {
-            if self.affected_path == rec.affected_path {
-                use ExpectedCallVariant as E;
-                use ReceivedCallVariant as R;
-
-                let e = &self.variant;
-
-                let equal = match rec.variant {
-                    R::FileCreated => matches!(e, E::CreateFile),
-                    R::FileDeleted => matches!(e, E::DeleteFile),
-                    R::ReadableFileOpened => matches!(e, E::OpenReadableFile),
-                    R::WritableFileOpened => matches!(e, E::OpenWritableFile),
-                    R::DirectoryCreated => matches!(e, E::CreateDirectory),
-                    R::DirectoryRead => matches!(e, E::ReadDirectory { .. }),
-                    R::DirectoryDeleted => matches!(e, E::DeleteDirectory),
-                    R::FileRead => matches!(e, E::ReadFile { .. }),
-                    R::FileWritten { ref received } => {
-                        if let E::WriteToFile { expected } = e {
-                            expected == received
-                        } else {
-                            false
-                        }
+        fn write_to_if_file(&mut self, path: &Path, buffer: Vec<u8>) -> bool {
+            match self.entries.get_mut(path) {
+                Some(entry) => match entry {
+                    EntryMock::File(file) => {
+                        file.content = buffer;
+                        true
                     }
-                    R::DoesPathExist => matches!(e, E::PathExists(_)),
-                };
+                    _ => false,
+                },
+                _ => false,
+            }
+        }
 
-                if equal {
-                    return;
+        fn create_directory(&mut self, path: &Path) -> bool {
+            if let Some(parent) = path.parent() {
+                if !self.is_directory(parent) && !self.create_directory(path) {
+                    return false;
                 }
             }
 
-            panic!(
-                "
-                Expected call does not equal received call.
-                Expected: {:?}
-                Recevied: {:?}
-                ",
-                self, rec
-            );
+            let path_buf = path.to_path_buf();
+            match self.entries.entry(path_buf.clone()) {
+                hash_map::Entry::Vacant(vacant) => {
+                    vacant.insert(EntryMock::Dir { path: path_buf });
+                    true
+                }
+                _ => false,
+            }
         }
-    }
 
-    #[derive(Debug, Clone)]
-    pub enum ExpectedCallVariant {
-        CreateFile,
-        DeleteFile,
-        OpenReadableFile,
-        OpenWritableFile,
-        CreateDirectory,
-        ReadDirectory { returned: Vec<EntryMock> },
-        DeleteDirectory,
-        ReadFile { returned: Vec<u8> },
-        WriteToFile { expected: Vec<u8> },
-        PathExists(bool),
-    }
-
-    #[derive(Debug)]
-    struct ReceivedCall {
-        affected_path: PathBuf,
-        variant: ReceivedCallVariant,
-    }
-
-    impl ReceivedCall {
-        fn new(path: &Path, variant: ReceivedCallVariant) -> Self {
-            ReceivedCall {
-                affected_path: path.to_path_buf(),
-                variant,
+        fn delete_if_directory(&mut self, path: &Path) -> bool {
+            if self.is_directory(path) {
+                self.entries.remove(path).is_some()
+            } else {
+                false
             }
         }
-    }
 
-    #[derive(Debug)]
-    enum ReceivedCallVariant {
-        FileCreated,
-        FileDeleted,
-        ReadableFileOpened,
-        WritableFileOpened,
-        DirectoryCreated,
-        DirectoryRead,
-        DirectoryDeleted,
-        FileRead,
-        FileWritten { received: Vec<u8> },
-        DoesPathExist,
-    }
-}
+        fn get_entries_if_directory(&self, path: &Path) -> Option<Vec<EntryMock>> {
+            if self.is_directory(path) {
+                let directory_entries = self
+                    .entries
+                    .iter()
+                    .filter(|&(path, _)| {
+                        if let Some(parent) = path.parent() {
+                            parent == path
+                        } else {
+                            false
+                        }
+                    })
+                    .map(|(_, entry)| entry.clone())
+                    .collect();
 
-#[cfg(test)]
-mod tests {
-    use std::path::Path;
+                Some(directory_entries)
+            } else {
+                None
+            }
+        }
 
-    use crate::filesystem::{
-        mock::{ExpectedCall, ExpectedCallVariant},
-        Fs,
-    };
+        fn is_file(&self, path: &Path) -> bool {
+            self.entries
+                .get(path)
+                .map_or(false, |e| matches!(e, EntryMock::File(_)))
+        }
 
-    use super::mock::FsMock;
+        fn is_directory(&self, path: &Path) -> bool {
+            self.entries
+                .get(path)
+                .map_or(false, |e| matches!(e, EntryMock::Dir { .. }))
+        }
 
-    #[test]
-    fn mock_empty() {
-        let fs_mock = FsMock::new();
-        fs_mock.assert_calls();
+        fn exists(&self, path: &Path) -> bool {
+            self.entries.contains_key(path)
+        }
     }
 
-    #[test]
-    fn mock_file_basic() {
-        let fs_mock = FsMock::new();
-
-        let path = Path::new("file").to_path_buf();
-
-        fs_mock.set_expected_calls(vec![
-            ExpectedCall::new(&path, ExpectedCallVariant::CreateFile),
-            ExpectedCall::new(
-                &path,
-                ExpectedCallVariant::ReadFile {
-                    returned: vec![1, 2, 3],
-                },
-            ),
-        ]);
-
-        let mut file = fs_mock.create_file(&path).unwrap();
-        let received_content = fs_mock.read_from_file(&mut file).unwrap();
-
-        assert_eq!(received_content, vec![1, 2, 3]);
-
-        fs_mock.assert_calls();
+    #[derive(Clone)]
+    pub struct FileMock {
+        path: PathBuf,
+        writable: bool,
+        content: Vec<u8>,
     }
 
-    #[test]
-    #[should_panic]
-    fn mock_unexpected_call() {
-        let fs_mock = FsMock::new();
-
-        let path = Path::new("file").to_path_buf();
-
-        fs_mock.set_expected_calls(vec![ExpectedCall::new(
-            &path,
-            ExpectedCallVariant::CreateFile,
-        )]);
-
-        fs_mock.delete_file(&path).unwrap();
-        fs_mock.assert_calls();
+    #[derive(Clone)]
+    pub enum EntryMock {
+        File(FileMock),
+        Dir { path: PathBuf },
     }
 
-    #[test]
-    #[should_panic]
-    fn mock_unequal_calls() {
-        let fs_mock = FsMock::new();
+    impl EntryMock {
+        pub fn file(path_str: &str, content: &[u8]) -> Self {
+            EntryMock::File(FileMock {
+                path: Path::new(path_str).to_path_buf(),
+                writable: true,
+                content: content.to_vec(),
+            })
+        }
 
-        let path = Path::new("file").to_path_buf();
+        pub fn dir(path_str: &str) -> Self {
+            EntryMock::Dir {
+                path: Path::new(path_str).to_path_buf(),
+            }
+        }
+    }
 
-        fs_mock.set_expected_calls(vec![
-            ExpectedCall::new(&path, ExpectedCallVariant::CreateFile),
-            ExpectedCall::new(&path, ExpectedCallVariant::DeleteFile),
-        ]);
+    impl FsEntry for EntryMock {
+        fn path(&self) -> PathBuf {
+            match self {
+                EntryMock::File(FileMock { path, .. }) => path.clone(),
+                EntryMock::Dir { path } => path.clone(),
+            }
+        }
 
-        fs_mock.create_file(&path).unwrap();
-        fs_mock.assert_calls();
+        fn is_directory(&self) -> Result<bool> {
+            Ok(matches!(self, EntryMock::Dir { .. }))
+        }
     }
 }