Skip to main content

vct_core/cache/
mod.rs

1//! Library-facing LRU cache of parsed session analyses.
2//!
3//! The global singleton is retained for source compatibility with library
4//! callers. CLI summary scans use the compact process-local cache instead.
5
6mod file_cache;
7
8pub use file_cache::{CacheStats, FileParseCache};
9
10use std::sync::LazyLock;
11
12/// Global singleton retained for library callers.
13pub static GLOBAL_FILE_CACHE: LazyLock<FileParseCache> = LazyLock::new(FileParseCache::new);
14
15/// Returns a reference to the global file parse cache.
16pub fn global_cache() -> &'static FileParseCache {
17    &GLOBAL_FILE_CACHE
18}
19
20/// Clears the global cache (primarily for testing).
21pub fn clear_global_cache() {
22    GLOBAL_FILE_CACHE.clear();
23}
24
25#[cfg(test)]
26mod tests {
27    use super::*;
28    use serial_test::serial;
29    use std::fs::File;
30    use std::io::Write;
31    use tempfile::tempdir;
32
33    #[test]
34    #[serial(global_cache)]
35    fn test_global_cache_exists() {
36        // Test that global cache can be accessed
37        let cache = global_cache();
38        let _stats = cache.stats();
39
40        // Stats should be accessible (entry_count is usize, always >= 0)
41    }
42
43    #[test]
44    #[serial(global_cache)]
45    fn test_global_cache_singleton() {
46        // Test that global_cache returns the same instance
47        let cache1 = global_cache();
48        let cache2 = global_cache();
49
50        // Should be the same instance (same memory address)
51        assert!(std::ptr::eq(cache1, cache2));
52    }
53
54    #[test]
55    #[serial(global_cache)]
56    fn test_global_cache_clear() {
57        // Test clearing global cache
58        let cache = global_cache();
59
60        // Add some entries to cache (if possible)
61        let dir = tempdir().unwrap();
62        let file_path = dir.path().join("test.jsonl");
63        let mut file = File::create(&file_path).unwrap();
64        writeln!(
65            file,
66            r#"{{"timestamp":"2026-07-12T00:00:00Z","type":"session_meta","payload":{{"type":"session_meta","id":"clear"}}}}"#
67        )
68        .unwrap();
69        drop(file);
70
71        // Try to cache it
72        let _ = cache.get_or_parse(&file_path);
73
74        // Clear cache
75        clear_global_cache();
76
77        // Cache should be cleared
78        let stats_after = cache.stats();
79        assert_eq!(stats_after.entry_count, 0);
80    }
81
82    #[test]
83    #[serial(global_cache)]
84    fn test_global_cache_persistence_across_calls() {
85        // Test that cache persists across function calls
86        let cache = global_cache();
87        clear_global_cache();
88
89        let dir = tempdir().unwrap();
90        let file_path = dir.path().join("test.jsonl");
91        let mut file = File::create(&file_path).unwrap();
92        writeln!(
93            file,
94            r#"{{"timestamp":"2026-07-12T00:00:00Z","type":"session_meta","payload":{{"type":"session_meta","id":"persistent"}}}}"#
95        )
96        .unwrap();
97        drop(file);
98
99        // First access
100        cache.get_or_parse(&file_path).unwrap();
101        let stats1 = cache.stats();
102        let count1 = stats1.entry_count;
103
104        // Second access (should use cache)
105        cache.get_or_parse(&file_path).unwrap();
106        let stats2 = cache.stats();
107
108        // Entry count should be the same (used cached value)
109        assert_eq!(stats2.entry_count, count1);
110    }
111
112    #[test]
113    #[serial(global_cache)]
114    fn test_global_cache_stats() {
115        // Test that cache stats are accessible
116        clear_global_cache();
117        let cache = global_cache();
118
119        let stats = cache.stats();
120
121        // Should have valid stats (entry_count is 0 after clear)
122        assert_eq!(stats.entry_count, 0);
123    }
124
125    #[test]
126    #[serial(global_cache)]
127    fn test_clear_global_cache_multiple_times() {
128        // Test that clearing cache multiple times works
129        clear_global_cache();
130        clear_global_cache();
131        clear_global_cache();
132
133        let cache = global_cache();
134        let stats = cache.stats();
135
136        assert_eq!(stats.entry_count, 0);
137    }
138
139    #[test]
140    #[serial(global_cache)]
141    fn test_global_cache_thread_safety() {
142        // Test that global cache can be accessed from multiple threads
143        use std::thread;
144
145        clear_global_cache();
146
147        let handles: Vec<_> = (0..5)
148            .map(|_| {
149                thread::spawn(|| {
150                    let cache = global_cache();
151                    let _ = cache.stats();
152                })
153            })
154            .collect();
155
156        for handle in handles {
157            handle.join().unwrap();
158        }
159
160        // Should still be accessible
161        let cache = global_cache();
162        let _ = cache.stats();
163    }
164
165    #[test]
166    #[serial(global_cache)]
167    fn test_global_cache_with_operations() {
168        // Test global cache with actual operations
169        clear_global_cache();
170        let cache = global_cache();
171
172        let dir = tempdir().unwrap();
173
174        // Create test files
175        let file1 = dir.path().join("test1.jsonl");
176        let mut f = File::create(&file1).unwrap();
177        writeln!(
178            f,
179            r#"{{"timestamp":"2026-07-12T00:00:00Z","type":"session_meta","payload":{{"type":"session_meta","id":"one"}}}}"#
180        )
181        .unwrap();
182        drop(f);
183
184        let file2 = dir.path().join("test2.jsonl");
185        let mut f = File::create(&file2).unwrap();
186        writeln!(
187            f,
188            r#"{{"timestamp":"2026-07-12T00:00:00Z","type":"session_meta","payload":{{"type":"session_meta","id":"two"}}}}"#
189        )
190        .unwrap();
191        drop(f);
192
193        // Parse files
194        cache.get_or_parse(&file1).unwrap();
195        cache.get_or_parse(&file2).unwrap();
196
197        let stats = cache.stats();
198        assert!(stats.entry_count >= 2);
199    }
200}