1use super::{LspSearchResult, LspServerType};
12use anyhow::Result;
13use dashmap::DashMap;
14use serde::{Deserialize, Serialize};
15use std::path::PathBuf;
16use std::sync::Arc;
17use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
18use tokio::sync::RwLock;
19use tokio::time::interval;
20use tracing::{debug, info, warn};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum HintType {
25 Definition,
26 References,
27 TypeDefinition,
28 Implementation,
29 Declaration,
30 Symbol,
31 Hover,
32 Completion,
33}
34
35impl HintType {
36 pub fn as_str(&self) -> &'static str {
37 match self {
38 HintType::Definition => "definition",
39 HintType::References => "references",
40 HintType::TypeDefinition => "type_definition",
41 HintType::Implementation => "implementation",
42 HintType::Declaration => "declaration",
43 HintType::Symbol => "symbol",
44 HintType::Hover => "hover",
45 HintType::Completion => "completion",
46 }
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct CachedHint {
53 pub results: Vec<LspSearchResult>,
54 pub created_at: u64,
55 pub expires_at: u64,
56 pub access_count: u64,
57 pub last_accessed: u64,
58 pub cache_key: String,
59}
60
61impl CachedHint {
62 pub fn new(results: Vec<LspSearchResult>, ttl_seconds: u64, cache_key: String) -> Self {
63 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
64 Self {
65 results,
66 created_at: now,
67 expires_at: now + ttl_seconds,
68 access_count: 0,
69 last_accessed: now,
70 cache_key,
71 }
72 }
73
74 pub fn is_expired(&self) -> bool {
75 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
76 now >= self.expires_at
77 }
78
79 pub fn touch(&mut self) {
80 self.access_count += 1;
81 self.last_accessed = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
82 }
83
84 pub fn age_seconds(&self) -> u64 {
85 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
86 now.saturating_sub(self.created_at)
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct FileTracker {
93 pub path: PathBuf,
94 pub last_modified: SystemTime,
95 pub size: u64,
96}
97
98impl FileTracker {
99 pub async fn new(path: PathBuf) -> Result<Self> {
100 let metadata = tokio::fs::metadata(&path).await?;
101 Ok(Self {
102 path,
103 last_modified: metadata.modified()?,
104 size: metadata.len(),
105 })
106 }
107
108 pub async fn has_changed(&self) -> bool {
109 match tokio::fs::metadata(&self.path).await {
110 Ok(metadata) => {
111 let new_modified = metadata.modified().unwrap_or(UNIX_EPOCH);
112 let new_size = metadata.len();
113 new_modified != self.last_modified || new_size != self.size
114 }
115 Err(_) => true, }
117 }
118}
119
120#[derive(Debug, Default, Clone)]
122pub struct CacheStats {
123 pub hits: u64,
124 pub misses: u64,
125 pub evictions: u64,
126 pub invalidations: u64,
127 pub size: usize,
128 pub memory_usage_bytes: u64,
129 pub avg_lookup_time_ns: u64,
130 pub avg_storage_time_ns: u64,
131 pub hit_rate: f64,
132}
133
134impl CacheStats {
135 pub fn update_hit_rate(&mut self) {
136 let total_requests = self.hits + self.misses;
137 if total_requests > 0 {
138 self.hit_rate = self.hits as f64 / total_requests as f64;
139 }
140 }
141
142 pub fn meets_performance_targets(&self) -> bool {
144 const MAX_LOOKUP_TIME_NS: u64 = 1_000_000; const MIN_HIT_RATE: f64 = 0.7; self.avg_lookup_time_ns <= MAX_LOOKUP_TIME_NS && self.hit_rate >= MIN_HIT_RATE
148 }
149}
150
151impl CacheStats {
152 pub fn calculate_hit_rate(&self) -> f64 {
154 if self.hits + self.misses == 0 {
155 0.0
156 } else {
157 self.hits as f64 / (self.hits + self.misses) as f64
158 }
159 }
160
161 pub fn update_lookup_timing(&mut self, lookup_time_ns: u64) {
163 const ALPHA: f64 = 0.1; if self.avg_lookup_time_ns == 0 {
165 self.avg_lookup_time_ns = lookup_time_ns;
166 } else {
167 self.avg_lookup_time_ns = ((1.0 - ALPHA) * self.avg_lookup_time_ns as f64
168 + ALPHA * lookup_time_ns as f64) as u64;
169 }
170 }
171
172 pub fn update_storage_timing(&mut self, storage_time_ns: u64) {
174 const ALPHA: f64 = 0.1; if self.avg_storage_time_ns == 0 {
176 self.avg_storage_time_ns = storage_time_ns;
177 } else {
178 self.avg_storage_time_ns = ((1.0 - ALPHA) * self.avg_storage_time_ns as f64
179 + ALPHA * storage_time_ns as f64) as u64;
180 }
181 }
182}
183
184pub struct HintCache {
186 cache: Arc<DashMap<String, CachedHint>>,
188
189 file_trackers: Arc<RwLock<DashMap<PathBuf, FileTracker>>>,
191
192 max_size: usize,
194 default_ttl_seconds: u64,
195
196 stats: Arc<RwLock<CacheStats>>,
198
199 cleanup_handle: Option<tokio::task::JoinHandle<()>>,
201 invalidation_handle: Option<tokio::task::JoinHandle<()>>,
202}
203
204impl HintCache {
205 pub async fn new(ttl_hours: u64) -> Result<Self> {
207 let cache = Arc::new(DashMap::new());
208 let file_trackers = Arc::new(RwLock::new(DashMap::new()));
209 let stats = Arc::new(RwLock::new(CacheStats::default()));
210
211 let mut hint_cache = Self {
212 cache: cache.clone(),
213 file_trackers,
214 max_size: 10000, default_ttl_seconds: ttl_hours * 3600,
216 stats,
217 cleanup_handle: None,
218 invalidation_handle: None,
219 };
220
221 hint_cache.start_cleanup_task().await;
223
224 hint_cache.start_invalidation_task().await;
226
227 info!("LSP hint cache initialized with {}h TTL", ttl_hours);
228 Ok(hint_cache)
229 }
230
231 pub async fn get(&self, key: &str) -> Result<Option<Vec<LspSearchResult>>> {
233 let start_time = std::time::Instant::now();
234 let result = self.get_internal(key).await;
235 let elapsed_ns = start_time.elapsed().as_nanos() as u64;
236
237 {
239 let mut stats = self.stats.write().await;
240 stats.update_lookup_timing(elapsed_ns);
241 stats.hit_rate = stats.calculate_hit_rate();
242 }
243
244 result
245 }
246
247 async fn get_internal(&self, key: &str) -> Result<Option<Vec<LspSearchResult>>> {
249 let mut stats = self.stats.write().await;
250
251 match self.cache.get_mut(key) {
252 Some(mut entry) => {
253 if entry.is_expired() {
254 drop(entry);
256 self.cache.remove(key);
257 stats.misses += 1;
258 debug!("Cache expired for key: {}", key);
259 Ok(None)
260 } else {
261 entry.touch();
263 let results = entry.results.clone();
264 stats.hits += 1;
265 debug!("Cache hit for key: {}", key);
266 Ok(Some(results))
267 }
268 }
269 None => {
270 stats.misses += 1;
271 debug!("Cache miss for key: {}", key);
272 Ok(None)
273 }
274 }
275 }
276
277 pub async fn set(&self, key: String, results: Vec<LspSearchResult>, ttl_seconds: u64) -> Result<()> {
279 let start_time = std::time::Instant::now();
280 let result = self.set_internal(key, results, ttl_seconds).await;
281 let elapsed_ns = start_time.elapsed().as_nanos() as u64;
282
283 {
285 let mut stats = self.stats.write().await;
286 stats.update_storage_timing(elapsed_ns);
287 }
288
289 result
290 }
291
292 async fn set_internal(&self, key: String, results: Vec<LspSearchResult>, ttl_seconds: u64) -> Result<()> {
294 if self.cache.len() >= self.max_size {
296 self.evict_lru().await;
297 }
298
299 let cached_hint = CachedHint::new(results, ttl_seconds, key.clone());
300
301 self.track_files_from_results(&cached_hint.results).await;
303
304 self.cache.insert(key.clone(), cached_hint);
305
306 {
308 let mut stats = self.stats.write().await;
309 stats.size = self.cache.len();
310 stats.memory_usage_bytes = self.estimate_memory_usage();
311 }
312
313 debug!("Cached hints for key: {}", key);
314 Ok(())
315 }
316
317 pub async fn invalidate_file(&self, file_path: &PathBuf) -> Result<u64> {
319 let mut invalidated = 0;
320 let file_path_str = file_path.to_string_lossy();
321
322 self.cache.retain(|_key, hint| {
324 let should_keep = !hint.results.iter().any(|result| {
325 result.file_path.contains(&*file_path_str)
326 });
327
328 if !should_keep {
329 invalidated += 1;
330 }
331
332 should_keep
333 });
334
335 if invalidated > 0 {
337 let mut stats = self.stats.write().await;
338 stats.invalidations += invalidated;
339 stats.size = self.cache.len();
340 debug!("Invalidated {} cache entries for file: {:?}", invalidated, file_path);
341 }
342
343 Ok(invalidated)
344 }
345
346 pub async fn clear(&self) -> Result<()> {
348 let size = self.cache.len();
349 self.cache.clear();
350
351 {
352 let mut stats = self.stats.write().await;
353 stats.size = 0;
354 stats.memory_usage_bytes = 0;
355 }
356
357 info!("Cleared {} cache entries", size);
358 Ok(())
359 }
360
361 pub async fn stats(&self) -> CacheStats {
363 self.stats.read().await.clone()
364 }
365
366 async fn track_files_from_results(&self, results: &[LspSearchResult]) {
368 let trackers = self.file_trackers.read().await;
369
370 for result in results {
371 let path = PathBuf::from(&result.file_path);
372
373 if !trackers.contains_key(&path) {
374 if let Ok(tracker) = FileTracker::new(path.clone()).await {
375 drop(trackers); let mut trackers_write = self.file_trackers.write().await;
377 trackers_write.insert(path, tracker);
378 return; }
380 }
381 }
382 }
383
384 async fn evict_lru(&self) {
386 if self.cache.is_empty() {
387 return;
388 }
389
390 let mut oldest_key: Option<String> = None;
392 let mut oldest_time = u64::MAX;
393
394 for entry in self.cache.iter() {
395 if entry.value().last_accessed < oldest_time {
396 oldest_time = entry.value().last_accessed;
397 oldest_key = Some(entry.key().clone());
398 }
399 }
400
401 if let Some(key) = oldest_key {
402 self.cache.remove(&key);
403
404 let mut stats = self.stats.write().await;
405 stats.evictions += 1;
406 stats.size = self.cache.len();
407
408 debug!("Evicted LRU cache entry: {}", key);
409 }
410 }
411
412 async fn start_cleanup_task(&mut self) {
414 let cache = self.cache.clone();
415 let stats = self.stats.clone();
416
417 let handle = tokio::spawn(async move {
418 let mut interval = interval(Duration::from_secs(300)); loop {
421 interval.tick().await;
422
423 let mut expired_keys = Vec::new();
424
425 for entry in cache.iter() {
427 if entry.value().is_expired() {
428 expired_keys.push(entry.key().clone());
429 }
430 }
431
432 let mut removed_count = 0;
434 for key in expired_keys {
435 if cache.remove(&key).is_some() {
436 removed_count += 1;
437 }
438 }
439
440 if removed_count > 0 {
441 let mut stats_lock = stats.write().await;
442 stats_lock.size = cache.len();
443 debug!("Cleaned up {} expired cache entries", removed_count);
444 }
445 }
446 });
447
448 self.cleanup_handle = Some(handle);
449 }
450
451 async fn start_invalidation_task(&mut self) {
453 let cache = self.cache.clone();
454 let file_trackers = self.file_trackers.clone();
455 let stats = self.stats.clone();
456
457 let handle = tokio::spawn(async move {
458 let mut interval = interval(Duration::from_secs(60)); loop {
461 interval.tick().await;
462
463 let trackers = file_trackers.read().await;
464 let mut changed_files = Vec::new();
465
466 for tracker in trackers.iter() {
468 if tracker.value().has_changed().await {
469 changed_files.push(tracker.key().clone());
470 }
471 }
472
473 if !changed_files.is_empty() {
474 drop(trackers); let mut total_invalidated = 0;
477
478 for file_path in changed_files {
479 let file_path_str = file_path.to_string_lossy();
480
481 cache.retain(|_key, hint| {
483 let should_keep = !hint.results.iter().any(|result| {
484 result.file_path.contains(&*file_path_str)
485 });
486
487 if !should_keep {
488 total_invalidated += 1;
489 }
490
491 should_keep
492 });
493
494 if let Ok(new_tracker) = FileTracker::new(file_path.clone()).await {
496 let mut trackers_write = file_trackers.write().await;
497 trackers_write.insert(file_path, new_tracker);
498 }
499 }
500
501 if total_invalidated > 0 {
502 let mut stats_lock = stats.write().await;
503 stats_lock.invalidations += total_invalidated;
504 stats_lock.size = cache.len();
505
506 debug!("Invalidated {} cache entries due to file changes", total_invalidated);
507 }
508 }
509 }
510 });
511
512 self.invalidation_handle = Some(handle);
513 }
514
515 fn estimate_memory_usage(&self) -> u64 {
517 self.cache.len() as u64 * 1024
519 }
520
521 pub async fn shutdown(&self) -> Result<()> {
523 info!("Shutting down LSP hint cache");
524
525 if let Some(handle) = &self.cleanup_handle {
526 handle.abort();
527 }
528
529 if let Some(handle) = &self.invalidation_handle {
530 handle.abort();
531 }
532
533 self.cache.clear();
534
535 Ok(())
536 }
537}
538
539impl Drop for HintCache {
540 fn drop(&mut self) {
541 debug!("LSP hint cache dropped");
542 }
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct SymbolHint {
548 pub symbol_name: String,
549 pub symbol_kind: String,
550 pub file_path: String,
551 pub line_number: u32,
552 pub column: u32,
553 pub documentation: Option<String>,
554 pub signature: Option<String>,
555 pub confidence: f64,
556}
557
558impl SymbolHint {
559 pub fn new(
560 symbol_name: String,
561 symbol_kind: String,
562 file_path: String,
563 line_number: u32,
564 column: u32,
565 confidence: f64,
566 ) -> Self {
567 Self {
568 symbol_name,
569 symbol_kind,
570 file_path,
571 line_number,
572 column,
573 documentation: None,
574 signature: None,
575 confidence,
576 }
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use tokio::time::sleep;
584
585 #[tokio::test]
586 async fn test_cache_basic_operations() {
587 let cache = HintCache::new(1).await.unwrap(); let key = "test_key".to_string();
589
590 assert!(cache.get(&key).await.unwrap().is_none());
592
593 let results = vec![LspSearchResult {
595 file_path: "/test/file.rs".to_string(),
596 line_number: 10,
597 column: 5,
598 content: "test content".to_string(),
599 hint_type: HintType::Definition,
600 server_type: LspServerType::Rust,
601 confidence: 0.9,
602 context_lines: None,
603 }];
604
605 cache.set(key.clone(), results.clone(), 3600).await.unwrap();
606
607 let cached = cache.get(&key).await.unwrap().unwrap();
608 assert_eq!(cached.len(), 1);
609 assert_eq!(cached[0].file_path, results[0].file_path);
610
611 let stats = cache.stats().await;
613 assert_eq!(stats.hits, 1);
614 assert_eq!(stats.misses, 1);
615 assert_eq!(stats.size, 1);
616 }
617
618 #[tokio::test]
619 async fn test_cache_expiration() {
620 let cache = HintCache::new(1).await.unwrap();
621 let key = "expiry_test".to_string();
622
623 let results = vec![LspSearchResult {
624 file_path: "/test/file.rs".to_string(),
625 line_number: 10,
626 column: 5,
627 content: "test content".to_string(),
628 hint_type: HintType::Definition,
629 server_type: LspServerType::Rust,
630 confidence: 0.9,
631 context_lines: None,
632 }];
633
634 cache.set(key.clone(), results, 1).await.unwrap();
636
637 assert!(cache.get(&key).await.unwrap().is_some());
639
640 sleep(Duration::from_secs(2)).await;
642
643 assert!(cache.get(&key).await.unwrap().is_none());
645 }
646
647 #[tokio::test]
648 async fn test_lru_eviction() {
649 let mut cache = HintCache::new(1).await.unwrap();
650 cache.max_size = 2; let results = vec![LspSearchResult {
653 file_path: "/test/file.rs".to_string(),
654 line_number: 10,
655 column: 5,
656 content: "test content".to_string(),
657 hint_type: HintType::Definition,
658 server_type: LspServerType::Rust,
659 confidence: 0.9,
660 context_lines: None,
661 }];
662
663 cache.set("key1".to_string(), results.clone(), 3600).await.unwrap();
665 sleep(Duration::from_millis(100)).await; cache.set("key2".to_string(), results.clone(), 3600).await.unwrap();
667
668 sleep(Duration::from_millis(100)).await; let _val = cache.get("key1").await.unwrap();
671 assert!(_val.is_some()); sleep(Duration::from_millis(100)).await; cache.set("key3".to_string(), results, 3600).await.unwrap();
676
677 assert!(cache.get("key1").await.unwrap().is_some());
679 assert!(cache.get("key2").await.unwrap().is_none());
680 assert!(cache.get("key3").await.unwrap().is_some());
681 }
682
683 #[tokio::test]
684 async fn test_hint_type_as_str() {
685 assert_eq!(HintType::Definition.as_str(), "definition");
686 assert_eq!(HintType::References.as_str(), "references");
687 assert_eq!(HintType::TypeDefinition.as_str(), "type_definition");
688 assert_eq!(HintType::Implementation.as_str(), "implementation");
689 assert_eq!(HintType::Declaration.as_str(), "declaration");
690 assert_eq!(HintType::Symbol.as_str(), "symbol");
691 assert_eq!(HintType::Hover.as_str(), "hover");
692 assert_eq!(HintType::Completion.as_str(), "completion");
693 }
694
695 #[tokio::test]
696 async fn test_hint_type_equality() {
697 assert_eq!(HintType::Definition, HintType::Definition);
698 assert_ne!(HintType::Definition, HintType::References);
699 }
700
701 #[tokio::test]
702 async fn test_cached_hint_creation() {
703 let results = vec![LspSearchResult {
704 file_path: "/test/file.rs".to_string(),
705 line_number: 10,
706 column: 5,
707 content: "test content".to_string(),
708 hint_type: HintType::Definition,
709 server_type: LspServerType::Rust,
710 confidence: 0.9,
711 context_lines: None,
712 }];
713
714 let cache_key = "test_key".to_string();
715 let ttl_seconds = 3600;
716
717 let cached_hint = CachedHint::new(results.clone(), ttl_seconds, cache_key.clone());
718
719 assert_eq!(cached_hint.results.len(), 1);
720 assert_eq!(cached_hint.cache_key, cache_key);
721 assert_eq!(cached_hint.access_count, 0);
722 assert!(!cached_hint.is_expired());
723 assert!(cached_hint.age_seconds() < 5); }
725
726 #[tokio::test]
727 async fn test_cached_hint_expiration() {
728 let results = vec![];
729 let cache_key = "test_key".to_string();
730
731 let mut cached_hint = CachedHint::new(results, 0, cache_key);
733 cached_hint.expires_at = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() - 1;
734
735 assert!(cached_hint.is_expired());
736 }
737
738 #[tokio::test]
739 async fn test_cached_hint_touch() {
740 let results = vec![];
741 let cache_key = "test_key".to_string();
742 let mut cached_hint = CachedHint::new(results, 3600, cache_key);
743
744 let initial_access_count = cached_hint.access_count;
745 let initial_last_accessed = cached_hint.last_accessed;
746
747 sleep(Duration::from_millis(10)).await;
749 cached_hint.touch();
750
751 assert_eq!(cached_hint.access_count, initial_access_count + 1);
752 assert!(cached_hint.last_accessed >= initial_last_accessed);
753 }
754
755 #[tokio::test]
756 async fn test_cached_hint_age() {
757 let results = vec![];
758 let cache_key = "test_key".to_string();
759 let cached_hint = CachedHint::new(results, 3600, cache_key);
760
761 let age = cached_hint.age_seconds();
762 assert!(age < 5); let mut old_hint = cached_hint;
766 old_hint.created_at = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() - 100;
767
768 let old_age = old_hint.age_seconds();
769 assert!(old_age >= 90); }
771
772 #[tokio::test]
773 async fn test_cache_stats_hit_rate() {
774 let mut stats = CacheStats::default();
775
776 stats.update_hit_rate();
778 assert_eq!(stats.hit_rate, 0.0);
779
780 stats.misses = 5;
782 stats.update_hit_rate();
783 assert_eq!(stats.hit_rate, 0.0);
784
785 stats.hits = 3;
787 stats.misses = 7;
788 stats.update_hit_rate();
789 assert_eq!(stats.hit_rate, 0.3);
790
791 stats.hits = 10;
793 stats.misses = 0;
794 stats.update_hit_rate();
795 assert_eq!(stats.hit_rate, 1.0);
796 }
797
798 #[tokio::test]
799 async fn test_cache_stats_clone() {
800 let mut stats = CacheStats::default();
801 stats.hits = 5;
802 stats.misses = 3;
803 stats.size = 10;
804
805 let cloned = stats.clone();
806 assert_eq!(cloned.hits, stats.hits);
807 assert_eq!(cloned.misses, stats.misses);
808 assert_eq!(cloned.size, stats.size);
809 }
810
811 #[tokio::test]
812 async fn test_file_tracker_creation() {
813 use tempfile::NamedTempFile;
814 use tokio::io::AsyncWriteExt;
815
816 let temp_file = NamedTempFile::new().unwrap();
817 let temp_path = temp_file.path().to_path_buf();
818
819 {
821 let mut file = tokio::fs::File::create(&temp_path).await.unwrap();
822 file.write_all(b"test content").await.unwrap();
823 }
824
825 let tracker = FileTracker::new(temp_path.clone()).await.unwrap();
826
827 assert_eq!(tracker.path, temp_path);
828 assert!(tracker.size > 0);
829 assert!(!tracker.has_changed().await);
830 }
831
832 #[tokio::test]
833 async fn test_file_tracker_change_detection() {
834 use tempfile::NamedTempFile;
835 use tokio::io::AsyncWriteExt;
836
837 let temp_file = NamedTempFile::new().unwrap();
838 let temp_path = temp_file.path().to_path_buf();
839
840 {
842 let mut file = tokio::fs::File::create(&temp_path).await.unwrap();
843 file.write_all(b"initial content").await.unwrap();
844 }
845
846 let tracker = FileTracker::new(temp_path.clone()).await.unwrap();
847 assert!(!tracker.has_changed().await);
848
849 sleep(Duration::from_millis(100)).await; {
852 let mut file = tokio::fs::File::create(&temp_path).await.unwrap();
853 file.write_all(b"modified content").await.unwrap();
854 }
855
856 assert!(tracker.has_changed().await);
857 }
858
859 #[tokio::test]
860 async fn test_file_tracker_missing_file() {
861 let non_existent_path = PathBuf::from("/definitely/does/not/exist.txt");
862 let tracker = FileTracker::new(non_existent_path.clone()).await;
863
864 assert!(tracker.is_err());
866
867 use tempfile::NamedTempFile;
869 let temp_file = NamedTempFile::new().unwrap();
870 let temp_path = temp_file.path().to_path_buf();
871
872 let tracker = FileTracker::new(temp_path.clone()).await.unwrap();
873
874 std::fs::remove_file(&temp_path).unwrap();
876
877 assert!(tracker.has_changed().await);
879 }
880
881 #[tokio::test]
882 async fn test_cache_clear() {
883 let cache = HintCache::new(1).await.unwrap();
884 let results = vec![LspSearchResult {
885 file_path: "/test/file.rs".to_string(),
886 line_number: 10,
887 column: 5,
888 content: "test content".to_string(),
889 hint_type: HintType::Definition,
890 server_type: LspServerType::Rust,
891 confidence: 0.9,
892 context_lines: None,
893 }];
894
895 cache.set("key1".to_string(), results.clone(), 3600).await.unwrap();
897 cache.set("key2".to_string(), results, 3600).await.unwrap();
898
899 let stats = cache.stats().await;
900 assert_eq!(stats.size, 2);
901
902 cache.clear().await.unwrap();
904
905 let stats_after = cache.stats().await;
906 assert_eq!(stats_after.size, 0);
907 assert_eq!(stats_after.memory_usage_bytes, 0);
908
909 assert!(cache.get("key1").await.unwrap().is_none());
911 assert!(cache.get("key2").await.unwrap().is_none());
912 }
913
914 #[tokio::test]
915 async fn test_cache_invalidate_file() {
916 let cache = HintCache::new(1).await.unwrap();
917
918 let results_file1 = vec![LspSearchResult {
919 file_path: "/test/file1.rs".to_string(),
920 line_number: 10,
921 column: 5,
922 content: "test content 1".to_string(),
923 hint_type: HintType::Definition,
924 server_type: LspServerType::Rust,
925 confidence: 0.9,
926 context_lines: None,
927 }];
928
929 let results_file2 = vec![LspSearchResult {
930 file_path: "/test/file2.rs".to_string(),
931 line_number: 20,
932 column: 10,
933 content: "test content 2".to_string(),
934 hint_type: HintType::References,
935 server_type: LspServerType::Rust,
936 confidence: 0.8,
937 context_lines: None,
938 }];
939
940 cache.set("key1".to_string(), results_file1, 3600).await.unwrap();
942 cache.set("key2".to_string(), results_file2, 3600).await.unwrap();
943
944 let file1_path = PathBuf::from("/test/file1.rs");
946 let invalidated = cache.invalidate_file(&file1_path).await.unwrap();
947
948 assert_eq!(invalidated, 1);
949
950 assert!(cache.get("key1").await.unwrap().is_none());
952 assert!(cache.get("key2").await.unwrap().is_some());
953
954 let stats = cache.stats().await;
955 assert_eq!(stats.invalidations, 1);
956 assert_eq!(stats.size, 1);
957 }
958
959 #[tokio::test]
960 async fn test_cache_memory_usage_estimation() {
961 let cache = HintCache::new(1).await.unwrap();
962
963 let initial_usage = cache.estimate_memory_usage();
964 assert_eq!(initial_usage, 0);
965
966 let results = vec![LspSearchResult {
967 file_path: "/test/file.rs".to_string(),
968 line_number: 10,
969 column: 5,
970 content: "test content".to_string(),
971 hint_type: HintType::Definition,
972 server_type: LspServerType::Rust,
973 confidence: 0.9,
974 context_lines: None,
975 }];
976
977 cache.set("key1".to_string(), results, 3600).await.unwrap();
978
979 let usage_after = cache.estimate_memory_usage();
980 assert_eq!(usage_after, 1024); }
982
983 #[tokio::test]
984 async fn test_cache_concurrent_access() {
985 let cache = Arc::new(HintCache::new(1).await.unwrap());
986
987 let results = vec![LspSearchResult {
988 file_path: "/test/file.rs".to_string(),
989 line_number: 10,
990 column: 5,
991 content: "test content".to_string(),
992 hint_type: HintType::Definition,
993 server_type: LspServerType::Rust,
994 confidence: 0.9,
995 context_lines: None,
996 }];
997
998 let mut handles = vec![];
1000 for i in 0..10 {
1001 let cache = cache.clone();
1002 let results = results.clone();
1003 let handle = tokio::spawn(async move {
1004 cache.set(format!("key{}", i), results, 3600).await
1005 });
1006 handles.push(handle);
1007 }
1008
1009 for handle in handles {
1010 handle.await.unwrap().unwrap();
1011 }
1012
1013 let stats = cache.stats().await;
1014 assert_eq!(stats.size, 10);
1015
1016 let mut handles = vec![];
1018 for i in 0..10 {
1019 let cache = cache.clone();
1020 let handle = tokio::spawn(async move {
1021 cache.get(&format!("key{}", i)).await
1022 });
1023 handles.push(handle);
1024 }
1025
1026 for handle in handles {
1027 let result = handle.await.unwrap().unwrap();
1028 assert!(result.is_some());
1029 }
1030
1031 let final_stats = cache.stats().await;
1032 assert_eq!(final_stats.hits, 10);
1033 }
1034
1035 #[tokio::test]
1036 async fn test_symbol_hint_creation() {
1037 let symbol_hint = SymbolHint::new(
1038 "test_function".to_string(),
1039 "function".to_string(),
1040 "/test/file.rs".to_string(),
1041 42,
1042 10,
1043 0.95,
1044 );
1045
1046 assert_eq!(symbol_hint.symbol_name, "test_function");
1047 assert_eq!(symbol_hint.symbol_kind, "function");
1048 assert_eq!(symbol_hint.file_path, "/test/file.rs");
1049 assert_eq!(symbol_hint.line_number, 42);
1050 assert_eq!(symbol_hint.column, 10);
1051 assert_eq!(symbol_hint.confidence, 0.95);
1052 assert!(symbol_hint.documentation.is_none());
1053 assert!(symbol_hint.signature.is_none());
1054 }
1055
1056 #[tokio::test]
1057 async fn test_symbol_hint_with_optional_fields() {
1058 let mut symbol_hint = SymbolHint::new(
1059 "test_function".to_string(),
1060 "function".to_string(),
1061 "/test/file.rs".to_string(),
1062 42,
1063 10,
1064 0.95,
1065 );
1066
1067 symbol_hint.documentation = Some("Test function documentation".to_string());
1068 symbol_hint.signature = Some("fn test_function() -> bool".to_string());
1069
1070 assert!(symbol_hint.documentation.is_some());
1071 assert!(symbol_hint.signature.is_some());
1072 assert_eq!(symbol_hint.documentation.unwrap(), "Test function documentation");
1073 }
1074
1075 #[tokio::test]
1076 async fn test_cache_shutdown() {
1077 let cache = HintCache::new(1).await.unwrap();
1078
1079 let results = vec![LspSearchResult {
1081 file_path: "/test/file.rs".to_string(),
1082 line_number: 10,
1083 column: 5,
1084 content: "test content".to_string(),
1085 hint_type: HintType::Definition,
1086 server_type: LspServerType::Rust,
1087 confidence: 0.9,
1088 context_lines: None,
1089 }];
1090
1091 cache.set("key1".to_string(), results, 3600).await.unwrap();
1092
1093 let stats_before = cache.stats().await;
1094 assert_eq!(stats_before.size, 1);
1095
1096 cache.shutdown().await.unwrap();
1098
1099 assert_eq!(cache.cache.len(), 0);
1101 }
1102
1103 #[tokio::test]
1104 async fn test_cache_drop() {
1105 let cache = HintCache::new(1).await.unwrap();
1106
1107 drop(cache);
1109 }
1110
1111 #[tokio::test]
1112 async fn test_cache_evict_lru_empty_cache() {
1113 let cache = HintCache::new(1).await.unwrap();
1114
1115 cache.evict_lru().await;
1117
1118 let stats = cache.stats().await;
1119 assert_eq!(stats.size, 0);
1120 assert_eq!(stats.evictions, 0);
1121 }
1122
1123 #[tokio::test]
1124 async fn test_large_cache_performance() {
1125 let cache = HintCache::new(1).await.unwrap();
1126 let start = Instant::now();
1127
1128 let results = vec![LspSearchResult {
1129 file_path: "/test/file.rs".to_string(),
1130 line_number: 10,
1131 column: 5,
1132 content: "test content".to_string(),
1133 hint_type: HintType::Definition,
1134 server_type: LspServerType::Rust,
1135 confidence: 0.9,
1136 context_lines: None,
1137 }];
1138
1139 for i in 0..1000 {
1141 cache.set(format!("key{}", i), results.clone(), 3600).await.unwrap();
1142 }
1143
1144 let insert_time = start.elapsed();
1145 assert!(insert_time < Duration::from_secs(5)); let start = Instant::now();
1149 for i in 0..1000 {
1150 let _result = cache.get(&format!("key{}", i)).await.unwrap();
1151 }
1152
1153 let retrieval_time = start.elapsed();
1154 assert!(retrieval_time < Duration::from_secs(2)); let stats = cache.stats().await;
1157 assert_eq!(stats.hits, 1000);
1158 assert_eq!(stats.size, 1000);
1159 }
1160
1161 #[tokio::test]
1162 async fn test_cache_key_with_special_characters() {
1163 let cache = HintCache::new(1).await.unwrap();
1164
1165 let results = vec![LspSearchResult {
1166 file_path: "/test/file.rs".to_string(),
1167 line_number: 10,
1168 column: 5,
1169 content: "test content".to_string(),
1170 hint_type: HintType::Definition,
1171 server_type: LspServerType::Rust,
1172 confidence: 0.9,
1173 context_lines: None,
1174 }];
1175
1176 let special_keys = vec![
1177 "key with spaces".to_string(),
1178 "key:with:colons".to_string(),
1179 "key/with/slashes".to_string(),
1180 "key-with-dashes".to_string(),
1181 "key_with_underscores".to_string(),
1182 "key.with.dots".to_string(),
1183 "key@with@symbols".to_string(),
1184 ];
1185
1186 for key in special_keys {
1187 cache.set(key.clone(), results.clone(), 3600).await.unwrap();
1188 let retrieved = cache.get(&key).await.unwrap();
1189 assert!(retrieved.is_some());
1190 }
1191 }
1192
1193 #[tokio::test]
1194 async fn test_empty_results_caching() {
1195 let cache = HintCache::new(1).await.unwrap();
1196 let key = "empty_results".to_string();
1197
1198 let empty_results: Vec<LspSearchResult> = vec![];
1200 cache.set(key.clone(), empty_results, 3600).await.unwrap();
1201
1202 let retrieved = cache.get(&key).await.unwrap().unwrap();
1203 assert!(retrieved.is_empty());
1204
1205 let stats = cache.stats().await;
1206 assert_eq!(stats.hits, 1);
1207 assert_eq!(stats.size, 1);
1208 }
1209}