diff options
| -rw-r--r-- | cli/src/main.rs | 11 | ||||
| -rw-r--r-- | src/actions/create.rs | 116 | ||||
| -rw-r--r-- | src/actions/update.rs | 8 | ||||
| -rw-r--r-- | src/files.rs | 2 | ||||
| -rw-r--r-- | src/filesystem.rs | 53 | ||||
| -rw-r--r-- | src/history.rs | 32 |
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(()) } |
