/ src / path_cache.rs
path_cache.rs
  1  use dashmap::DashMap;
  2  use parking_lot::RwLock;
  3  use std::collections::HashMap;
  4  use std::path::{Path, PathBuf};
  5  use std::sync::Arc;
  6  
  7  #[derive(Clone)]
  8  pub struct PathCache {
  9      resolved: Arc<DashMap<PathBuf, PathBuf>>,
 10  
 11      fallback: Arc<RwLock<HashMap<PathBuf, PathBuf>>>,
 12  
 13      stats: Arc<RwLock<CacheStats>>,
 14  }
 15  
 16  #[derive(Debug, Default, Clone, PartialEq, Eq)]
 17  pub struct CacheStats {
 18      hits: usize,
 19      misses: usize,
 20      errors: usize,
 21  }
 22  
 23  impl PathCache {
 24      pub fn new() -> Self {
 25          Self {
 26              resolved: Arc::new(DashMap::new()),
 27              fallback: Arc::new(RwLock::new(HashMap::new())),
 28              stats: Arc::new(RwLock::new(CacheStats::default())),
 29          }
 30      }
 31  
 32      pub fn canonicalize(&self, path: &Path) -> PathBuf {
 33          let path_buf = path.to_path_buf();
 34  
 35          if let Some(cached) = self.resolved.get(&path_buf) {
 36              self.stats.write().hits += 1;
 37              return cached.clone();
 38          }
 39  
 40          {
 41              let fallback = self.fallback.read();
 42              if let Some(cached) = fallback.get(&path_buf) {
 43                  self.stats.write().hits += 1;
 44                  return cached.clone();
 45              }
 46          }
 47  
 48          self.stats.write().misses += 1;
 49          let resolved = match path.canonicalize() {
 50              Ok(c) => {
 51                  self.resolved.insert(path_buf.clone(), c.clone());
 52                  c
 53              }
 54              Err(_) => {
 55                  self.stats.write().errors += 1;
 56  
 57                  self.fallback
 58                      .write()
 59                      .insert(path_buf.clone(), path_buf.clone());
 60                  path_buf
 61              }
 62          };
 63  
 64          resolved
 65      }
 66  
 67      pub fn canonicalize_many(&self, paths: &[PathBuf]) -> Vec<PathBuf> {
 68          paths.iter().map(|p| self.canonicalize(p)).collect()
 69      }
 70  
 71      pub fn invalidate(&self, path: &Path) {
 72          let path_buf = path.to_path_buf();
 73          self.resolved.remove(&path_buf);
 74          self.fallback.write().remove(&path_buf);
 75      }
 76  
 77      pub fn clear(&self) {
 78          self.resolved.clear();
 79          self.fallback.write().clear();
 80          self.stats.write().hits = 0;
 81          self.stats.write().misses = 0;
 82          self.stats.write().errors = 0;
 83      }
 84  
 85      pub fn stats(&self) -> CacheStats {
 86          let stats = self.stats.read();
 87          CacheStats {
 88              hits: stats.hits,
 89              misses: stats.misses,
 90              errors: stats.errors,
 91          }
 92      }
 93  
 94      pub fn len(&self) -> usize {
 95          self.resolved.len() + self.fallback.read().len()
 96      }
 97  
 98      pub fn is_empty(&self) -> bool {
 99          self.resolved.is_empty() && self.fallback.read().is_empty()
100      }
101  
102      pub fn hit_rate(&self) -> f64 {
103          let stats = self.stats.read();
104          let total = stats.hits + stats.misses;
105          if total == 0 {
106              return 0.0;
107          }
108          stats.hits as f64 / total as f64
109      }
110  }
111  
112  impl Default for PathCache {
113      fn default() -> Self {
114          Self::new()
115      }
116  }
117  
118  #[cfg(test)]
119  mod tests {
120      use super::*;
121  
122      #[test]
123      fn test_canonicalize_cache() {
124          let cache = PathCache::new();
125  
126          let path = PathBuf::from(".");
127          let result1 = cache.canonicalize(&path);
128          let stats = cache.stats();
129          assert_eq!(stats.misses, 1);
130          assert_eq!(stats.hits, 0);
131  
132          let result2 = cache.canonicalize(&path);
133          assert_eq!(result1, result2);
134          let stats = cache.stats();
135          assert_eq!(stats.misses, 1);
136          assert_eq!(stats.hits, 1);
137      }
138  
139      #[test]
140      fn test_hit_rate() {
141          let cache = PathCache::new();
142          let path = PathBuf::from(".");
143  
144          for _ in 0..10 {
145              cache.canonicalize(&path);
146          }
147  
148          let rate = cache.hit_rate();
149          assert!(rate >= 0.9);
150      }
151  
152      #[test]
153      fn test_invalidate() {
154          let cache = PathCache::new();
155          let path = PathBuf::from(".");
156  
157          cache.canonicalize(&path);
158          assert!(!cache.is_empty());
159  
160          cache.invalidate(&path);
161          assert!(cache.is_empty() || cache.len() == 1);
162      }
163  
164      #[test]
165      fn test_clear() {
166          let cache = PathCache::new();
167          cache.canonicalize(&PathBuf::from("."));
168          cache.canonicalize(&PathBuf::from(".."));
169  
170          assert!(!cache.is_empty());
171          cache.clear();
172          assert!(cache.is_empty());
173          assert_eq!(cache.stats().hits, 0);
174      }
175  
176      #[test]
177      fn test_nonexistent_path() {
178          let cache = PathCache::new();
179          let path = PathBuf::from("/nonexistent/path/that/does/not/exist");
180  
181          let result = cache.canonicalize(&path);
182          assert_eq!(result, path);
183  
184          let stats = cache.stats();
185          assert_eq!(stats.errors, 1);
186      }
187  }