aboutsummaryrefslogtreecommitdiff
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(())
}