about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--cli/src/main.rs11
-rw-r--r--src/actions/create.rs116
-rw-r--r--src/actions/update.rs8
-rw-r--r--src/files.rs2
-rw-r--r--src/filesystem.rs53
-rw-r--r--src/history.rs32
6 files changed, 196 insertions, 26 deletions
diff --git a/cli/src/main.rs b/cli/src/main.rs
index d140d97..6504366 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -1,4 +1,4 @@
-use std::env;
+use std::{env, time::SystemTime};
 
 use ka::{
     actions::{create, shift, update, ActionOptions},
@@ -14,12 +14,17 @@ fn main() {
 
     let filesystem = FsImpl {};
 
+    let timestamp = SystemTime::now()
+        .duration_since(SystemTime::UNIX_EPOCH)
+        .expect("Could not get current system time.")
+        .as_secs();
+
     match command {
         "create" => {
-            create(options, &filesystem).expect("Failed executing Create action.");
+            create(options, &filesystem, timestamp).expect("Failed executing Create action.");
         }
         "update" => {
-            update(options, &filesystem).expect("Failed executing Update action.");
+            update(options, &filesystem, timestamp).expect("Failed executing Update action.");
         }
         "shift" => {
             let new_cursor: usize = args[2].as_str().parse().expect("Invalid cursor.");
diff --git a/src/actions/create.rs b/src/actions/create.rs
index 56d946b..184e249 100644
--- a/src/actions/create.rs
+++ b/src/actions/create.rs
@@ -3,10 +3,10 @@ use anyhow::Result;
 
 use super::ActionOptions;
 
-pub fn create(command_options: ActionOptions, fs: &impl Fs) -> Result<()> {
+pub fn create(command_options: ActionOptions, fs: &impl Fs, timestamp: u64) -> Result<()> {
     let locations = Locations::from(&command_options);
 
-    if locations.ka_path.exists() {
+    if fs.path_exists(&locations.ka_path) {
         fs.delete_directory(&locations.ka_path)?;
     }
 
@@ -17,7 +17,117 @@ pub fn create(command_options: ActionOptions, fs: &impl Fs) -> Result<()> {
     let empty_history = RepositoryHistory::default();
     empty_history.write_to_file(fs, &mut index_file)?;
 
-    update(command_options, fs)?;
+    update(command_options, fs, timestamp)?;
 
     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/actions/update.rs b/src/actions/update.rs
index 421d5b0..a4a64b3 100644
--- a/src/actions/update.rs
+++ b/src/actions/update.rs
@@ -1,5 +1,3 @@
-use std::time::SystemTime;
-
 use anyhow::{Context, Result};
 
 use crate::{
@@ -11,11 +9,7 @@ use crate::{
 
 use super::ActionOptions;
 
-pub fn update(command_options: ActionOptions, fs: &impl Fs) -> Result<()> {
-    let timestamp = SystemTime::now()
-        .duration_since(SystemTime::UNIX_EPOCH)?
-        .as_secs();
-
+pub fn update(command_options: ActionOptions, fs: &impl Fs, timestamp: u64) -> Result<()> {
     let locations = Locations::from(&command_options);
 
     let repository_index_path = locations.get_repository_index_path();
diff --git a/src/files.rs b/src/files.rs
index 0feee7f..29f5048 100644
--- a/src/files.rs
+++ b/src/files.rs
@@ -23,7 +23,7 @@ impl Locations {
             .read_directory(&self.repository_path)
             .context("Failed reading working file entries.")?
             .into_iter()
-            .filter(|e| e.path() == self.ka_path)
+            .filter(|e| e.path() != self.ka_path)
             .collect();
         let history_entries = fs
             .read_directory(&self.ka_files_path)
diff --git a/src/filesystem.rs b/src/filesystem.rs
index 91f7d20..f1f3af8 100644
--- a/src/filesystem.rs
+++ b/src/filesystem.rs
@@ -129,6 +129,12 @@ pub mod mock {
 
     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>>,
     }
@@ -158,10 +164,26 @@ pub mod mock {
         pub fn assert_calls(self) {
             let state = self.state.lock().expect("File system lock poisoned.");
 
-            let calls = state.received_calls.iter().zip(state.expected_calls.iter());
+            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()
+                    )
+                });
 
-            for (received, expected) in calls {
-                expected.assert_received(received)
+                expected_call.assert_received(received_call);
             }
         }
 
@@ -326,6 +348,15 @@ pub mod mock {
         is_directory: bool,
     }
 
+    impl EntryMock {
+        pub fn new(path: &Path, is_directory: bool) -> Self {
+            EntryMock {
+                path: path.to_path_buf(),
+                is_directory,
+            }
+        }
+    }
+
     impl FsEntry for EntryMock {
         fn path(&self) -> PathBuf {
             self.path.clone()
@@ -498,4 +529,20 @@ mod tests {
         fs_mock.delete_file(&path).unwrap();
         fs_mock.assert_calls();
     }
+
+    #[test]
+    #[should_panic]
+    fn mock_unequal_calls() {
+        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::DeleteFile),
+        ]);
+
+        fs_mock.create_file(&path).unwrap();
+        fs_mock.assert_calls();
+    }
 }
diff --git a/src/history.rs b/src/history.rs
index d023330..d31ecb9 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -13,17 +13,24 @@ pub struct RepositoryHistory {
 }
 
 impl RepositoryHistory {
-    pub fn from_file<FS: Fs>(fs: &FS, file: &mut FS::File) -> Result<RepositoryHistory> {
+    pub fn encode(&self) -> Result<Vec<u8>> {
+        serde_json::to_vec(self).context("Failed encoding repository history.")
+    }
+
+    pub fn decode(buffer: &[u8]) -> Result<Self> {
+        serde_json::from_slice::<Self>(buffer).context("Failed decoding repository history.")
+    }
+
+    pub fn from_file<FS: Fs>(fs: &FS, file: &mut FS::File) -> Result<Self> {
         let buffer = fs
             .read_from_file(file)
             .context("Failed reading repository history.")?;
 
-        let repository_history = serde_json::from_slice::<RepositoryHistory>(&buffer);
-        repository_history.context("Corrupted repository history.")
+        Self::decode(&buffer)
     }
 
-    pub fn write_to_file<FS: Fs>(&self, fs: &FS, file: &mut FS::File) -> anyhow::Result<()> {
-        let encoded: Vec<u8> = serde_json::to_vec(self)?;
+    pub fn write_to_file<FS: Fs>(&self, fs: &FS, file: &mut FS::File) -> Result<()> {
+        let encoded: Vec<u8> = self.encode()?;
         fs.write_to_file(file, encoded)?;
         Ok(())
     }
@@ -58,17 +65,24 @@ pub struct FileHistory {
 }
 
 impl FileHistory {
-    pub fn from_file<FS: Fs>(fs: &FS, file: &mut FS::File) -> Result<FileHistory> {
+    pub fn encode(&self) -> Result<Vec<u8>> {
+        serde_json::to_vec(self).context("Failed encoding file history.")
+    }
+
+    pub fn decode(buffer: &[u8]) -> Result<Self> {
+        serde_json::from_slice::<Self>(buffer).context("Failed decoding file history.")
+    }
+
+    pub fn from_file<FS: Fs>(fs: &FS, file: &mut FS::File) -> Result<Self> {
         let buffer = fs
             .read_from_file(file)
             .context("Failed reading file history.")?;
 
-        let file_history = serde_json::from_slice::<FileHistory>(&buffer);
-        file_history.context("Corrupted file history.")
+        Self::decode(&buffer)
     }
 
     pub fn write_to_file<FS: Fs>(&self, fs: &FS, file: &mut FS::File) -> Result<()> {
-        let encoded: Vec<u8> = serde_json::to_vec(self)?;
+        let encoded: Vec<u8> = self.encode()?;
         fs.write_to_file(file, encoded)?;
         Ok(())
     }