context_creator/core/
cache.rs1use anyhow::Result;
7use dashmap::DashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11pub struct FileCache {
13 cache: DashMap<PathBuf, Arc<str>>,
14}
15
16impl FileCache {
17 pub fn new() -> Self {
19 FileCache {
20 cache: DashMap::new(),
21 }
22 }
23
24 pub fn get_or_load(&self, path: &Path) -> Result<Arc<str>> {
26 let canonical_path = path.canonicalize()?;
28
29 if let Some(content) = self.cache.get(&canonical_path) {
31 return Ok(content.clone());
32 }
33
34 let content = std::fs::read_to_string(&canonical_path)?;
36 let arc_content: Arc<str> = Arc::from(content.as_str());
37
38 self.cache.insert(canonical_path, arc_content.clone());
40
41 Ok(arc_content)
42 }
43
44 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#[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 let content1 = cache.get_or_load(&file_path).unwrap();
81 assert_eq!(&*content1, content);
82
83 let content2 = cache.get_or_load(&file_path).unwrap();
85 assert_eq!(&*content2, content);
86
87 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 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 if let Ok(content2) = cache.get_or_load(&relative_path) {
132 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 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 for handle in handles {
165 handle.join().unwrap();
166 }
167
168 assert_eq!(cache.stats().entries, 1);
170 }
171}