Skip to main content

oxirs_samm/
cache.rs

1//! LRU Cache for SAMM Model Elements
2//!
3//! This module provides an efficient Least Recently Used (LRU) cache for SAMM model elements.
4//! The cache automatically evicts the least recently used items when it reaches capacity.
5//!
6//! # Use Cases
7//!
8//! - **Large model repositories**: Cache frequently accessed aspects
9//! - **Web applications**: Reduce parsing overhead for repeated requests
10//! - **Batch processing**: Reuse parsed models across multiple operations
11//! - **Memory constraints**: Limit cache size while maintaining performance
12//!
13//! # Example
14//!
15//! ```rust
16//! use oxirs_samm::cache::LruModelCache;
17//! use oxirs_samm::metamodel::Aspect;
18//! use std::sync::Arc;
19//!
20//! let mut cache = LruModelCache::new(100); // Cache up to 100 aspects
21//!
22//! let aspect = Aspect::new("urn:samm:org.example:1.0.0#MyAspect".to_string());
23//! cache.put("my-aspect".to_string(), Arc::new(aspect));
24//!
25//! // Later retrieval
26//! if let Some(cached_aspect) = cache.get("my-aspect") {
27//!     println!("Cache hit!");
28//! }
29//!
30//! // Check cache statistics
31//! println!("Cache size: {}/{}", cache.len(), cache.capacity());
32//! println!("Hit rate: {:.2}%", cache.hit_rate() * 100.0);
33//! ```
34
35use crate::metamodel::{Aspect, Characteristic, Entity, Operation, Property};
36use std::collections::HashMap;
37use std::sync::{Arc, RwLock};
38
39/// Entry in the LRU cache with access tracking
40#[derive(Clone)]
41struct CacheEntry<T> {
42    value: Arc<T>,
43    access_count: usize,
44    last_accessed: std::time::Instant,
45}
46
47/// LRU (Least Recently Used) cache for model elements
48///
49/// Thread-safe cache with automatic eviction of least recently used items.
50pub struct LruModelCache<T> {
51    capacity: usize,
52    entries: Arc<RwLock<HashMap<String, CacheEntry<T>>>>,
53    access_order: Arc<RwLock<Vec<String>>>,
54    hits: Arc<RwLock<usize>>,
55    misses: Arc<RwLock<usize>>,
56}
57
58impl<T> LruModelCache<T>
59where
60    T: Clone,
61{
62    /// Create a new LRU cache with specified capacity
63    ///
64    /// # Arguments
65    ///
66    /// * `capacity` - Maximum number of items to cache
67    ///
68    /// # Example
69    ///
70    /// ```rust
71    /// use oxirs_samm::cache::LruModelCache;
72    /// use oxirs_samm::metamodel::Aspect;
73    ///
74    /// let cache: LruModelCache<Aspect> = LruModelCache::new(50);
75    /// assert_eq!(cache.capacity(), 50);
76    /// ```
77    pub fn new(capacity: usize) -> Self {
78        Self {
79            capacity: capacity.max(1), // Minimum capacity of 1
80            entries: Arc::new(RwLock::new(HashMap::new())),
81            access_order: Arc::new(RwLock::new(Vec::new())),
82            hits: Arc::new(RwLock::new(0)),
83            misses: Arc::new(RwLock::new(0)),
84        }
85    }
86
87    /// Get an item from the cache
88    ///
89    /// Updates access statistics and LRU ordering.
90    pub fn get(&self, key: &str) -> Option<Arc<T>> {
91        let mut entries = self.entries.write().expect("lock poisoned");
92
93        if let Some(entry) = entries.get_mut(key) {
94            // Update access statistics
95            entry.access_count += 1;
96            entry.last_accessed = std::time::Instant::now();
97
98            // Update access order
99            let mut access_order = self.access_order.write().expect("lock poisoned");
100            if let Some(pos) = access_order.iter().position(|k| k == key) {
101                access_order.remove(pos);
102            }
103            access_order.push(key.to_string());
104
105            // Record hit
106            *self.hits.write().expect("lock poisoned") += 1;
107
108            Some(Arc::clone(&entry.value))
109        } else {
110            // Record miss
111            *self.misses.write().expect("lock poisoned") += 1;
112            None
113        }
114    }
115
116    /// Put an item into the cache
117    ///
118    /// If the cache is at capacity, evicts the least recently used item.
119    pub fn put(&mut self, key: String, value: Arc<T>) {
120        let mut entries = self.entries.write().expect("lock poisoned");
121        let mut access_order = self.access_order.write().expect("lock poisoned");
122
123        // Remove existing entry if present
124        if entries.contains_key(&key) {
125            if let Some(pos) = access_order.iter().position(|k| k == &key) {
126                access_order.remove(pos);
127            }
128        }
129
130        // Evict LRU entry if at capacity
131        if entries.len() >= self.capacity && !entries.contains_key(&key) {
132            if let Some(lru_key) = access_order.first() {
133                let lru_key = lru_key.clone();
134                entries.remove(&lru_key);
135                access_order.remove(0);
136            }
137        }
138
139        // Insert new entry
140        let entry = CacheEntry {
141            value,
142            access_count: 0,
143            last_accessed: std::time::Instant::now(),
144        };
145
146        entries.insert(key.clone(), entry);
147        access_order.push(key);
148    }
149
150    /// Check if the cache contains a key
151    pub fn contains(&self, key: &str) -> bool {
152        let entries = self.entries.read().expect("lock poisoned");
153        entries.contains_key(key)
154    }
155
156    /// Remove an item from the cache
157    pub fn remove(&mut self, key: &str) -> Option<Arc<T>> {
158        let mut entries = self.entries.write().expect("lock poisoned");
159        let mut access_order = self.access_order.write().expect("lock poisoned");
160
161        if let Some(pos) = access_order.iter().position(|k| k == key) {
162            access_order.remove(pos);
163        }
164
165        entries.remove(key).map(|entry| entry.value)
166    }
167
168    /// Clear all items from the cache
169    pub fn clear(&mut self) {
170        let mut entries = self.entries.write().expect("lock poisoned");
171        let mut access_order = self.access_order.write().expect("lock poisoned");
172
173        entries.clear();
174        access_order.clear();
175    }
176
177    /// Get the number of items in the cache
178    pub fn len(&self) -> usize {
179        let entries = self.entries.read().expect("lock poisoned");
180        entries.len()
181    }
182
183    /// Check if the cache is empty
184    pub fn is_empty(&self) -> bool {
185        self.len() == 0
186    }
187
188    /// Get the cache capacity
189    pub fn capacity(&self) -> usize {
190        self.capacity
191    }
192
193    /// Resize the cache capacity
194    ///
195    /// If the new capacity is smaller than current size, evicts LRU items.
196    pub fn resize(&mut self, new_capacity: usize) {
197        self.capacity = new_capacity.max(1);
198
199        let mut entries = self.entries.write().expect("lock poisoned");
200        let mut access_order = self.access_order.write().expect("lock poisoned");
201
202        // Evict items if over capacity
203        while entries.len() > self.capacity {
204            if let Some(lru_key) = access_order.first() {
205                let lru_key = lru_key.clone();
206                entries.remove(&lru_key);
207                access_order.remove(0);
208            } else {
209                break;
210            }
211        }
212    }
213
214    /// Get cache hit rate (0.0 to 1.0)
215    pub fn hit_rate(&self) -> f64 {
216        let hits = *self.hits.read().expect("lock poisoned");
217        let misses = *self.misses.read().expect("lock poisoned");
218        let total = hits + misses;
219
220        if total == 0 {
221            0.0
222        } else {
223            hits as f64 / total as f64
224        }
225    }
226
227    /// Get total number of cache hits
228    pub fn hits(&self) -> usize {
229        *self.hits.read().expect("lock poisoned")
230    }
231
232    /// Get total number of cache misses
233    pub fn misses(&self) -> usize {
234        *self.misses.read().expect("lock poisoned")
235    }
236
237    /// Reset cache statistics
238    pub fn reset_statistics(&mut self) {
239        *self.hits.write().expect("lock poisoned") = 0;
240        *self.misses.write().expect("lock poisoned") = 0;
241    }
242
243    /// Get all keys in the cache (in LRU order)
244    pub fn keys(&self) -> Vec<String> {
245        let access_order = self.access_order.read().expect("lock poisoned");
246        access_order.clone()
247    }
248
249    /// Get cache statistics
250    pub fn statistics(&self) -> CacheStatistics {
251        CacheStatistics {
252            size: self.len(),
253            capacity: self.capacity,
254            hits: self.hits(),
255            misses: self.misses(),
256            hit_rate: self.hit_rate(),
257        }
258    }
259}
260
261impl<T> Clone for LruModelCache<T> {
262    fn clone(&self) -> Self {
263        Self {
264            capacity: self.capacity,
265            entries: Arc::clone(&self.entries),
266            access_order: Arc::clone(&self.access_order),
267            hits: Arc::clone(&self.hits),
268            misses: Arc::clone(&self.misses),
269        }
270    }
271}
272
273/// Cache statistics
274#[derive(Debug, Clone)]
275pub struct CacheStatistics {
276    /// Current number of items
277    pub size: usize,
278    /// Maximum capacity
279    pub capacity: usize,
280    /// Total hits
281    pub hits: usize,
282    /// Total misses
283    pub misses: usize,
284    /// Hit rate (0.0 to 1.0)
285    pub hit_rate: f64,
286}
287
288impl CacheStatistics {
289    /// Get the fill percentage (0.0 to 100.0)
290    pub fn fill_percentage(&self) -> f64 {
291        if self.capacity == 0 {
292            0.0
293        } else {
294            (self.size as f64 / self.capacity as f64) * 100.0
295        }
296    }
297
298    /// Get total accesses
299    pub fn total_accesses(&self) -> usize {
300        self.hits + self.misses
301    }
302}
303
304// Type aliases for common cache types
305/// LRU cache for Aspect models
306pub type AspectCache = LruModelCache<Aspect>;
307
308/// LRU cache for Property elements
309pub type PropertyCache = LruModelCache<Property>;
310
311/// LRU cache for Characteristic elements
312pub type CharacteristicCache = LruModelCache<Characteristic>;
313
314/// LRU cache for Entity elements
315pub type EntityCache = LruModelCache<Entity>;
316
317/// LRU cache for Operation elements
318pub type OperationCache = LruModelCache<Operation>;
319
320/// TTL (Time-To-Live) cache entry with expiration time
321#[derive(Clone)]
322struct TtlCacheEntry<T> {
323    value: Arc<T>,
324    access_count: usize,
325    created_at: std::time::Instant,
326    expires_at: std::time::Instant,
327}
328
329/// Cache with Time-To-Live (TTL) support
330///
331/// Combines LRU eviction with time-based expiration. Entries automatically
332/// expire after a configurable TTL period.
333///
334/// # Use Cases
335///
336/// - **Remote model caching**: Auto-expire cached HTTP-fetched models
337/// - **Session-based caching**: Expire models after session timeout
338/// - **Memory safety**: Ensure stale data is automatically removed
339/// - **Time-sensitive data**: Cache models with validity periods
340///
341/// # Example
342///
343/// ```rust
344/// use oxirs_samm::cache::TtlCache;
345/// use oxirs_samm::metamodel::Aspect;
346/// use std::sync::Arc;
347/// use std::time::Duration;
348///
349/// let mut cache = TtlCache::new(100, Duration::from_secs(300)); // 5 min TTL
350///
351/// let aspect = Aspect::new("urn:samm:org.example:1.0.0#MyAspect".to_string());
352/// cache.put("my-aspect".to_string(), Arc::new(aspect));
353///
354/// // Entry expires after 5 minutes
355/// // Manual expiration check
356/// cache.evict_expired();
357/// ```
358pub struct TtlCache<T> {
359    capacity: usize,
360    ttl: std::time::Duration,
361    entries: Arc<RwLock<HashMap<String, TtlCacheEntry<T>>>>,
362    access_order: Arc<RwLock<Vec<String>>>,
363    hits: Arc<RwLock<usize>>,
364    misses: Arc<RwLock<usize>>,
365    expirations: Arc<RwLock<usize>>,
366}
367
368impl<T> TtlCache<T>
369where
370    T: Clone,
371{
372    /// Create a new TTL cache with specified capacity and TTL
373    ///
374    /// # Arguments
375    ///
376    /// * `capacity` - Maximum number of items to cache
377    /// * `ttl` - Time-to-live duration for each entry
378    ///
379    /// # Example
380    ///
381    /// ```rust
382    /// use oxirs_samm::cache::TtlCache;
383    /// use oxirs_samm::metamodel::Aspect;
384    /// use std::time::Duration;
385    ///
386    /// let cache: TtlCache<Aspect> = TtlCache::new(50, Duration::from_secs(600));
387    /// assert_eq!(cache.capacity(), 50);
388    /// ```
389    pub fn new(capacity: usize, ttl: std::time::Duration) -> Self {
390        Self {
391            capacity: capacity.max(1),
392            ttl,
393            entries: Arc::new(RwLock::new(HashMap::new())),
394            access_order: Arc::new(RwLock::new(Vec::new())),
395            hits: Arc::new(RwLock::new(0)),
396            misses: Arc::new(RwLock::new(0)),
397            expirations: Arc::new(RwLock::new(0)),
398        }
399    }
400
401    /// Get an item from the cache
402    ///
403    /// Returns None if item is expired or not found.
404    pub fn get(&self, key: &str) -> Option<Arc<T>> {
405        let mut entries = self.entries.write().expect("lock poisoned");
406
407        if let Some(entry) = entries.get_mut(key) {
408            let now = std::time::Instant::now();
409
410            // Check if expired
411            if now >= entry.expires_at {
412                // Remove expired entry
413                entries.remove(key);
414                let mut access_order = self.access_order.write().expect("lock poisoned");
415                if let Some(pos) = access_order.iter().position(|k| k == key) {
416                    access_order.remove(pos);
417                }
418                *self.expirations.write().expect("lock poisoned") += 1;
419                *self.misses.write().expect("lock poisoned") += 1;
420                return None;
421            }
422
423            // Update access statistics
424            entry.access_count += 1;
425
426            // Update access order
427            let mut access_order = self.access_order.write().expect("lock poisoned");
428            if let Some(pos) = access_order.iter().position(|k| k == key) {
429                access_order.remove(pos);
430            }
431            access_order.push(key.to_string());
432
433            // Record hit
434            *self.hits.write().expect("lock poisoned") += 1;
435
436            Some(Arc::clone(&entry.value))
437        } else {
438            // Record miss
439            *self.misses.write().expect("lock poisoned") += 1;
440            None
441        }
442    }
443
444    /// Put an item into the cache with TTL
445    ///
446    /// If the cache is at capacity, evicts the least recently used item.
447    pub fn put(&mut self, key: String, value: Arc<T>) {
448        let mut entries = self.entries.write().expect("lock poisoned");
449        let mut access_order = self.access_order.write().expect("lock poisoned");
450
451        // Remove existing entry if present
452        if entries.contains_key(&key) {
453            if let Some(pos) = access_order.iter().position(|k| k == &key) {
454                access_order.remove(pos);
455            }
456        }
457
458        // Evict LRU entry if at capacity
459        if entries.len() >= self.capacity && !entries.contains_key(&key) {
460            if let Some(lru_key) = access_order.first() {
461                let lru_key = lru_key.clone();
462                entries.remove(&lru_key);
463                access_order.remove(0);
464            }
465        }
466
467        // Insert new entry with expiration
468        let now = std::time::Instant::now();
469        let entry = TtlCacheEntry {
470            value,
471            access_count: 0,
472            created_at: now,
473            expires_at: now + self.ttl,
474        };
475
476        entries.insert(key.clone(), entry);
477        access_order.push(key);
478    }
479
480    /// Evict all expired entries
481    ///
482    /// Returns the number of entries evicted.
483    pub fn evict_expired(&mut self) -> usize {
484        let mut entries = self.entries.write().expect("lock poisoned");
485        let mut access_order = self.access_order.write().expect("lock poisoned");
486        let now = std::time::Instant::now();
487
488        let expired_keys: Vec<String> = entries
489            .iter()
490            .filter(|(_, entry)| now >= entry.expires_at)
491            .map(|(key, _)| key.clone())
492            .collect();
493
494        let count = expired_keys.len();
495
496        for key in expired_keys {
497            entries.remove(&key);
498            if let Some(pos) = access_order.iter().position(|k| k == &key) {
499                access_order.remove(pos);
500            }
501        }
502
503        *self.expirations.write().expect("lock poisoned") += count;
504        count
505    }
506
507    /// Get the number of items in the cache
508    pub fn len(&self) -> usize {
509        let entries = self.entries.read().expect("lock poisoned");
510        entries.len()
511    }
512
513    /// Check if the cache is empty
514    pub fn is_empty(&self) -> bool {
515        self.len() == 0
516    }
517
518    /// Get the cache capacity
519    pub fn capacity(&self) -> usize {
520        self.capacity
521    }
522
523    /// Get the TTL duration
524    pub fn ttl(&self) -> std::time::Duration {
525        self.ttl
526    }
527
528    /// Clear all items from the cache
529    pub fn clear(&mut self) {
530        let mut entries = self.entries.write().expect("lock poisoned");
531        let mut access_order = self.access_order.write().expect("lock poisoned");
532
533        entries.clear();
534        access_order.clear();
535    }
536
537    /// Get cache hit rate (0.0 to 1.0)
538    pub fn hit_rate(&self) -> f64 {
539        let hits = *self.hits.read().expect("lock poisoned");
540        let misses = *self.misses.read().expect("lock poisoned");
541        let total = hits + misses;
542
543        if total == 0 {
544            0.0
545        } else {
546            hits as f64 / total as f64
547        }
548    }
549
550    /// Get total number of expirations
551    pub fn expirations(&self) -> usize {
552        *self.expirations.read().expect("lock poisoned")
553    }
554
555    /// Get cache statistics including TTL information
556    pub fn statistics(&self) -> TtlCacheStatistics {
557        TtlCacheStatistics {
558            size: self.len(),
559            capacity: self.capacity,
560            hits: *self.hits.read().expect("lock poisoned"),
561            misses: *self.misses.read().expect("lock poisoned"),
562            expirations: self.expirations(),
563            hit_rate: self.hit_rate(),
564            ttl_seconds: self.ttl.as_secs(),
565        }
566    }
567}
568
569/// TTL cache statistics
570#[derive(Debug, Clone)]
571pub struct TtlCacheStatistics {
572    /// Current number of items
573    pub size: usize,
574    /// Maximum capacity
575    pub capacity: usize,
576    /// Total hits
577    pub hits: usize,
578    /// Total misses
579    pub misses: usize,
580    /// Total expirations
581    pub expirations: usize,
582    /// Hit rate (0.0 to 1.0)
583    pub hit_rate: f64,
584    /// TTL in seconds
585    pub ttl_seconds: u64,
586}
587
588impl TtlCacheStatistics {
589    /// Get the fill percentage (0.0 to 100.0)
590    pub fn fill_percentage(&self) -> f64 {
591        if self.capacity == 0 {
592            0.0
593        } else {
594            (self.size as f64 / self.capacity as f64) * 100.0
595        }
596    }
597
598    /// Get total accesses
599    pub fn total_accesses(&self) -> usize {
600        self.hits + self.misses
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607    use crate::metamodel::ElementMetadata;
608
609    #[test]
610    fn test_cache_creation() {
611        let cache: LruModelCache<Aspect> = LruModelCache::new(10);
612        assert_eq!(cache.capacity(), 10);
613        assert_eq!(cache.len(), 0);
614        assert!(cache.is_empty());
615    }
616
617    #[test]
618    fn test_put_and_get() {
619        let mut cache = LruModelCache::new(5);
620        let aspect = Arc::new(Aspect::new("urn:test:1.0.0#Test".to_string()));
621
622        cache.put("test".to_string(), Arc::clone(&aspect));
623        assert_eq!(cache.len(), 1);
624
625        let retrieved = cache.get("test");
626        assert!(retrieved.is_some());
627    }
628
629    #[test]
630    fn test_lru_eviction() {
631        let mut cache = LruModelCache::new(3);
632
633        // Fill cache to capacity
634        cache.put(
635            "a".to_string(),
636            Arc::new(Aspect::new("urn:test:1.0.0#A".to_string())),
637        );
638        cache.put(
639            "b".to_string(),
640            Arc::new(Aspect::new("urn:test:1.0.0#B".to_string())),
641        );
642        cache.put(
643            "c".to_string(),
644            Arc::new(Aspect::new("urn:test:1.0.0#C".to_string())),
645        );
646
647        assert_eq!(cache.len(), 3);
648
649        // Add one more - should evict "a" (least recently used)
650        cache.put(
651            "d".to_string(),
652            Arc::new(Aspect::new("urn:test:1.0.0#D".to_string())),
653        );
654
655        assert_eq!(cache.len(), 3);
656        assert!(!cache.contains("a")); // "a" was evicted
657        assert!(cache.contains("b"));
658        assert!(cache.contains("c"));
659        assert!(cache.contains("d"));
660    }
661
662    #[test]
663    fn test_lru_access_updates() {
664        let mut cache = LruModelCache::new(3);
665
666        cache.put(
667            "a".to_string(),
668            Arc::new(Aspect::new("urn:test:1.0.0#A".to_string())),
669        );
670        cache.put(
671            "b".to_string(),
672            Arc::new(Aspect::new("urn:test:1.0.0#B".to_string())),
673        );
674        cache.put(
675            "c".to_string(),
676            Arc::new(Aspect::new("urn:test:1.0.0#C".to_string())),
677        );
678
679        // Access "a" to make it recently used
680        cache.get("a");
681
682        // Add "d" - should evict "b" (now LRU)
683        cache.put(
684            "d".to_string(),
685            Arc::new(Aspect::new("urn:test:1.0.0#D".to_string())),
686        );
687
688        assert!(cache.contains("a")); // "a" was accessed, not evicted
689        assert!(!cache.contains("b")); // "b" was evicted
690        assert!(cache.contains("c"));
691        assert!(cache.contains("d"));
692    }
693
694    #[test]
695    fn test_remove() {
696        let mut cache = LruModelCache::new(5);
697        cache.put(
698            "test".to_string(),
699            Arc::new(Aspect::new("urn:test:1.0.0#Test".to_string())),
700        );
701
702        assert_eq!(cache.len(), 1);
703        let removed = cache.remove("test");
704        assert!(removed.is_some());
705        assert_eq!(cache.len(), 0);
706    }
707
708    #[test]
709    fn test_clear() {
710        let mut cache = LruModelCache::new(5);
711        cache.put(
712            "a".to_string(),
713            Arc::new(Aspect::new("urn:test:1.0.0#A".to_string())),
714        );
715        cache.put(
716            "b".to_string(),
717            Arc::new(Aspect::new("urn:test:1.0.0#B".to_string())),
718        );
719
720        assert_eq!(cache.len(), 2);
721        cache.clear();
722        assert_eq!(cache.len(), 0);
723        assert!(cache.is_empty());
724    }
725
726    #[test]
727    fn test_hit_rate() {
728        let mut cache = LruModelCache::new(5);
729        cache.put(
730            "test".to_string(),
731            Arc::new(Aspect::new("urn:test:1.0.0#Test".to_string())),
732        );
733
734        // 2 hits
735        cache.get("test");
736        cache.get("test");
737
738        // 1 miss
739        cache.get("nonexistent");
740
741        assert_eq!(cache.hits(), 2);
742        assert_eq!(cache.misses(), 1);
743        assert!((cache.hit_rate() - 0.666).abs() < 0.01);
744    }
745
746    #[test]
747    fn test_resize() {
748        let mut cache = LruModelCache::new(5);
749
750        // Fill with 5 items
751        for i in 0..5 {
752            cache.put(
753                format!("item{}", i),
754                Arc::new(Aspect::new(format!("urn:test:1.0.0#Item{}", i))),
755            );
756        }
757
758        assert_eq!(cache.len(), 5);
759
760        // Resize to 3 - should evict 2 LRU items
761        cache.resize(3);
762        assert_eq!(cache.capacity(), 3);
763        assert_eq!(cache.len(), 3);
764
765        // Oldest items should be evicted
766        assert!(!cache.contains("item0"));
767        assert!(!cache.contains("item1"));
768        assert!(cache.contains("item2"));
769        assert!(cache.contains("item3"));
770        assert!(cache.contains("item4"));
771    }
772
773    #[test]
774    fn test_statistics() {
775        let mut cache = LruModelCache::new(10);
776        cache.put(
777            "test".to_string(),
778            Arc::new(Aspect::new("urn:test:1.0.0#Test".to_string())),
779        );
780
781        cache.get("test");
782        cache.get("test");
783        cache.get("nonexistent");
784
785        let stats = cache.statistics();
786        assert_eq!(stats.size, 1);
787        assert_eq!(stats.capacity, 10);
788        assert_eq!(stats.hits, 2);
789        assert_eq!(stats.misses, 1);
790        assert_eq!(stats.total_accesses(), 3);
791        assert_eq!(stats.fill_percentage(), 10.0);
792    }
793
794    #[test]
795    fn test_keys() {
796        let mut cache = LruModelCache::new(5);
797        cache.put(
798            "a".to_string(),
799            Arc::new(Aspect::new("urn:test:1.0.0#A".to_string())),
800        );
801        cache.put(
802            "b".to_string(),
803            Arc::new(Aspect::new("urn:test:1.0.0#B".to_string())),
804        );
805        cache.put(
806            "c".to_string(),
807            Arc::new(Aspect::new("urn:test:1.0.0#C".to_string())),
808        );
809
810        let keys = cache.keys();
811        assert_eq!(keys.len(), 3);
812        assert!(keys.contains(&"a".to_string()));
813        assert!(keys.contains(&"b".to_string()));
814        assert!(keys.contains(&"c".to_string()));
815    }
816
817    // TTL Cache Tests
818
819    #[test]
820    fn test_ttl_cache_creation() {
821        let cache: TtlCache<Aspect> = TtlCache::new(10, std::time::Duration::from_secs(60));
822        assert_eq!(cache.capacity(), 10);
823        assert_eq!(cache.len(), 0);
824        assert!(cache.is_empty());
825        assert_eq!(cache.ttl().as_secs(), 60);
826    }
827
828    #[test]
829    fn test_ttl_cache_put_and_get() {
830        let mut cache = TtlCache::new(5, std::time::Duration::from_secs(60));
831        let aspect = Arc::new(Aspect::new("urn:test:1.0.0#Test".to_string()));
832
833        cache.put("test".to_string(), Arc::clone(&aspect));
834        assert_eq!(cache.len(), 1);
835
836        let retrieved = cache.get("test");
837        assert!(retrieved.is_some());
838    }
839
840    #[test]
841    fn test_ttl_cache_expiration() {
842        let mut cache = TtlCache::new(5, std::time::Duration::from_millis(50));
843        let aspect = Arc::new(Aspect::new("urn:test:1.0.0#Test".to_string()));
844
845        cache.put("test".to_string(), Arc::clone(&aspect));
846        assert_eq!(cache.len(), 1);
847
848        // Item should be accessible immediately
849        assert!(cache.get("test").is_some());
850
851        // Wait for expiration
852        std::thread::sleep(std::time::Duration::from_millis(100));
853
854        // Item should be expired
855        assert!(cache.get("test").is_none());
856        assert_eq!(cache.expirations(), 1);
857    }
858
859    #[test]
860    fn test_ttl_cache_evict_expired() {
861        let mut cache = TtlCache::new(5, std::time::Duration::from_millis(50));
862
863        // Add multiple items
864        for i in 0..3 {
865            cache.put(
866                format!("item{}", i),
867                Arc::new(Aspect::new(format!("urn:test:1.0.0#Item{}", i))),
868            );
869        }
870
871        assert_eq!(cache.len(), 3);
872
873        // Wait for expiration
874        std::thread::sleep(std::time::Duration::from_millis(100));
875
876        // Manually evict expired entries
877        let evicted = cache.evict_expired();
878        assert_eq!(evicted, 3);
879        assert_eq!(cache.len(), 0);
880    }
881
882    #[test]
883    fn test_ttl_cache_statistics() {
884        let mut cache = TtlCache::new(10, std::time::Duration::from_secs(60));
885        cache.put(
886            "test".to_string(),
887            Arc::new(Aspect::new("urn:test:1.0.0#Test".to_string())),
888        );
889
890        cache.get("test");
891        cache.get("test");
892        cache.get("nonexistent");
893
894        let stats = cache.statistics();
895        assert_eq!(stats.size, 1);
896        assert_eq!(stats.capacity, 10);
897        assert_eq!(stats.hits, 2);
898        assert_eq!(stats.misses, 1);
899        assert_eq!(stats.total_accesses(), 3);
900        assert_eq!(stats.ttl_seconds, 60);
901    }
902
903    #[test]
904    fn test_ttl_cache_clear() {
905        let mut cache = TtlCache::new(5, std::time::Duration::from_secs(60));
906        cache.put(
907            "a".to_string(),
908            Arc::new(Aspect::new("urn:test:1.0.0#A".to_string())),
909        );
910        cache.put(
911            "b".to_string(),
912            Arc::new(Aspect::new("urn:test:1.0.0#B".to_string())),
913        );
914
915        assert_eq!(cache.len(), 2);
916        cache.clear();
917        assert_eq!(cache.len(), 0);
918        assert!(cache.is_empty());
919    }
920
921    #[test]
922    fn test_ttl_cache_lru_eviction() {
923        let mut cache = TtlCache::new(3, std::time::Duration::from_secs(60));
924
925        // Fill cache to capacity
926        cache.put(
927            "a".to_string(),
928            Arc::new(Aspect::new("urn:test:1.0.0#A".to_string())),
929        );
930        cache.put(
931            "b".to_string(),
932            Arc::new(Aspect::new("urn:test:1.0.0#B".to_string())),
933        );
934        cache.put(
935            "c".to_string(),
936            Arc::new(Aspect::new("urn:test:1.0.0#C".to_string())),
937        );
938
939        assert_eq!(cache.len(), 3);
940
941        // Add one more - should evict "a" (LRU)
942        cache.put(
943            "d".to_string(),
944            Arc::new(Aspect::new("urn:test:1.0.0#D".to_string())),
945        );
946
947        assert_eq!(cache.len(), 3);
948        // Note: Cannot easily check which was evicted without accessing cache
949        // Just verify size is maintained
950    }
951}