suture_core/engine/
tree.rs1use std::collections::BTreeMap;
8use suture_common::Hash;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct FileTree {
15 entries: BTreeMap<String, Hash>,
17}
18
19impl FileTree {
20 pub fn empty() -> Self {
22 Self {
23 entries: BTreeMap::new(),
24 }
25 }
26
27 pub fn from_map(entries: BTreeMap<String, Hash>) -> Self {
29 Self { entries }
30 }
31
32 pub fn get(&self, path: &str) -> Option<&Hash> {
34 self.entries.get(path)
35 }
36
37 pub fn contains(&self, path: &str) -> bool {
39 self.entries.contains_key(path)
40 }
41
42 pub fn insert(&mut self, path: String, hash: Hash) {
44 self.entries.insert(path, hash);
45 }
46
47 pub fn remove(&mut self, path: &str) -> Option<Hash> {
49 self.entries.remove(path)
50 }
51
52 pub fn rename(&mut self, old_path: &str, new_path: String) -> Option<Hash> {
54 if let Some(hash) = self.entries.remove(old_path) {
55 self.entries.insert(new_path, hash);
56 Some(hash)
57 } else {
58 None
59 }
60 }
61
62 pub fn len(&self) -> usize {
64 self.entries.len()
65 }
66
67 pub fn is_empty(&self) -> bool {
69 self.entries.is_empty()
70 }
71
72 pub fn iter(&self) -> impl Iterator<Item = (&String, &Hash)> {
74 self.entries.iter()
75 }
76
77 pub fn paths(&self) -> Vec<&String> {
79 self.entries.keys().collect()
80 }
81
82 pub fn content_hash(&self) -> Hash {
86 let mut hasher = blake3::Hasher::new();
87 for (path, hash) in &self.entries {
88 hasher.update(path.as_bytes());
89 hasher.update(b"\0");
90 hasher.update(&hash.0);
91 hasher.update(b"\0");
92 }
93 Hash::from(*hasher.finalize().as_bytes())
94 }
95}
96
97impl Default for FileTree {
98 fn default() -> Self {
99 Self::empty()
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn test_empty_tree() {
109 let tree = FileTree::empty();
110 assert!(tree.is_empty());
111 assert_eq!(tree.len(), 0);
112 }
113
114 #[test]
115 fn test_insert_and_get() {
116 let mut tree = FileTree::empty();
117 let hash = Hash::from_data(b"hello");
118 tree.insert("src/main.rs".to_string(), hash);
119 assert_eq!(tree.len(), 1);
120 assert!(tree.contains("src/main.rs"));
121 assert_eq!(tree.get("src/main.rs"), Some(&hash));
122 }
123
124 #[test]
125 fn test_remove() {
126 let mut tree = FileTree::empty();
127 tree.insert("file.txt".to_string(), Hash::from_data(b"data"));
128 assert!(tree.contains("file.txt"));
129 tree.remove("file.txt");
130 assert!(!tree.contains("file.txt"));
131 assert!(tree.is_empty());
132 }
133
134 #[test]
135 fn test_rename() {
136 let mut tree = FileTree::empty();
137 let hash = Hash::from_data(b"data");
138 tree.insert("old.txt".to_string(), hash);
139 tree.rename("old.txt", "new.txt".to_string());
140 assert!(!tree.contains("old.txt"));
141 assert!(tree.contains("new.txt"));
142 assert_eq!(tree.get("new.txt"), Some(&hash));
143 }
144
145 #[test]
146 fn test_content_hash_deterministic() {
147 let mut tree1 = FileTree::empty();
148 let mut tree2 = FileTree::empty();
149 let h1 = Hash::from_data(b"file1");
150 let h2 = Hash::from_data(b"file2");
151 tree1.insert("a.txt".to_string(), h1);
152 tree1.insert("b.txt".to_string(), h2);
153 tree2.insert("a.txt".to_string(), h1);
154 tree2.insert("b.txt".to_string(), h2);
155 assert_eq!(tree1.content_hash(), tree2.content_hash());
156 }
157
158 #[test]
159 fn test_content_hash_different() {
160 let mut tree1 = FileTree::empty();
161 let mut tree2 = FileTree::empty();
162 tree1.insert("a.txt".to_string(), Hash::from_data(b"v1"));
163 tree2.insert("a.txt".to_string(), Hash::from_data(b"v2"));
164 assert_ne!(tree1.content_hash(), tree2.content_hash());
165 }
166
167 #[test]
168 fn test_paths_sorted() {
169 let mut tree = FileTree::empty();
170 tree.insert("z.txt".to_string(), Hash::ZERO);
171 tree.insert("a.txt".to_string(), Hash::ZERO);
172 tree.insert("m.txt".to_string(), Hash::ZERO);
173 let paths = tree.paths();
174 assert_eq!(paths[0], &"a.txt".to_string());
175 assert_eq!(paths[1], &"m.txt".to_string());
176 assert_eq!(paths[2], &"z.txt".to_string());
177 }
178
179 mod proptests {
180 use super::*;
181 use proptest::prelude::*;
182 use suture_common::Hash;
183
184 fn valid_path() -> impl Strategy<Value = String> {
185 proptest::string::string_regex("[a-zA-Z0-9_/:-]{1,100}").unwrap()
186 }
187
188 fn hash_bytes_strategy() -> impl Strategy<Value = [u8; 32]> {
189 proptest::array::uniform32(proptest::num::u8::ANY)
190 }
191
192 proptest! {
193 #[test]
194 fn insert_then_contains(path in valid_path(), hash_bytes in hash_bytes_strategy()) {
195 let mut tree = FileTree::empty();
196 let hash = Hash::from(hash_bytes);
197 tree.insert(path.clone(), hash);
198 prop_assert!(tree.contains(&path));
199 prop_assert_eq!(tree.get(&path), Some(&hash));
200 }
201
202 #[test]
203 fn remove_then_not_contains(path in valid_path(), hash_bytes in hash_bytes_strategy()) {
204 let mut tree = FileTree::empty();
205 let hash = Hash::from(hash_bytes);
206 tree.insert(path.clone(), hash);
207 tree.remove(&path);
208 prop_assert!(!tree.contains(&path));
209 prop_assert_eq!(tree.get(&path), None);
210 }
211
212 #[test]
213 fn insert_remove_insert(path in valid_path(), hash_bytes in hash_bytes_strategy()) {
214 let mut tree = FileTree::empty();
215 let hash = Hash::from(hash_bytes);
216 tree.insert(path.clone(), hash);
217 tree.remove(&path);
218 prop_assert!(!tree.contains(&path));
219 tree.insert(path.clone(), hash);
220 prop_assert!(tree.contains(&path));
221 prop_assert_eq!(tree.get(&path), Some(&hash));
222 }
223
224 #[test]
225 fn rename(
226 old_path in valid_path(),
227 new_path in valid_path(),
228 hash_bytes in hash_bytes_strategy()
229 ) {
230 prop_assume!(old_path != new_path);
231 let mut tree = FileTree::empty();
232 let hash = Hash::from(hash_bytes);
233 tree.insert(old_path.clone(), hash);
234 tree.rename(&old_path, new_path.clone());
235 prop_assert!(!tree.contains(&old_path));
236 prop_assert!(tree.contains(&new_path));
237 prop_assert_eq!(tree.get(&new_path), Some(&hash));
238 }
239
240 #[test]
241 fn trees_equal_self(entries in proptest::collection::btree_map(valid_path(), hash_bytes_strategy().prop_map(Hash::from), 0..20)) {
242 let tree = FileTree::from_map(entries);
243 prop_assert_eq!(&tree, &tree);
244 }
245
246 #[test]
247 fn trees_equal_symmetry(entries in proptest::collection::btree_map(valid_path(), hash_bytes_strategy().prop_map(Hash::from), 0..20)) {
248 let tree1 = FileTree::from_map(entries.clone());
249 let tree2 = FileTree::from_map(entries);
250 prop_assert_eq!(tree1, tree2);
251 }
252 }
253 }
254}