clnrm_core/cache/
memory_cache.rs1use super::cache_trait::{Cache, CacheStats};
7use super::hash;
8use crate::error::{CleanroomError, Result};
9use chrono::Utc;
10use std::collections::HashMap;
11use std::path::Path;
12use std::sync::{Arc, Mutex};
13use tracing::debug;
14
15#[derive(Debug, Clone)]
41pub struct MemoryCache {
42 hashes: Arc<Mutex<HashMap<String, String>>>,
44}
45
46impl MemoryCache {
47 pub fn new() -> Self {
49 Self {
50 hashes: Arc::new(Mutex::new(HashMap::new())),
51 }
52 }
53
54 pub fn len(&self) -> usize {
56 self.hashes.lock().map(|h| h.len()).unwrap_or(0)
57 }
58
59 pub fn is_empty(&self) -> bool {
61 self.len() == 0
62 }
63}
64
65impl Cache for MemoryCache {
66 fn has_changed(&self, file_path: &Path, rendered_content: &str) -> Result<bool> {
67 let file_key = file_path
68 .to_str()
69 .ok_or_else(|| CleanroomError::validation_error("Invalid file path encoding"))?
70 .to_string();
71
72 let current_hash = hash::hash_content(rendered_content)?;
74
75 let hashes = self.hashes.lock().map_err(|e| {
77 CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
78 })?;
79
80 match hashes.get(&file_key) {
81 Some(cached_hash) if cached_hash == ¤t_hash => {
82 debug!("Memory cache hit: {} (unchanged)", file_key);
83 Ok(false)
84 }
85 Some(_) => {
86 debug!("Memory cache miss: {} (changed)", file_key);
87 Ok(true)
88 }
89 None => {
90 debug!("Memory cache miss: {} (new file)", file_key);
91 Ok(true)
92 }
93 }
94 }
95
96 fn update(&self, file_path: &Path, rendered_content: &str) -> Result<()> {
97 let file_key = file_path
98 .to_str()
99 .ok_or_else(|| CleanroomError::validation_error("Invalid file path encoding"))?
100 .to_string();
101
102 let hash = hash::hash_content(rendered_content)?;
103
104 let mut hashes = self.hashes.lock().map_err(|e| {
105 CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
106 })?;
107
108 hashes.insert(file_key.clone(), hash);
109 debug!("Memory cache updated: {}", file_key);
110
111 Ok(())
112 }
113
114 fn remove(&self, file_path: &Path) -> Result<()> {
115 let file_key = file_path
116 .to_str()
117 .ok_or_else(|| CleanroomError::validation_error("Invalid file path encoding"))?
118 .to_string();
119
120 let mut hashes = self.hashes.lock().map_err(|e| {
121 CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
122 })?;
123
124 if hashes.remove(&file_key).is_some() {
125 debug!("Removed from memory cache: {}", file_key);
126 }
127
128 Ok(())
129 }
130
131 fn save(&self) -> Result<()> {
132 Ok(())
134 }
135
136 fn stats(&self) -> Result<CacheStats> {
137 let hashes = self.hashes.lock().map_err(|e| {
138 CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
139 })?;
140
141 Ok(CacheStats {
142 total_files: hashes.len(),
143 last_updated: Utc::now(),
144 cache_path: None,
145 })
146 }
147
148 fn clear(&self) -> Result<()> {
149 let mut hashes = self.hashes.lock().map_err(|e| {
150 CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
151 })?;
152
153 hashes.clear();
154 debug!("Memory cache cleared");
155
156 Ok(())
157 }
158}
159
160impl Default for MemoryCache {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 #![allow(
169 clippy::unwrap_used,
170 clippy::expect_used,
171 clippy::indexing_slicing,
172 clippy::panic
173 )]
174
175 use super::*;
176 use std::path::PathBuf;
177
178 #[test]
179 fn test_memory_cache_implements_trait() -> Result<()> {
180 let cache: Box<dyn Cache> = Box::new(MemoryCache::new());
182 let test_path = PathBuf::from("/test/file.toml");
183 let content = "test content";
184
185 let changed = cache.has_changed(&test_path, content)?;
187 assert!(changed);
188
189 Ok(())
190 }
191
192 #[test]
193 fn test_memory_cache_has_changed_new_file() -> Result<()> {
194 let cache = MemoryCache::new();
196 let test_path = PathBuf::from("/test/file.toml");
197 let content = "test content";
198
199 let changed = cache.has_changed(&test_path, content)?;
201
202 assert!(changed, "New file should be marked as changed");
204
205 Ok(())
206 }
207
208 #[test]
209 fn test_memory_cache_update_and_check() -> Result<()> {
210 let cache = MemoryCache::new();
212 let test_path = PathBuf::from("/test/file.toml");
213 let content = "test content";
214
215 cache.update(&test_path, content)?;
217 let changed = cache.has_changed(&test_path, content)?;
218
219 assert!(!changed, "Unchanged file should not be marked as changed");
221
222 Ok(())
223 }
224
225 #[test]
226 fn test_memory_cache_detects_changes() -> Result<()> {
227 let cache = MemoryCache::new();
229 let test_path = PathBuf::from("/test/file.toml");
230 let content1 = "test content 1";
231 let content2 = "test content 2";
232
233 cache.update(&test_path, content1)?;
235 let changed = cache.has_changed(&test_path, content2)?;
236
237 assert!(changed, "Changed file should be marked as changed");
239
240 Ok(())
241 }
242
243 #[test]
244 fn test_memory_cache_thread_safety() -> Result<()> {
245 use std::thread;
246
247 let cache = MemoryCache::new();
249
250 let mut handles = vec![];
252 for i in 0..10 {
253 let cache_clone = cache.clone();
254 let handle = thread::spawn(move || {
255 let path = PathBuf::from(format!("/test/file{}.toml", i));
256 let content = format!("content {}", i);
257 cache_clone.update(&path, &content).unwrap();
258 });
259 handles.push(handle);
260 }
261
262 for handle in handles {
264 let _ = handle.join();
266 }
267
268 let stats = cache.stats()?;
270 assert_eq!(stats.total_files, 10);
271
272 Ok(())
273 }
274
275 #[test]
276 fn test_memory_cache_remove() -> Result<()> {
277 let cache = MemoryCache::new();
279 let test_path = PathBuf::from("/test/file.toml");
280 let content = "test content";
281
282 cache.update(&test_path, content)?;
284 cache.remove(&test_path)?;
285 let changed = cache.has_changed(&test_path, content)?;
286
287 assert!(changed, "Removed file should be marked as changed");
289
290 Ok(())
291 }
292
293 #[test]
294 fn test_memory_cache_clear() -> Result<()> {
295 let cache = MemoryCache::new();
297 cache.update(&PathBuf::from("/test/file1.toml"), "content1")?;
298 cache.update(&PathBuf::from("/test/file2.toml"), "content2")?;
299
300 cache.clear()?;
302 let stats = cache.stats()?;
303
304 assert_eq!(stats.total_files, 0);
306 assert!(cache.is_empty());
307
308 Ok(())
309 }
310
311 #[test]
312 fn test_memory_cache_save_noop() -> Result<()> {
313 let cache = MemoryCache::new();
315
316 cache.save()?;
318
319 Ok(())
320 }
321
322 #[test]
323 fn test_memory_cache_stats_no_path() -> Result<()> {
324 let cache = MemoryCache::new();
326 cache.update(&PathBuf::from("/test/file.toml"), "content")?;
327
328 let stats = cache.stats()?;
330
331 assert_eq!(stats.total_files, 1);
333 assert!(stats.cache_path.is_none());
334
335 Ok(())
336 }
337
338 #[test]
339 fn test_memory_cache_collaboration_workflow() -> Result<()> {
340 let cache = MemoryCache::new();
342 let file1 = PathBuf::from("/test/file1.toml");
343 let file2 = PathBuf::from("/test/file2.toml");
344 let content = "shared content";
345
346 assert!(cache.has_changed(&file1, content)?);
349 assert!(cache.has_changed(&file2, content)?);
350
351 cache.update(&file1, content)?;
352 cache.update(&file2, content)?;
353
354 assert!(!cache.has_changed(&file1, content)?);
356 assert!(!cache.has_changed(&file2, content)?);
357
358 let new_content = "new content";
360 assert!(cache.has_changed(&file1, new_content)?);
361 assert!(!cache.has_changed(&file2, content)?);
362
363 let stats = cache.stats()?;
365 assert_eq!(stats.total_files, 2);
366
367 Ok(())
368 }
369}