memscope_rs/export/binary/
cache.rs

1//! Index caching system for binary file indexes
2//!
3//! This module provides persistent caching of binary file indexes to avoid
4//! rebuilding indexes for unchanged files. Uses LRU eviction and intelligent
5//! cache validation based on file hash and modification time.
6
7use crate::export::binary::error::BinaryExportError;
8use crate::export::binary::index::BinaryIndex;
9use crate::export::binary::index_builder::BinaryIndexBuilder;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::time::{Duration, SystemTime, UNIX_EPOCH};
15
16/// Cache entry metadata for tracking cache validity and usage
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct CacheEntry {
19    /// File path that this cache entry corresponds to
20    pub file_path: PathBuf,
21    /// Hash of the file content when cached
22    pub file_hash: u64,
23    /// File size when cached
24    pub file_size: u64,
25    /// File modification time when cached
26    pub file_modified: u64,
27    /// When this cache entry was created
28    pub cached_at: u64,
29    /// When this cache entry was last accessed
30    pub last_accessed: u64,
31    /// Number of times this cache entry has been accessed
32    pub access_count: u64,
33    /// Path to the cached index file
34    pub cache_file_path: PathBuf,
35}
36
37impl CacheEntry {
38    /// Create a new cache entry
39    pub fn new(
40        file_path: PathBuf,
41        file_hash: u64,
42        file_size: u64,
43        file_modified: u64,
44        cache_file_path: PathBuf,
45    ) -> Self {
46        let now = SystemTime::now()
47            .duration_since(UNIX_EPOCH)
48            .unwrap_or(Duration::ZERO)
49            .as_secs();
50
51        Self {
52            file_path,
53            file_hash,
54            file_size,
55            file_modified,
56            cached_at: now,
57            last_accessed: now,
58            access_count: 0,
59            cache_file_path,
60        }
61    }
62
63    /// Update access statistics
64    pub fn mark_accessed(&mut self) {
65        self.last_accessed = SystemTime::now()
66            .duration_since(UNIX_EPOCH)
67            .unwrap_or(Duration::ZERO)
68            .as_secs();
69        self.access_count += 1;
70    }
71
72    /// Check if this cache entry is still valid for the given file
73    pub fn is_valid_for_file<P: AsRef<Path>>(
74        &self,
75        file_path: P,
76    ) -> Result<bool, BinaryExportError> {
77        let path = file_path.as_ref();
78
79        // Check if file exists
80        if !path.exists() {
81            return Ok(false);
82        }
83
84        // Check file metadata
85        let metadata = fs::metadata(path)?;
86        let file_size = metadata.len();
87
88        // Check if file size changed
89        if file_size != self.file_size {
90            return Ok(false);
91        }
92
93        // Check modification time
94        if let Ok(modified) = metadata.modified() {
95            if let Ok(duration) = modified.duration_since(UNIX_EPOCH) {
96                let file_modified = duration.as_secs();
97                if file_modified != self.file_modified {
98                    return Ok(false);
99                }
100            }
101        }
102
103        // Check if cache file still exists
104        if !self.cache_file_path.exists() {
105            return Ok(false);
106        }
107
108        Ok(true)
109    }
110}
111
112/// Configuration for the index cache
113#[derive(Debug, Clone)]
114pub struct IndexCacheConfig {
115    /// Maximum number of cached indexes to keep
116    pub max_entries: usize,
117    /// Maximum age of cache entries in seconds
118    pub max_age_seconds: u64,
119    /// Directory to store cached indexes
120    pub cache_directory: PathBuf,
121    /// Whether to enable cache compression
122    pub enable_compression: bool,
123}
124
125impl Default for IndexCacheConfig {
126    fn default() -> Self {
127        let cache_dir = std::env::temp_dir().join("memscope_index_cache");
128        Self {
129            max_entries: 100,
130            max_age_seconds: 7 * 24 * 3600, // 7 days
131            cache_directory: cache_dir,
132            enable_compression: true,
133        }
134    }
135}
136
137/// Statistics about cache performance
138#[derive(Debug, Clone, Default)]
139pub struct CacheStats {
140    /// Total number of cache requests
141    pub total_requests: u64,
142    /// Number of cache hits
143    pub cache_hits: u64,
144    /// Number of cache misses
145    pub cache_misses: u64,
146    /// Number of cache entries evicted
147    pub evictions: u64,
148    /// Total time saved by cache hits (in milliseconds)
149    pub time_saved_ms: u64,
150}
151
152impl CacheStats {
153    /// Calculate cache hit rate as a percentage
154    pub fn hit_rate(&self) -> f64 {
155        if self.total_requests == 0 {
156            0.0
157        } else {
158            (self.cache_hits as f64 / self.total_requests as f64) * 100.0
159        }
160    }
161
162    /// Record a cache hit
163    pub fn record_hit(&mut self, time_saved_ms: u64) {
164        self.total_requests += 1;
165        self.cache_hits += 1;
166        self.time_saved_ms += time_saved_ms;
167    }
168
169    /// Record a cache miss
170    pub fn record_miss(&mut self) {
171        self.total_requests += 1;
172        self.cache_misses += 1;
173    }
174
175    /// Record a cache eviction
176    pub fn record_eviction(&mut self) {
177        self.evictions += 1;
178    }
179}
180
181/// Index cache manager with LRU eviction and intelligent validation
182pub struct IndexCache {
183    /// Cache configuration
184    config: IndexCacheConfig,
185    /// Cache entries metadata
186    entries: HashMap<String, CacheEntry>,
187    /// Cache statistics
188    stats: CacheStats,
189    /// Path to the cache metadata file
190    metadata_file: PathBuf,
191}
192
193impl IndexCache {
194    /// Create a new index cache with the given configuration
195    pub fn new(config: IndexCacheConfig) -> Result<Self, BinaryExportError> {
196        // Ensure cache directory exists
197        if !config.cache_directory.exists() {
198            fs::create_dir_all(&config.cache_directory)?;
199        }
200
201        let metadata_file = config.cache_directory.join("cache_metadata.json");
202
203        let mut cache = Self {
204            config,
205            entries: HashMap::new(),
206            stats: CacheStats::default(),
207            metadata_file,
208        };
209
210        // Load existing cache metadata
211        cache.load_metadata()?;
212
213        // Clean up expired entries
214        cache.cleanup_expired_entries()?;
215
216        Ok(cache)
217    }
218
219    /// Create a new index cache with default configuration
220    pub fn with_default_config() -> Result<Self, BinaryExportError> {
221        Self::new(IndexCacheConfig::default())
222    }
223
224    /// Get or build an index for the specified file
225    pub fn get_or_build_index<P: AsRef<Path>>(
226        &mut self,
227        file_path: P,
228        builder: &BinaryIndexBuilder,
229    ) -> Result<BinaryIndex, BinaryExportError> {
230        let path = file_path.as_ref();
231        let cache_key = self.generate_cache_key(path);
232
233        // Check if we have a valid cached entry
234        if let Some(entry) = self.entries.get(&cache_key) {
235            if entry.is_valid_for_file(path)? {
236                // Cache hit - load from cache
237                let start_time = std::time::Instant::now();
238                let index = self.load_index_from_cache(entry)?;
239                let load_time = start_time.elapsed().as_millis() as u64;
240
241                // Update access statistics
242                if let Some(entry) = self.entries.get_mut(&cache_key) {
243                    entry.mark_accessed();
244                }
245                self.stats.record_hit(load_time);
246
247                return Ok(index);
248            } else {
249                // Cache entry is invalid - remove it
250                self.remove_cache_entry(&cache_key)?;
251            }
252        }
253
254        // Cache miss - build new index
255        self.stats.record_miss();
256        let start_time = std::time::Instant::now();
257        let index = builder.build_index(path)?;
258        let build_time = start_time.elapsed().as_millis() as u64;
259
260        // Cache the newly built index
261        self.cache_index(path, &index, build_time)?;
262
263        Ok(index)
264    }
265
266    /// Get cache statistics
267    pub fn get_stats(&self) -> &CacheStats {
268        &self.stats
269    }
270
271    /// Clear all cache entries
272    pub fn clear(&mut self) -> Result<(), BinaryExportError> {
273        // Remove all cache files
274        for entry in self.entries.values() {
275            if entry.cache_file_path.exists() {
276                fs::remove_file(&entry.cache_file_path)?;
277            }
278        }
279
280        // Clear entries and reset stats
281        self.entries.clear();
282        self.stats = CacheStats::default();
283
284        // Save empty metadata
285        self.save_metadata()?;
286
287        Ok(())
288    }
289
290    /// Generate a cache key for the given file path
291    fn generate_cache_key<P: AsRef<Path>>(&self, file_path: P) -> String {
292        use std::collections::hash_map::DefaultHasher;
293        use std::hash::{Hash, Hasher};
294
295        let path = file_path.as_ref();
296        let mut hasher = DefaultHasher::new();
297        path.hash(&mut hasher);
298        format!("index_{:x}", hasher.finish())
299    }
300
301    /// Cache an index for the given file
302    fn cache_index<P: AsRef<Path>>(
303        &mut self,
304        file_path: P,
305        index: &BinaryIndex,
306        _build_time_ms: u64,
307    ) -> Result<(), BinaryExportError> {
308        let path = file_path.as_ref();
309        let cache_key = self.generate_cache_key(path);
310
311        // Get file metadata
312        let metadata = fs::metadata(path)?;
313        let file_size = metadata.len();
314        let file_modified = metadata
315            .modified()?
316            .duration_since(UNIX_EPOCH)
317            .unwrap_or(Duration::ZERO)
318            .as_secs();
319
320        // Generate cache file path
321        let cache_file_name = format!("{cache_key}.index");
322        let cache_file_path = self.config.cache_directory.join(cache_file_name);
323
324        // Serialize and save the index
325        self.save_index_to_cache(index, &cache_file_path)?;
326
327        // Create cache entry
328        let entry = CacheEntry::new(
329            path.to_path_buf(),
330            index.file_hash,
331            file_size,
332            file_modified,
333            cache_file_path,
334        );
335
336        // Add to cache
337        self.entries.insert(cache_key, entry);
338
339        // Enforce cache size limit
340        self.enforce_cache_limits()?;
341
342        // Save metadata
343        self.save_metadata()?;
344
345        Ok(())
346    }
347
348    /// Load an index from cache
349    fn load_index_from_cache(&self, entry: &CacheEntry) -> Result<BinaryIndex, BinaryExportError> {
350        let cache_data = fs::read(&entry.cache_file_path)?;
351
352        if self.config.enable_compression {
353            // Decompress if compression is enabled
354            let decompressed = self.decompress_data(&cache_data)?;
355            self.deserialize_index(&decompressed)
356        } else {
357            self.deserialize_index(&cache_data)
358        }
359    }
360
361    /// Save an index to cache
362    fn save_index_to_cache(
363        &self,
364        index: &BinaryIndex,
365        cache_file_path: &Path,
366    ) -> Result<(), BinaryExportError> {
367        let serialized = self.serialize_index(index)?;
368
369        let data_to_write = if self.config.enable_compression {
370            self.compress_data(&serialized)?
371        } else {
372            serialized
373        };
374
375        fs::write(cache_file_path, data_to_write)?;
376        Ok(())
377    }
378
379    /// Serialize an index to bytes
380    fn serialize_index(&self, index: &BinaryIndex) -> Result<Vec<u8>, BinaryExportError> {
381        bincode::serialize(index).map_err(|e| {
382            BinaryExportError::SerializationError(format!("Failed to serialize index: {e}"))
383        })
384    }
385
386    /// Deserialize an index from bytes
387    fn deserialize_index(&self, data: &[u8]) -> Result<BinaryIndex, BinaryExportError> {
388        bincode::deserialize(data).map_err(|e| {
389            BinaryExportError::SerializationError(format!("Failed to deserialize index: {e}"))
390        })
391    }
392
393    /// Compress data using a simple compression algorithm
394    fn compress_data(&self, data: &[u8]) -> Result<Vec<u8>, BinaryExportError> {
395        // Simple compression using flate2 (gzip)
396        use std::io::Write;
397        let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
398        encoder.write_all(data)?;
399        encoder
400            .finish()
401            .map_err(|e| BinaryExportError::CompressionError(format!("Compression failed: {e}")))
402    }
403
404    /// Decompress data
405    fn decompress_data(&self, data: &[u8]) -> Result<Vec<u8>, BinaryExportError> {
406        use std::io::Read;
407        let mut decoder = flate2::read::GzDecoder::new(data);
408        let mut decompressed = Vec::new();
409        decoder.read_to_end(&mut decompressed)?;
410        Ok(decompressed)
411    }
412
413    /// Remove a cache entry and its associated file
414    fn remove_cache_entry(&mut self, cache_key: &str) -> Result<(), BinaryExportError> {
415        if let Some(entry) = self.entries.remove(cache_key) {
416            if entry.cache_file_path.exists() {
417                fs::remove_file(&entry.cache_file_path)?;
418            }
419        }
420        Ok(())
421    }
422
423    /// Enforce cache size and age limits using LRU eviction
424    fn enforce_cache_limits(&mut self) -> Result<(), BinaryExportError> {
425        let now = SystemTime::now()
426            .duration_since(UNIX_EPOCH)
427            .unwrap_or(Duration::ZERO)
428            .as_secs();
429
430        // Remove expired entries
431        let expired_keys: Vec<String> = self
432            .entries
433            .iter()
434            .filter(|(_, entry)| (now - entry.cached_at) > self.config.max_age_seconds)
435            .map(|(key, _)| key.clone())
436            .collect();
437
438        for key in expired_keys {
439            self.remove_cache_entry(&key)?;
440            self.stats.record_eviction();
441        }
442
443        // Enforce size limit using LRU eviction
444        while self.entries.len() > self.config.max_entries {
445            // Find the least recently used entry
446            let lru_key = self
447                .entries
448                .iter()
449                .min_by_key(|(_, entry)| entry.last_accessed)
450                .map(|(key, _)| key.clone());
451
452            if let Some(key) = lru_key {
453                self.remove_cache_entry(&key)?;
454                self.stats.record_eviction();
455            } else {
456                break;
457            }
458        }
459
460        Ok(())
461    }
462
463    /// Clean up expired cache entries
464    fn cleanup_expired_entries(&mut self) -> Result<(), BinaryExportError> {
465        let now = SystemTime::now()
466            .duration_since(UNIX_EPOCH)
467            .unwrap_or(Duration::ZERO)
468            .as_secs();
469
470        let expired_keys: Vec<String> = self
471            .entries
472            .iter()
473            .filter(|(_, entry)| {
474                (now - entry.cached_at) > self.config.max_age_seconds
475                    || !entry.cache_file_path.exists()
476            })
477            .map(|(key, _)| key.clone())
478            .collect();
479
480        for key in expired_keys {
481            self.remove_cache_entry(&key)?;
482        }
483
484        Ok(())
485    }
486
487    /// Load cache metadata from disk
488    fn load_metadata(&mut self) -> Result<(), BinaryExportError> {
489        if !self.metadata_file.exists() {
490            return Ok(());
491        }
492
493        let metadata_content = fs::read_to_string(&self.metadata_file)?;
494        let entries: HashMap<String, CacheEntry> = serde_json::from_str(&metadata_content)
495            .map_err(|e| {
496                BinaryExportError::SerializationError(format!(
497                    "Failed to parse cache metadata: {e}",
498                ))
499            })?;
500
501        self.entries = entries;
502        Ok(())
503    }
504
505    /// Save cache metadata to disk
506    fn save_metadata(&self) -> Result<(), BinaryExportError> {
507        let metadata_content = serde_json::to_string_pretty(&self.entries).map_err(|e| {
508            BinaryExportError::SerializationError(format!(
509                "Failed to serialize cache metadata: {e}",
510            ))
511        })?;
512
513        fs::write(&self.metadata_file, metadata_content)?;
514        Ok(())
515    }
516}
517
518impl Drop for IndexCache {
519    fn drop(&mut self) {
520        // Save metadata when cache is dropped
521        let _ = self.save_metadata();
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528    use crate::core::types::AllocationInfo;
529    use crate::export::binary::writer::BinaryWriter;
530    use tempfile::{NamedTempFile, TempDir};
531
532    fn create_test_allocation() -> AllocationInfo {
533        AllocationInfo {
534            ptr: 0x1000,
535            size: 1024,
536            var_name: Some("test_var".to_string()),
537            type_name: Some("i32".to_string()),
538            scope_name: None,
539            timestamp_alloc: 1234567890,
540            timestamp_dealloc: None,
541            thread_id: "main".to_string(),
542            borrow_count: 0,
543            stack_trace: None,
544            is_leaked: false,
545            lifetime_ms: None,
546            borrow_info: None,
547            clone_info: None,
548            ownership_history_available: false,
549            smart_pointer_info: None,
550            memory_layout: None,
551            generic_info: None,
552            dynamic_type_info: None,
553            runtime_state: None,
554            stack_allocation: None,
555            temporary_object: None,
556            fragmentation_analysis: None,
557            generic_instantiation: None,
558            type_relationships: None,
559            type_usage: None,
560            function_call_tracking: None,
561            lifecycle_tracking: None,
562            access_tracking: None,
563            drop_chain_analysis: None,
564        }
565    }
566
567    fn create_test_binary_file() -> NamedTempFile {
568        let temp_file = NamedTempFile::new().expect("Failed to create temp file");
569        let test_allocations = vec![create_test_allocation()];
570
571        // Write test data to binary file
572        {
573            let mut writer =
574                BinaryWriter::new(temp_file.path()).expect("Failed to create temp file");
575            writer
576                .write_header(test_allocations.len() as u32)
577                .expect("Failed to write header");
578            for alloc in &test_allocations {
579                writer
580                    .write_allocation(alloc)
581                    .expect("Failed to write allocation");
582            }
583            writer.finish().expect("Failed to finish writing");
584        }
585
586        temp_file
587    }
588
589    #[test]
590    fn test_cache_entry_creation() {
591        let entry = CacheEntry::new(
592            PathBuf::from("/test/file.bin"),
593            12345,
594            1024,
595            1000000,
596            PathBuf::from("/cache/entry.index"),
597        );
598
599        assert_eq!(entry.file_path, PathBuf::from("/test/file.bin"));
600        assert_eq!(entry.file_hash, 12345);
601        assert_eq!(entry.file_size, 1024);
602        assert_eq!(entry.file_modified, 1000000);
603        assert_eq!(entry.access_count, 0);
604    }
605
606    #[test]
607    fn test_cache_entry_access_tracking() {
608        let mut entry = CacheEntry::new(
609            PathBuf::from("/test/file.bin"),
610            12345,
611            1024,
612            1000000,
613            PathBuf::from("/cache/entry.index"),
614        );
615
616        let initial_access_time = entry.last_accessed;
617        let initial_count = entry.access_count;
618
619        // Wait a bit to ensure time difference
620        std::thread::sleep(std::time::Duration::from_millis(100));
621
622        entry.mark_accessed();
623
624        assert!(entry.last_accessed >= initial_access_time);
625        assert_eq!(entry.access_count, initial_count + 1);
626    }
627
628    #[test]
629    fn test_cache_stats() {
630        let mut stats = CacheStats::default();
631
632        assert_eq!(stats.hit_rate(), 0.0);
633
634        stats.record_miss();
635        assert_eq!(stats.hit_rate(), 0.0);
636        assert_eq!(stats.total_requests, 1);
637        assert_eq!(stats.cache_misses, 1);
638
639        stats.record_hit(100);
640        assert_eq!(stats.hit_rate(), 50.0);
641        assert_eq!(stats.total_requests, 2);
642        assert_eq!(stats.cache_hits, 1);
643        assert_eq!(stats.time_saved_ms, 100);
644
645        stats.record_eviction();
646        assert_eq!(stats.evictions, 1);
647    }
648
649    #[test]
650    fn test_index_cache_creation() {
651        let temp_dir = TempDir::new().expect("Failed to get test value");
652        let config = IndexCacheConfig {
653            cache_directory: temp_dir.path().to_path_buf(),
654            ..Default::default()
655        };
656
657        let cache = IndexCache::new(config).expect("Failed to get test value");
658        assert_eq!(cache.entries.len(), 0);
659        assert!(temp_dir.path().exists());
660    }
661
662    #[test]
663    fn test_cache_miss_and_build() {
664        let temp_dir = TempDir::new().expect("Failed to get test value");
665        let config = IndexCacheConfig {
666            cache_directory: temp_dir.path().to_path_buf(),
667            max_entries: 10,
668            ..Default::default()
669        };
670
671        let mut cache = IndexCache::new(config).expect("Failed to create cache");
672        let test_file = create_test_binary_file();
673        let builder = BinaryIndexBuilder::new();
674
675        // First access should be a cache miss
676        let index1 = cache
677            .get_or_build_index(test_file.path(), &builder)
678            .expect("Test operation failed");
679        assert_eq!(cache.get_stats().cache_misses, 1);
680        assert_eq!(cache.get_stats().cache_hits, 0);
681        assert_eq!(cache.entries.len(), 1);
682
683        // Second access should be a cache hit
684        let index2 = cache
685            .get_or_build_index(test_file.path(), &builder)
686            .expect("Test operation failed");
687        assert_eq!(cache.get_stats().cache_misses, 1);
688        assert_eq!(cache.get_stats().cache_hits, 1);
689        assert_eq!(cache.get_stats().hit_rate(), 50.0);
690
691        // Indexes should be equivalent
692        assert_eq!(index1.file_hash, index2.file_hash);
693        assert_eq!(index1.record_count(), index2.record_count());
694    }
695
696    #[test]
697    fn test_cache_invalidation_on_file_change() {
698        let temp_dir = TempDir::new().expect("Failed to get test value");
699        let config = IndexCacheConfig {
700            cache_directory: temp_dir.path().to_path_buf(),
701            ..Default::default()
702        };
703
704        let mut cache = IndexCache::new(config).expect("Failed to create cache");
705        let test_file = create_test_binary_file();
706        let builder = BinaryIndexBuilder::new();
707
708        // First access - cache miss
709        let _index1 = cache
710            .get_or_build_index(test_file.path(), &builder)
711            .expect("Test operation failed");
712        assert_eq!(cache.get_stats().cache_misses, 1);
713        assert_eq!(cache.entries.len(), 1);
714
715        // Wait a bit to ensure file modification time changes
716        std::thread::sleep(std::time::Duration::from_millis(100));
717
718        // Modify the file by creating a new valid binary file with different content
719        let test_allocations = vec![{
720            let mut alloc = create_test_allocation();
721            alloc.ptr = 0x2000; // Different pointer value
722            alloc.size = 2048; // Different size
723            alloc
724        }];
725
726        // Write new test data to binary file
727        {
728            let mut writer = BinaryWriter::new(test_file.path()).expect("Failed to create writer");
729            writer
730                .write_header(test_allocations.len() as u32)
731                .expect("Failed to write header");
732            for alloc in &test_allocations {
733                writer
734                    .write_allocation(alloc)
735                    .expect("Failed to write allocation");
736            }
737            writer.finish().expect("Failed to finish writing");
738        }
739
740        // Next access should be a cache miss due to file change
741        let result = cache.get_or_build_index(test_file.path(), &builder);
742        assert!(result.is_ok());
743
744        // The cache should have detected the file change and invalidated the entry
745        // This should result in either 2 misses (if invalidated) or still 1 miss but different content
746        assert!(cache.get_stats().cache_misses >= 1);
747    }
748
749    #[test]
750    fn test_cache_size_limit_enforcement() {
751        let temp_dir = TempDir::new().expect("Failed to get test value");
752        let config = IndexCacheConfig {
753            cache_directory: temp_dir.path().to_path_buf(),
754            max_entries: 2, // Small limit for testing
755            ..Default::default()
756        };
757
758        let mut cache = IndexCache::new(config).expect("Failed to create cache");
759        let builder = BinaryIndexBuilder::new();
760
761        // Create multiple test files
762        let file1 = create_test_binary_file();
763        let file2 = create_test_binary_file();
764        let file3 = create_test_binary_file();
765
766        // Cache first two files
767        let _index1 = cache
768            .get_or_build_index(file1.path(), &builder)
769            .expect("Failed to get or build index");
770        let _index2 = cache
771            .get_or_build_index(file2.path(), &builder)
772            .expect("Failed to get or build index");
773        assert_eq!(cache.entries.len(), 2);
774
775        // Adding third file should evict the least recently used entry
776        let _index3 = cache
777            .get_or_build_index(file3.path(), &builder)
778            .expect("Failed to get or build index");
779        assert_eq!(cache.entries.len(), 2);
780        assert!(cache.get_stats().evictions > 0);
781    }
782
783    #[test]
784    fn test_cache_clear() {
785        let temp_dir = TempDir::new().expect("Failed to get test value");
786        let config = IndexCacheConfig {
787            cache_directory: temp_dir.path().to_path_buf(),
788            ..Default::default()
789        };
790
791        let mut cache = IndexCache::new(config).expect("Failed to create cache");
792        let test_file = create_test_binary_file();
793        let builder = BinaryIndexBuilder::new();
794
795        // Add an entry to cache
796        let _index = cache
797            .get_or_build_index(test_file.path(), &builder)
798            .expect("Test operation failed");
799        assert_eq!(cache.entries.len(), 1);
800
801        // Clear cache
802        cache.clear().expect("Test operation failed");
803        assert_eq!(cache.entries.len(), 0);
804        assert_eq!(cache.get_stats().total_requests, 0);
805    }
806}