From 7e045760b3573cc417259fb0821d123c497dce6c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 19 May 2025 10:48:39 +0200 Subject: [PATCH 1/3] feat(backup): add delta mutation methods; start union tests --- backup/src/delta.rs | 102 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/backup/src/delta.rs b/backup/src/delta.rs index 6bdff88..dde906b 100644 --- a/backup/src/delta.rs +++ b/backup/src/delta.rs @@ -1,11 +1,15 @@ -use std::{borrow::Borrow, fmt}; +use std::{ + borrow::Borrow, + fmt, + path::{Path, PathBuf}, +}; use serde::{Deserialize, Serialize}; use super::State; /// Represents the changes relative to the previous backup -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] pub struct Delta { /// What files were added/modified in each part of the tarball. pub added: State, @@ -19,7 +23,6 @@ pub struct Delta { impl Delta { /// Returns whether the delta is empty by checking whether both its added and removed state /// return true for their `is_empty`. - #[allow(dead_code)] pub fn is_empty(&self) -> bool { self.added.is_empty() && self.removed.is_empty() } @@ -143,6 +146,58 @@ impl Delta { contributions } + + /// Append the given files to the directory's list of added files + pub fn append_added(&mut self, dir: impl Into, files: I) + where + I: IntoIterator, + I::Item: Into, + { + let dir: PathBuf = dir.into(); + let files = files.into_iter().map(Into::into); + + if let Some(dir_files) = self.added.get_mut(&dir) { + dir_files.extend(files); + } else { + self.added.insert(dir, files.collect()); + } + } + + /// Wrapper around the `append_added` method for a builder-style construction of delta's + pub fn with_added(mut self, dir: impl Into, files: I) -> Self + where + I: IntoIterator, + I::Item: Into, + { + self.append_added(dir, files); + self + } + + /// Append the given files to the directory's list of removed files + pub fn append_removed(&mut self, dir: impl Into, files: I) + where + I: IntoIterator, + I::Item: Into, + { + let dir: PathBuf = dir.into(); + let files = files.into_iter().map(Into::into); + + if let Some(dir_files) = self.removed.get_mut(&dir) { + dir_files.extend(files); + } else { + self.removed.insert(dir, files.collect()); + } + } + + /// Wrapper around the `append_removed` method for a builder-style construction of delta's + pub fn with_removed(mut self, dir: impl Into, files: I) -> Self + where + I: IntoIterator, + I::Item: Into, + { + self.append_removed(dir, files); + self + } } impl fmt::Display for Delta { @@ -153,3 +208,44 @@ impl fmt::Display for Delta { write!(f, "+{}-{}", added_count, removed_count) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_union_disjunct_dirs() { + let a = Delta::default() + .with_added("dir_added_1", ["file1", "file2"]) + .with_removed("dir_removed_1", ["file1", "file2"]); + let b = Delta::default() + .with_added("dir_added_3", ["file1", "file2"]) + .with_removed("dir_removed_3", ["file1", "file2"]); + + let expected = Delta::default() + .with_added("dir_added_1", ["file1", "file2"]) + .with_added("dir_added_3", ["file1", "file2"]) + .with_removed("dir_removed_1", ["file1", "file2"]) + .with_removed("dir_removed_3", ["file1", "file2"]); + + assert_eq!(expected, a.union(&b)); + assert_eq!(expected, b.union(&a)); + } + + #[test] + fn test_union_disjunct_files() { + let a = Delta::default() + .with_added("dir_added_1", ["file1", "file2"]) + .with_removed("dir_removed_1", ["file1", "file2"]); + let b = Delta::default() + .with_added("dir_added_1", ["file3", "file4"]) + .with_removed("dir_removed_1", ["file3", "file4"]); + + let expected = Delta::default() + .with_added("dir_added_1", ["file1", "file2", "file3", "file4"]) + .with_removed("dir_removed_1", ["file1", "file2", "file3", "file4"]); + + assert_eq!(expected, a.union(&b)); + assert_eq!(expected, b.union(&a)); + } +} From a9fa0e15904e0d859dc3fa8e6a791f2612585d4b Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 19 May 2025 11:45:49 +0200 Subject: [PATCH 2/3] feat(backup): implement mutation methods and specialized PartialEq for State --- backup/src/delta.rs | 24 ++------------ backup/src/state.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/backup/src/delta.rs b/backup/src/delta.rs index dde906b..85b36a0 100644 --- a/backup/src/delta.rs +++ b/backup/src/delta.rs @@ -1,8 +1,4 @@ -use std::{ - borrow::Borrow, - fmt, - path::{Path, PathBuf}, -}; +use std::{borrow::Borrow, fmt, path::PathBuf}; use serde::{Deserialize, Serialize}; @@ -153,14 +149,7 @@ impl Delta { I: IntoIterator, I::Item: Into, { - let dir: PathBuf = dir.into(); - let files = files.into_iter().map(Into::into); - - if let Some(dir_files) = self.added.get_mut(&dir) { - dir_files.extend(files); - } else { - self.added.insert(dir, files.collect()); - } + self.added.append_dir(dir, files); } /// Wrapper around the `append_added` method for a builder-style construction of delta's @@ -179,14 +168,7 @@ impl Delta { I: IntoIterator, I::Item: Into, { - let dir: PathBuf = dir.into(); - let files = files.into_iter().map(Into::into); - - if let Some(dir_files) = self.removed.get_mut(&dir) { - dir_files.extend(files); - } else { - self.removed.insert(dir, files.collect()); - } + self.removed.append_dir(dir, files); } /// Wrapper around the `append_removed` method for a builder-style construction of delta's diff --git a/backup/src/state.rs b/backup/src/state.rs index c0f66b9..b94370f 100644 --- a/backup/src/state.rs +++ b/backup/src/state.rs @@ -11,7 +11,7 @@ use crate::Delta; /// Struct that represents a current state for a backup. This struct acts as a smart pointer around /// a HashMap. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct State(HashMap>); impl State { @@ -49,8 +49,52 @@ impl State { pub fn is_empty(&self) -> bool { self.0.values().all(|s| s.is_empty()) } + + pub fn append_dir(&mut self, dir: impl Into, files: I) + where + I: IntoIterator, + I::Item: Into, + { + let dir = dir.into(); + let files = files.into_iter().map(Into::into); + + if let Some(dir_files) = self.0.get_mut(&dir) { + dir_files.extend(files); + } else { + self.0.insert(dir, files.collect()); + } + } + + pub fn with_dir(mut self, dir: impl Into, files: I) -> Self + where + I: IntoIterator, + I::Item: Into, + { + self.append_dir(dir, files); + self + } } +impl PartialEq for State { + fn eq(&self, other: &Self) -> bool { + let self_non_empty = self.0.values().filter(|files| !files.is_empty()).count(); + let other_non_empty = other.0.values().filter(|files| !files.is_empty()).count(); + + if self_non_empty != other_non_empty { + return false; + } + + // If both states have the same number of non-empty directories, then comparing each + // directory of one with the other will only be true if their list of non-empty directories + // is identical. + self.0 + .iter() + .all(|(dir, files)| files.is_empty() || other.0.get(dir).map_or(false, |v| v == files)) + } +} + +impl Eq for State {} + impl From for State where T: IntoIterator, @@ -86,3 +130,33 @@ impl DerefMut for State { &mut self.0 } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_eq() { + let a = State::default().with_dir("dir1", ["file1", "file2"]); + let b = State::default().with_dir("dir1", ["file1", "file2"]); + + assert_eq!(a, b); + + let b = b.with_dir("dir2", ["file3"]); + + assert_ne!(a, b); + } + + #[test] + fn test_eq_empty_dirs() { + let a = State::default().with_dir("dir1", ["file1", "file2"]); + let b = State::default() + .with_dir("dir1", ["file1", "file2"]) + .with_dir("dir2", Vec::::new()); + + assert_eq!(a, b); + + let b = b.with_dir("dir2", ["file3"]); + assert_ne!(a, b); + } +} From ed13fb7511842f5099bfb163b878beb09aa8bccc Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 19 May 2025 11:46:19 +0200 Subject: [PATCH 3/3] test(backup): add delta union test --- backup/src/delta.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backup/src/delta.rs b/backup/src/delta.rs index 85b36a0..cc831ac 100644 --- a/backup/src/delta.rs +++ b/backup/src/delta.rs @@ -230,4 +230,16 @@ mod tests { assert_eq!(expected, a.union(&b)); assert_eq!(expected, b.union(&a)); } + + #[test] + fn test_union_full_revert() { + let a = Delta::default().with_added("dir_1", ["file1", "file2"]); + let b = Delta::default().with_removed("dir_1", ["file1", "file2"]); + + let expected = Delta::default().with_removed("dir_1", ["file1", "file2"]); + assert_eq!(expected, a.union(&b)); + + let expected = Delta::default().with_added("dir_1", ["file1", "file2"]); + assert_eq!(expected, b.union(&a)); + } }