context_creator/core/
cache.rs

1//! File caching functionality for eliminating redundant I/O
2//!
3//! This module provides a thread-safe cache for file contents using `Arc<str>`
4//! for cheap cloning across threads.
5
6use anyhow::Result;
7use dashmap::DashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11/// Thread-safe file content cache
12pub struct FileCache {
13    cache: DashMap<PathBuf, Arc<str>>,
14}
15
16impl FileCache {
17    /// Create a new empty cache
18    pub fn new() -> Self {
19        FileCache {
20            cache: DashMap::new(),
21        }
22    }
23
24    /// Get file content from cache or load from disk
25    pub fn get_or_load(&self, path: &Path) -> Result<Arc<str>> {
26        // Canonicalize path to avoid cache misses from different representations
27        let canonical_path = path.canonicalize()?;
28
29        // Check if already cached
30        if let Some(content) = self.cache.get(&canonical_path) {
31            return Ok(content.clone());
32        }
33
34        // Load from disk
35        let content = std::fs::read_to_string(&canonical_path)?;
36        let arc_content: Arc<str> = Arc::from(content.as_str());
37
38        // Store in cache
39        self.cache.insert(canonical_path, arc_content.clone());
40
41        Ok(arc_content)
42    }
43
44    /// Get cache statistics
45    pub fn stats(&self) -> CacheStats {
46        CacheStats {
47            entries: self.cache.len(),
48        }
49    }
50}
51
52impl Default for FileCache {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58/// Cache statistics
59#[derive(Debug, Clone)]
60pub struct CacheStats {
61    pub entries: usize,
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use std::fs;
68    use tempfile::TempDir;
69
70    #[test]
71    fn test_cache_hit_returns_same_content() {
72        let temp_dir = TempDir::new().unwrap();
73        let file_path = temp_dir.path().join("test.txt");
74        let content = "Hello, cache!";
75        fs::write(&file_path, content).unwrap();
76
77        let cache = FileCache::new();
78
79        // First access - cache miss
80        let content1 = cache.get_or_load(&file_path).unwrap();
81        assert_eq!(&*content1, content);
82
83        // Second access - cache hit
84        let content2 = cache.get_or_load(&file_path).unwrap();
85        assert_eq!(&*content2, content);
86
87        // Should be the same Arc
88        assert!(Arc::ptr_eq(&content1, &content2));
89    }
90
91    #[test]
92    fn test_cache_miss_loads_from_disk() {
93        let temp_dir = TempDir::new().unwrap();
94        let file_path = temp_dir.path().join("test.txt");
95        let content = "Content from disk";
96        fs::write(&file_path, content).unwrap();
97
98        let cache = FileCache::new();
99        let loaded = cache.get_or_load(&file_path).unwrap();
100
101        assert_eq!(&*loaded, content);
102        assert_eq!(cache.stats().entries, 1);
103    }
104
105    #[test]
106    fn test_non_existent_file_returns_error() {
107        let temp_dir = TempDir::new().unwrap();
108        let file_path = temp_dir.path().join("does_not_exist.txt");
109
110        let cache = FileCache::new();
111        let result = cache.get_or_load(&file_path);
112
113        assert!(result.is_err());
114        assert_eq!(cache.stats().entries, 0);
115    }
116
117    #[test]
118    fn test_canonicalized_paths() {
119        let temp_dir = TempDir::new().unwrap();
120        let file_path = temp_dir.path().join("test.txt");
121        fs::write(&file_path, "content").unwrap();
122
123        let cache = FileCache::new();
124
125        // Access with different path representations
126        let _content1 = cache.get_or_load(&file_path).unwrap();
127        let relative_path =
128            PathBuf::from(".").join(file_path.strip_prefix("/").unwrap_or(&file_path));
129
130        // This might fail on canonicalization, which is fine
131        if let Ok(content2) = cache.get_or_load(&relative_path) {
132            // If it succeeds, should still only have one entry
133            assert_eq!(cache.stats().entries, 1);
134            assert_eq!(&*content2, "content");
135        }
136    }
137
138    #[test]
139    fn test_concurrent_access() {
140        use std::sync::Arc as StdArc;
141        use std::thread;
142
143        let temp_dir = TempDir::new().unwrap();
144        let file_path = temp_dir.path().join("concurrent.txt");
145        fs::write(&file_path, "concurrent content").unwrap();
146
147        let cache = StdArc::new(FileCache::new());
148        let mut handles = vec![];
149
150        // Spawn multiple threads accessing the same file
151        for _ in 0..10 {
152            let cache_clone = cache.clone();
153            let path_clone = file_path.clone();
154
155            let handle = thread::spawn(move || {
156                let content = cache_clone.get_or_load(&path_clone).unwrap();
157                assert_eq!(&*content, "concurrent content");
158            });
159
160            handles.push(handle);
161        }
162
163        // Wait for all threads
164        for handle in handles {
165            handle.join().unwrap();
166        }
167
168        // Should only have one cache entry
169        assert_eq!(cache.stats().entries, 1);
170    }
171}