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::encode_to_vec(index, bincode::config::standard()).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::decode_from_slice(data, bincode::config::standard())
389            .map(|(result, _)| result)
390            .map_err(|e| {
391                BinaryExportError::SerializationError(format!("Failed to deserialize index: {e}"))
392            })
393    }
394
395    /// Compress data using a simple compression algorithm
396    fn compress_data(&self, data: &[u8]) -> Result<Vec<u8>, BinaryExportError> {
397        // Simple compression using flate2 (gzip)
398        use std::io::Write;
399        let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
400        encoder.write_all(data)?;
401        encoder
402            .finish()
403            .map_err(|e| BinaryExportError::CompressionError(format!("Compression failed: {e}")))
404    }
405
406    /// Decompress data
407    fn decompress_data(&self, data: &[u8]) -> Result<Vec<u8>, BinaryExportError> {
408        use std::io::Read;
409        let mut decoder = flate2::read::GzDecoder::new(data);
410        let mut decompressed = Vec::new();
411        decoder.read_to_end(&mut decompressed)?;
412        Ok(decompressed)
413    }
414
415    /// Remove a cache entry and its associated file
416    fn remove_cache_entry(&mut self, cache_key: &str) -> Result<(), BinaryExportError> {
417        if let Some(entry) = self.entries.remove(cache_key) {
418            if entry.cache_file_path.exists() {
419                fs::remove_file(&entry.cache_file_path)?;
420            }
421        }
422        Ok(())
423    }
424
425    /// Enforce cache size and age limits using LRU eviction
426    fn enforce_cache_limits(&mut self) -> Result<(), BinaryExportError> {
427        let now = SystemTime::now()
428            .duration_since(UNIX_EPOCH)
429            .unwrap_or(Duration::ZERO)
430            .as_secs();
431
432        // Remove expired entries
433        let expired_keys: Vec<String> = self
434            .entries
435            .iter()
436            .filter(|(_, entry)| (now - entry.cached_at) > self.config.max_age_seconds)
437            .map(|(key, _)| key.clone())
438            .collect();
439
440        for key in expired_keys {
441            self.remove_cache_entry(&key)?;
442            self.stats.record_eviction();
443        }
444
445        // Enforce size limit using LRU eviction
446        while self.entries.len() > self.config.max_entries {
447            // Find the least recently used entry
448            let lru_key = self
449                .entries
450                .iter()
451                .min_by_key(|(_, entry)| entry.last_accessed)
452                .map(|(key, _)| key.clone());
453
454            if let Some(key) = lru_key {
455                self.remove_cache_entry(&key)?;
456                self.stats.record_eviction();
457            } else {
458                break;
459            }
460        }
461
462        Ok(())
463    }
464
465    /// Clean up expired cache entries
466    fn cleanup_expired_entries(&mut self) -> Result<(), BinaryExportError> {
467        let now = SystemTime::now()
468            .duration_since(UNIX_EPOCH)
469            .unwrap_or(Duration::ZERO)
470            .as_secs();
471
472        let expired_keys: Vec<String> = self
473            .entries
474            .iter()
475            .filter(|(_, entry)| {
476                (now - entry.cached_at) > self.config.max_age_seconds
477                    || !entry.cache_file_path.exists()
478            })
479            .map(|(key, _)| key.clone())
480            .collect();
481
482        for key in expired_keys {
483            self.remove_cache_entry(&key)?;
484        }
485
486        Ok(())
487    }
488
489    /// Load cache metadata from disk
490    fn load_metadata(&mut self) -> Result<(), BinaryExportError> {
491        if !self.metadata_file.exists() {
492            return Ok(());
493        }
494
495        let metadata_content = fs::read_to_string(&self.metadata_file)?;
496        let entries: HashMap<String, CacheEntry> = serde_json::from_str(&metadata_content)
497            .map_err(|e| {
498                BinaryExportError::SerializationError(format!(
499                    "Failed to parse cache metadata: {e}",
500                ))
501            })?;
502
503        self.entries = entries;
504        Ok(())
505    }
506
507    /// Save cache metadata to disk
508    fn save_metadata(&self) -> Result<(), BinaryExportError> {
509        let metadata_content = serde_json::to_string_pretty(&self.entries).map_err(|e| {
510            BinaryExportError::SerializationError(format!(
511                "Failed to serialize cache metadata: {e}",
512            ))
513        })?;
514
515        fs::write(&self.metadata_file, metadata_content)?;
516        Ok(())
517    }
518}
519
520impl Drop for IndexCache {
521    fn drop(&mut self) {
522        // Save metadata when cache is dropped
523        let _ = self.save_metadata();
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use crate::core::types::AllocationInfo;
531    use crate::export::binary::writer::BinaryWriter;
532    use tempfile::{NamedTempFile, TempDir};
533
534    fn create_test_allocation() -> AllocationInfo {
535        AllocationInfo {
536            ptr: 0x1000,
537            size: 1024,
538            var_name: Some("test_var".to_string()),
539            type_name: Some("i32".to_string()),
540            scope_name: None,
541            timestamp_alloc: 1234567890,
542            timestamp_dealloc: None,
543            thread_id: "main".to_string(),
544            borrow_count: 0,
545            stack_trace: None,
546            is_leaked: false,
547            lifetime_ms: None,
548            borrow_info: None,
549            clone_info: None,
550            ownership_history_available: false,
551            smart_pointer_info: None,
552            memory_layout: None,
553            generic_info: None,
554            dynamic_type_info: None,
555            runtime_state: None,
556            stack_allocation: None,
557            temporary_object: None,
558            fragmentation_analysis: None,
559            generic_instantiation: None,
560            type_relationships: None,
561            type_usage: None,
562            function_call_tracking: None,
563            lifecycle_tracking: None,
564            access_tracking: None,
565            drop_chain_analysis: None,
566        }
567    }
568
569    fn create_test_binary_file() -> NamedTempFile {
570        let temp_file = NamedTempFile::new().expect("Failed to create temp file");
571        let test_allocations = vec![create_test_allocation()];
572
573        // Write test data to binary file
574        {
575            let mut writer =
576                BinaryWriter::new(temp_file.path()).expect("Failed to create temp file");
577            writer
578                .write_header(test_allocations.len() as u32)
579                .expect("Failed to write header");
580            for alloc in &test_allocations {
581                writer
582                    .write_allocation(alloc)
583                    .expect("Failed to write allocation");
584            }
585            writer.finish().expect("Failed to finish writing");
586        }
587
588        temp_file
589    }
590
591    #[test]
592    fn test_cache_entry_creation() {
593        let entry = CacheEntry::new(
594            PathBuf::from("/test/file.bin"),
595            12345,
596            1024,
597            1000000,
598            PathBuf::from("/cache/entry.index"),
599        );
600
601        assert_eq!(entry.file_path, PathBuf::from("/test/file.bin"));
602        assert_eq!(entry.file_hash, 12345);
603        assert_eq!(entry.file_size, 1024);
604        assert_eq!(entry.file_modified, 1000000);
605        assert_eq!(entry.access_count, 0);
606    }
607
608    #[test]
609    fn test_cache_entry_access_tracking() {
610        let mut entry = CacheEntry::new(
611            PathBuf::from("/test/file.bin"),
612            12345,
613            1024,
614            1000000,
615            PathBuf::from("/cache/entry.index"),
616        );
617
618        let initial_access_time = entry.last_accessed;
619        let initial_count = entry.access_count;
620
621        // Wait a bit to ensure time difference
622        std::thread::sleep(std::time::Duration::from_millis(100));
623
624        entry.mark_accessed();
625
626        assert!(entry.last_accessed >= initial_access_time);
627        assert_eq!(entry.access_count, initial_count + 1);
628    }
629
630    #[test]
631    fn test_cache_stats() {
632        let mut stats = CacheStats::default();
633
634        assert_eq!(stats.hit_rate(), 0.0);
635
636        stats.record_miss();
637        assert_eq!(stats.hit_rate(), 0.0);
638        assert_eq!(stats.total_requests, 1);
639        assert_eq!(stats.cache_misses, 1);
640
641        stats.record_hit(100);
642        assert_eq!(stats.hit_rate(), 50.0);
643        assert_eq!(stats.total_requests, 2);
644        assert_eq!(stats.cache_hits, 1);
645        assert_eq!(stats.time_saved_ms, 100);
646
647        stats.record_eviction();
648        assert_eq!(stats.evictions, 1);
649    }
650
651    #[test]
652    fn test_index_cache_creation() {
653        let temp_dir = TempDir::new().expect("Failed to get test value");
654        let config = IndexCacheConfig {
655            cache_directory: temp_dir.path().to_path_buf(),
656            ..Default::default()
657        };
658
659        let cache = IndexCache::new(config).expect("Failed to get test value");
660        assert_eq!(cache.entries.len(), 0);
661        assert!(temp_dir.path().exists());
662    }
663
664    #[test]
665    fn test_cache_miss_and_build() {
666        let temp_dir = TempDir::new().expect("Failed to get test value");
667        let config = IndexCacheConfig {
668            cache_directory: temp_dir.path().to_path_buf(),
669            max_entries: 10,
670            ..Default::default()
671        };
672
673        let mut cache = IndexCache::new(config).expect("Failed to create cache");
674        let test_file = create_test_binary_file();
675        let builder = BinaryIndexBuilder::new();
676
677        // First access should be a cache miss
678        let index1 = cache
679            .get_or_build_index(test_file.path(), &builder)
680            .expect("Test operation failed");
681        assert_eq!(cache.get_stats().cache_misses, 1);
682        assert_eq!(cache.get_stats().cache_hits, 0);
683        assert_eq!(cache.entries.len(), 1);
684
685        // Second access should be a cache hit
686        let index2 = cache
687            .get_or_build_index(test_file.path(), &builder)
688            .expect("Test operation failed");
689        assert_eq!(cache.get_stats().cache_misses, 1);
690        assert_eq!(cache.get_stats().cache_hits, 1);
691        assert_eq!(cache.get_stats().hit_rate(), 50.0);
692
693        // Indexes should be equivalent
694        assert_eq!(index1.file_hash, index2.file_hash);
695        assert_eq!(index1.record_count(), index2.record_count());
696    }
697
698    #[test]
699    fn test_cache_invalidation_on_file_change() {
700        let temp_dir = TempDir::new().expect("Failed to get test value");
701        let config = IndexCacheConfig {
702            cache_directory: temp_dir.path().to_path_buf(),
703            ..Default::default()
704        };
705
706        let mut cache = IndexCache::new(config).expect("Failed to create cache");
707        let test_file = create_test_binary_file();
708        let builder = BinaryIndexBuilder::new();
709
710        // First access - cache miss
711        let _index1 = cache
712            .get_or_build_index(test_file.path(), &builder)
713            .expect("Test operation failed");
714        assert_eq!(cache.get_stats().cache_misses, 1);
715        assert_eq!(cache.entries.len(), 1);
716
717        // Wait a bit to ensure file modification time changes
718        std::thread::sleep(std::time::Duration::from_millis(100));
719
720        // Modify the file by creating a new valid binary file with different content
721        let test_allocations = vec![{
722            let mut alloc = create_test_allocation();
723            alloc.ptr = 0x2000; // Different pointer value
724            alloc.size = 2048; // Different size
725            alloc
726        }];
727
728        // Write new test data to binary file
729        {
730            let mut writer = BinaryWriter::new(test_file.path()).expect("Failed to create writer");
731            writer
732                .write_header(test_allocations.len() as u32)
733                .expect("Failed to write header");
734            for alloc in &test_allocations {
735                writer
736                    .write_allocation(alloc)
737                    .expect("Failed to write allocation");
738            }
739            writer.finish().expect("Failed to finish writing");
740        }
741
742        // Next access should be a cache miss due to file change
743        let result = cache.get_or_build_index(test_file.path(), &builder);
744        assert!(result.is_ok());
745
746        // The cache should have detected the file change and invalidated the entry
747        // This should result in either 2 misses (if invalidated) or still 1 miss but different content
748        assert!(cache.get_stats().cache_misses >= 1);
749    }
750
751    #[test]
752    fn test_cache_size_limit_enforcement() {
753        let temp_dir = TempDir::new().expect("Failed to get test value");
754        let config = IndexCacheConfig {
755            cache_directory: temp_dir.path().to_path_buf(),
756            max_entries: 2, // Small limit for testing
757            ..Default::default()
758        };
759
760        let mut cache = IndexCache::new(config).expect("Failed to create cache");
761        let builder = BinaryIndexBuilder::new();
762
763        // Create multiple test files
764        let file1 = create_test_binary_file();
765        let file2 = create_test_binary_file();
766        let file3 = create_test_binary_file();
767
768        // Cache first two files
769        let _index1 = cache
770            .get_or_build_index(file1.path(), &builder)
771            .expect("Failed to get or build index");
772        let _index2 = cache
773            .get_or_build_index(file2.path(), &builder)
774            .expect("Failed to get or build index");
775        assert_eq!(cache.entries.len(), 2);
776
777        // Adding third file should evict the least recently used entry
778        let _index3 = cache
779            .get_or_build_index(file3.path(), &builder)
780            .expect("Failed to get or build index");
781        assert_eq!(cache.entries.len(), 2);
782        assert!(cache.get_stats().evictions > 0);
783    }
784
785    #[test]
786    fn test_cache_clear() {
787        let temp_dir = TempDir::new().expect("Failed to get test value");
788        let config = IndexCacheConfig {
789            cache_directory: temp_dir.path().to_path_buf(),
790            ..Default::default()
791        };
792
793        let mut cache = IndexCache::new(config).expect("Failed to create cache");
794        let test_file = create_test_binary_file();
795        let builder = BinaryIndexBuilder::new();
796
797        // Add an entry to cache
798        let _index = cache
799            .get_or_build_index(test_file.path(), &builder)
800            .expect("Test operation failed");
801        assert_eq!(cache.entries.len(), 1);
802
803        // Clear cache
804        cache.clear().expect("Test operation failed");
805        assert_eq!(cache.entries.len(), 0);
806        assert_eq!(cache.get_stats().total_requests, 0);
807    }
808}