1use std::collections::HashMap;
9use std::time::{Duration, Instant};
10
11const SENTINEL: usize = usize::MAX;
13
14struct LruNode<K, V> {
17 key: K,
18 value: V,
19 prev: usize,
20 next: usize,
21 size_bytes: usize,
23 access_count: u64,
25 last_accessed: Instant,
27 expires_at: Option<Instant>,
29 pinned: bool,
31}
32
33#[derive(Debug, Clone)]
37pub struct CacheStats {
38 pub hits: u64,
40 pub misses: u64,
42 pub evictions: u64,
44 pub total_size_bytes: usize,
46 pub capacity: usize,
48 pub entry_count: usize,
50 pub ttl_expirations: u64,
52 pub pinned_count: usize,
54}
55
56pub struct LruCache<K: Eq + std::hash::Hash + Clone, V> {
62 capacity: usize,
63 map: HashMap<K, usize>,
65 slots: Vec<Option<LruNode<K, V>>>,
67 head: usize,
69 tail: usize,
71 free: Vec<usize>,
73 len: usize,
75 hits: u64,
77 misses: u64,
78 evictions: u64,
79 total_size_bytes: usize,
80 ttl_expirations: u64,
82 default_ttl: Option<Duration>,
84}
85
86impl<K: Eq + std::hash::Hash + Clone, V> LruCache<K, V> {
89 fn detach(&mut self, idx: usize) {
91 let (prev, next) = {
92 let node = self.slots[idx].as_ref().expect("detach: slot must be Some");
93 (node.prev, node.next)
94 };
95 if prev != SENTINEL {
96 if let Some(n) = self.slots[prev].as_mut() {
97 n.next = next;
98 }
99 } else {
100 self.head = next;
101 }
102 if next != SENTINEL {
103 if let Some(n) = self.slots[next].as_mut() {
104 n.prev = prev;
105 }
106 } else {
107 self.tail = prev;
108 }
109 if let Some(n) = self.slots[idx].as_mut() {
110 n.prev = SENTINEL;
111 n.next = SENTINEL;
112 }
113 }
114
115 fn attach_head(&mut self, idx: usize) {
117 let old_head = self.head;
118 if let Some(n) = self.slots[idx].as_mut() {
119 n.prev = SENTINEL;
120 n.next = old_head;
121 }
122 if old_head != SENTINEL {
123 if let Some(n) = self.slots[old_head].as_mut() {
124 n.prev = idx;
125 }
126 } else {
127 self.tail = idx;
129 }
130 self.head = idx;
131 }
132
133 fn alloc_slot(&mut self) -> usize {
136 if let Some(idx) = self.free.pop() {
137 idx
138 } else {
139 let idx = self.slots.len();
140 self.slots.push(None);
141 idx
142 }
143 }
144}
145
146impl<K: Eq + std::hash::Hash + Clone, V> LruCache<K, V> {
149 pub fn new(capacity: usize) -> Self {
151 Self {
152 capacity,
153 map: HashMap::new(),
154 slots: Vec::new(),
155 head: SENTINEL,
156 tail: SENTINEL,
157 free: Vec::new(),
158 len: 0,
159 hits: 0,
160 misses: 0,
161 evictions: 0,
162 total_size_bytes: 0,
163 ttl_expirations: 0,
164 default_ttl: None,
165 }
166 }
167
168 pub fn with_default_ttl(capacity: usize, ttl: Duration) -> Self {
171 Self {
172 default_ttl: Some(ttl),
173 ..Self::new(capacity)
174 }
175 }
176
177 pub fn get(&mut self, key: &K) -> Option<&V> {
183 if let Some(&idx) = self.map.get(key) {
184 let expired = self.slots[idx]
186 .as_ref()
187 .and_then(|n| n.expires_at)
188 .map(|exp| Instant::now() >= exp)
189 .unwrap_or(false);
190 if expired {
191 self.ttl_expirations += 1;
192 self.misses += 1;
193 let key_clone = self.slots[idx].as_ref().map(|n| n.key.clone());
194 if let Some(k) = key_clone {
195 self.map.remove(&k);
196 }
197 self.detach(idx);
198 if let Some(node) = self.slots[idx].take() {
199 self.total_size_bytes = self.total_size_bytes.saturating_sub(node.size_bytes);
200 }
201 self.len -= 1;
202 self.free.push(idx);
203 return None;
204 }
205 self.hits += 1;
206 if let Some(node) = self.slots[idx].as_mut() {
207 node.access_count += 1;
208 node.last_accessed = Instant::now();
209 }
210 self.detach(idx);
211 self.attach_head(idx);
212 self.slots[idx].as_ref().map(|n| &n.value)
213 } else {
214 self.misses += 1;
215 None
216 }
217 }
218
219 pub fn insert(&mut self, key: K, value: V, size_bytes: usize) {
227 let expires_at = self.default_ttl.map(|d| Instant::now() + d);
228 self.insert_inner(key, value, size_bytes, expires_at, false);
229 }
230
231 pub fn insert_with_ttl(&mut self, key: K, value: V, size_bytes: usize, ttl: Duration) {
235 let expires_at = Some(Instant::now() + ttl);
236 self.insert_inner(key, value, size_bytes, expires_at, false);
237 }
238
239 pub fn insert_pinned(&mut self, key: K, value: V, size_bytes: usize) {
244 let expires_at = self.default_ttl.map(|d| Instant::now() + d);
245 self.insert_inner(key, value, size_bytes, expires_at, true);
246 }
247
248 fn insert_inner(
250 &mut self,
251 key: K,
252 value: V,
253 size_bytes: usize,
254 expires_at: Option<Instant>,
255 pinned: bool,
256 ) {
257 if let Some(&idx) = self.map.get(&key) {
258 let old_size = self.slots[idx].as_ref().map(|n| n.size_bytes).unwrap_or(0);
260 self.total_size_bytes = self.total_size_bytes.saturating_sub(old_size);
261 if let Some(node) = self.slots[idx].as_mut() {
262 node.value = value;
263 node.size_bytes = size_bytes;
264 node.last_accessed = Instant::now();
265 node.expires_at = expires_at;
266 node.pinned = pinned;
267 }
268 self.total_size_bytes += size_bytes;
269 self.detach(idx);
270 self.attach_head(idx);
271 return;
272 }
273
274 if self.len == self.capacity {
276 self.evict_lru();
277 }
278
279 let idx = self.alloc_slot();
280 self.slots[idx] = Some(LruNode {
281 key: key.clone(),
282 value,
283 prev: SENTINEL,
284 next: SENTINEL,
285 size_bytes,
286 access_count: 0,
287 last_accessed: Instant::now(),
288 expires_at,
289 pinned,
290 });
291 self.attach_head(idx);
292 self.map.insert(key, idx);
293 self.total_size_bytes += size_bytes;
294 self.len += 1;
295 }
296
297 pub fn remove(&mut self, key: &K) -> Option<V> {
299 let idx = self.map.remove(key)?;
300 self.detach(idx);
301 let node = self.slots[idx].take()?;
302 self.total_size_bytes = self.total_size_bytes.saturating_sub(node.size_bytes);
303 self.len -= 1;
304 self.free.push(idx);
305 Some(node.value)
306 }
307
308 pub fn contains(&self, key: &K) -> bool {
310 self.map.contains_key(key)
311 }
312
313 pub fn len(&self) -> usize {
315 self.len
316 }
317
318 pub fn is_empty(&self) -> bool {
320 self.len == 0
321 }
322
323 pub fn stats(&self) -> CacheStats {
325 let pinned_count = self
326 .slots
327 .iter()
328 .filter(|s| s.as_ref().map(|n| n.pinned).unwrap_or(false))
329 .count();
330 CacheStats {
331 hits: self.hits,
332 misses: self.misses,
333 evictions: self.evictions,
334 total_size_bytes: self.total_size_bytes,
335 capacity: self.capacity,
336 entry_count: self.len,
337 ttl_expirations: self.ttl_expirations,
338 pinned_count,
339 }
340 }
341
342 pub fn peek(&self, key: &K) -> Option<&V> {
345 self.map
346 .get(key)
347 .and_then(|&idx| self.slots[idx].as_ref())
348 .map(|n| &n.value)
349 }
350
351 pub fn evict_lru(&mut self) -> Option<(K, V)> {
357 if self.tail == SENTINEL {
358 return None;
359 }
360 let mut candidate = self.tail;
363 while candidate != SENTINEL {
364 let is_pinned = self.slots[candidate]
365 .as_ref()
366 .map(|n| n.pinned)
367 .unwrap_or(false);
368 if !is_pinned {
369 break;
370 }
371 candidate = self.slots[candidate]
372 .as_ref()
373 .map(|n| n.prev)
374 .unwrap_or(SENTINEL);
375 }
376 if candidate == SENTINEL {
377 return None;
378 }
379 let key = self.slots[candidate].as_ref()?.key.clone();
380 self.map.remove(&key);
381 self.detach(candidate);
382 let node = self.slots[candidate].take()?;
383 self.total_size_bytes = self.total_size_bytes.saturating_sub(node.size_bytes);
384 self.len -= 1;
385 self.evictions += 1;
386 self.free.push(candidate);
387 Some((key, node.value))
388 }
389
390 pub fn set_default_ttl(&mut self, ttl: Option<Duration>) {
394 self.default_ttl = ttl;
395 }
396
397 pub fn purge_expired(&mut self) -> usize {
400 let now = Instant::now();
401 let expired_keys: Vec<K> = self
402 .map
403 .keys()
404 .filter(|k| {
405 self.map
406 .get(*k)
407 .and_then(|&idx| self.slots[idx].as_ref().and_then(|n| n.expires_at))
408 .map(|exp| now >= exp)
409 .unwrap_or(false)
410 })
411 .cloned()
412 .collect();
413 let count = expired_keys.len();
414 for key in expired_keys {
415 if let Some(idx) = self.map.remove(&key) {
416 self.detach(idx);
417 if let Some(node) = self.slots[idx].take() {
418 self.total_size_bytes = self.total_size_bytes.saturating_sub(node.size_bytes);
419 }
420 self.len -= 1;
421 self.ttl_expirations += 1;
422 self.free.push(idx);
423 }
424 }
425 count
426 }
427
428 pub fn pin(&mut self, key: &K) -> bool {
434 if let Some(&idx) = self.map.get(key) {
435 if let Some(node) = self.slots[idx].as_mut() {
436 node.pinned = true;
437 return true;
438 }
439 }
440 false
441 }
442
443 pub fn unpin(&mut self, key: &K) -> bool {
447 if let Some(&idx) = self.map.get(key) {
448 if let Some(node) = self.slots[idx].as_mut() {
449 node.pinned = false;
450 return true;
451 }
452 }
453 false
454 }
455
456 pub fn is_pinned(&self, key: &K) -> bool {
458 self.map
459 .get(key)
460 .and_then(|&idx| self.slots[idx].as_ref())
461 .map(|n| n.pinned)
462 .unwrap_or(false)
463 }
464
465 pub fn refresh_ttl(&mut self, key: &K) -> bool {
473 if let Some(&idx) = self.map.get(key) {
474 if let Some(node) = self.slots[idx].as_mut() {
475 if let Some(old_exp) = node.expires_at {
476 let ttl = if let Some(d) = self.default_ttl {
483 d
484 } else {
485 let now = Instant::now();
488 if old_exp > now {
489 old_exp.duration_since(now)
490 } else {
491 return false;
494 }
495 };
496 node.expires_at = Some(Instant::now() + ttl);
497 return true;
498 }
499 }
500 }
501 false
502 }
503
504 pub fn set_entry_ttl(&mut self, key: &K, ttl: Duration) -> bool {
508 if let Some(&idx) = self.map.get(key) {
509 if let Some(node) = self.slots[idx].as_mut() {
510 node.expires_at = Some(Instant::now() + ttl);
511 return true;
512 }
513 }
514 false
515 }
516
517 pub fn clear_entry_ttl(&mut self, key: &K) -> bool {
522 if let Some(&idx) = self.map.get(key) {
523 if let Some(node) = self.slots[idx].as_mut() {
524 node.expires_at = None;
525 return true;
526 }
527 }
528 false
529 }
530
531 pub fn remaining_ttl(&self, key: &K) -> Option<Duration> {
534 self.map
535 .get(key)
536 .and_then(|&idx| self.slots[idx].as_ref())
537 .and_then(|n| n.expires_at)
538 .and_then(|exp| {
539 let now = Instant::now();
540 if exp > now {
541 Some(exp.duration_since(now))
542 } else {
543 None }
545 })
546 }
547
548 pub fn pinned_count(&self) -> usize {
552 self.slots
553 .iter()
554 .filter(|s| s.as_ref().map(|n| n.pinned).unwrap_or(false))
555 .count()
556 }
557
558 pub fn unpin_all(&mut self) -> usize {
561 let mut count = 0usize;
562 for slot in &mut self.slots {
563 if let Some(node) = slot.as_mut() {
564 if node.pinned {
565 node.pinned = false;
566 count += 1;
567 }
568 }
569 }
570 count
571 }
572
573 pub fn resize(&mut self, new_capacity: usize) -> usize {
580 let new_cap = new_capacity.max(1);
581 self.capacity = new_cap;
582 let mut evicted = 0usize;
583 while self.len > new_cap {
584 if self.evict_lru().is_some() {
585 evicted += 1;
586 } else {
587 break; }
589 }
590 evicted
591 }
592
593 pub fn capacity(&self) -> usize {
595 self.capacity
596 }
597
598 pub fn keys(&self) -> Vec<K> {
603 self.map.keys().cloned().collect()
604 }
605
606 pub fn clear(&mut self) {
608 self.map.clear();
609 self.slots.clear();
610 self.free.clear();
611 self.head = SENTINEL;
612 self.tail = SENTINEL;
613 self.len = 0;
614 self.total_size_bytes = 0;
615 }
616
617 pub fn access_count(&self, key: &K) -> Option<u64> {
620 self.map
621 .get(key)
622 .and_then(|&idx| self.slots[idx].as_ref())
623 .map(|n| n.access_count)
624 }
625}
626
627#[cfg(test)]
630mod tests {
631 use super::*;
632
633 #[test]
635 fn test_insert_and_get() {
636 let mut cache: LruCache<&str, i32> = LruCache::new(4);
637 cache.insert("a", 1, 10);
638 cache.insert("b", 2, 20);
639 assert_eq!(cache.get(&"a"), Some(&1));
640 assert_eq!(cache.get(&"b"), Some(&2));
641 }
642
643 #[test]
645 fn test_miss_on_absent_key() {
646 let mut cache: LruCache<&str, i32> = LruCache::new(4);
647 cache.insert("a", 1, 10);
648 assert_eq!(cache.get(&"z"), None);
649 }
650
651 #[test]
653 fn test_lru_eviction() {
654 let mut cache: LruCache<i32, &str> = LruCache::new(3);
655 cache.insert(1, "one", 1);
656 cache.insert(2, "two", 1);
657 cache.insert(3, "three", 1);
658 cache.get(&1);
660 cache.insert(4, "four", 1);
662 assert!(!cache.contains(&2), "key 2 should have been evicted");
663 assert!(cache.contains(&1));
664 assert!(cache.contains(&3));
665 assert!(cache.contains(&4));
666 }
667
668 #[test]
670 fn test_len_and_is_empty() {
671 let mut cache: LruCache<u32, u32> = LruCache::new(5);
672 assert!(cache.is_empty());
673 cache.insert(1, 100, 8);
674 cache.insert(2, 200, 8);
675 assert_eq!(cache.len(), 2);
676 assert!(!cache.is_empty());
677 }
678
679 #[test]
681 fn test_remove() {
682 let mut cache: LruCache<&str, u64> = LruCache::new(4);
683 cache.insert("x", 42, 8);
684 let removed = cache.remove(&"x");
685 assert_eq!(removed, Some(42));
686 assert!(!cache.contains(&"x"));
687 assert_eq!(cache.len(), 0);
688 }
689
690 #[test]
692 fn test_peek_no_side_effects() {
693 let mut cache: LruCache<i32, i32> = LruCache::new(3);
694 cache.insert(1, 10, 1);
695 cache.insert(2, 20, 1);
696 cache.insert(3, 30, 1);
697 let _ = cache.peek(&1);
699 cache.insert(4, 40, 1);
701 assert!(!cache.contains(&1));
702 }
703
704 #[test]
706 fn test_evict_lru_manual() {
707 let mut cache: LruCache<&str, i32> = LruCache::new(4);
708 cache.insert("a", 1, 1);
709 cache.insert("b", 2, 1);
710 cache.insert("c", 3, 1);
711 let evicted = cache.evict_lru();
712 assert!(evicted.is_some());
713 let (k, _v) = evicted.expect("eviction should succeed");
714 assert_eq!(
715 k, "a",
716 "oldest-inserted key should be evicted when nothing was accessed"
717 );
718 }
719
720 #[test]
722 fn test_stats_hit_miss() {
723 let mut cache: LruCache<i32, i32> = LruCache::new(4);
724 cache.insert(1, 10, 8);
725 cache.get(&1);
726 cache.get(&1);
727 cache.get(&99); let s = cache.stats();
729 assert_eq!(s.hits, 2);
730 assert_eq!(s.misses, 1);
731 }
732
733 #[test]
735 fn test_stats_evictions() {
736 let mut cache: LruCache<i32, i32> = LruCache::new(2);
737 cache.insert(1, 1, 1);
738 cache.insert(2, 2, 1);
739 cache.insert(3, 3, 1); cache.insert(4, 4, 1); assert_eq!(cache.stats().evictions, 2);
742 }
743
744 #[test]
746 fn test_total_size_bytes() {
747 let mut cache: LruCache<i32, i32> = LruCache::new(10);
748 cache.insert(1, 1, 100);
749 cache.insert(2, 2, 200);
750 assert_eq!(cache.stats().total_size_bytes, 300);
751 cache.remove(&1);
752 assert_eq!(cache.stats().total_size_bytes, 200);
753 }
754
755 #[test]
757 fn test_update_existing_key() {
758 let mut cache: LruCache<i32, i32> = LruCache::new(4);
759 cache.insert(1, 10, 100);
760 cache.insert(1, 20, 50); assert_eq!(cache.get(&1), Some(&20));
762 assert_eq!(cache.stats().total_size_bytes, 50);
763 assert_eq!(cache.len(), 1);
764 }
765
766 #[test]
768 fn test_contains() {
769 let mut cache: LruCache<&str, i32> = LruCache::new(4);
770 cache.insert("hello", 1, 5);
771 assert!(cache.contains(&"hello"));
772 assert!(!cache.contains(&"world"));
773 }
774
775 #[test]
777 fn test_evict_lru_empty() {
778 let mut cache: LruCache<i32, i32> = LruCache::new(4);
779 assert_eq!(cache.evict_lru(), None);
780 }
781
782 #[test]
784 fn test_stats_capacity() {
785 let cache: LruCache<i32, i32> = LruCache::new(7);
786 assert_eq!(cache.stats().capacity, 7);
787 assert_eq!(cache.stats().entry_count, 0);
788 }
789
790 #[test]
792 fn test_large_sequential_workload() {
793 let cap = 10usize;
794 let mut cache: LruCache<usize, usize> = LruCache::new(cap);
795 for i in 0..100 {
796 cache.insert(i, i * 2, 1);
797 }
798 assert_eq!(cache.len(), cap);
799 for i in (100 - cap)..100 {
801 assert!(cache.contains(&i), "key {i} should be present");
802 }
803 }
804
805 #[test]
809 fn test_ttl_expired_entry_returns_none() {
810 let mut cache: LruCache<&str, i32> = LruCache::new(4);
811 cache.insert_with_ttl("ephemeral", 42, 8, Duration::from_millis(0));
813 std::thread::sleep(Duration::from_millis(2));
815 assert_eq!(
816 cache.get(&"ephemeral"),
817 None,
818 "expired entry should be gone"
819 );
820 assert_eq!(cache.len(), 0);
821 assert_eq!(cache.stats().ttl_expirations, 1);
822 }
823
824 #[test]
826 fn test_ttl_non_expired_entry() {
827 let mut cache: LruCache<&str, i32> = LruCache::new(4);
828 cache.insert_with_ttl("long_lived", 99, 8, Duration::from_secs(3600));
829 assert_eq!(cache.get(&"long_lived"), Some(&99));
830 }
831
832 #[test]
834 fn test_default_ttl() {
835 let mut cache: LruCache<&str, i32> =
836 LruCache::with_default_ttl(4, Duration::from_millis(0));
837 cache.insert("a", 1, 8);
838 std::thread::sleep(Duration::from_millis(2));
839 assert_eq!(cache.get(&"a"), None, "default TTL should expire entry");
840 }
841
842 #[test]
844 fn test_purge_expired() {
845 let mut cache: LruCache<&str, i32> = LruCache::new(4);
846 cache.insert_with_ttl("x", 1, 8, Duration::from_millis(0));
847 cache.insert("y", 2, 8); std::thread::sleep(Duration::from_millis(2));
849 let purged = cache.purge_expired();
850 assert_eq!(purged, 1);
851 assert_eq!(cache.len(), 1);
852 assert!(cache.contains(&"y"));
853 }
854
855 #[test]
857 fn test_set_default_ttl() {
858 let mut cache: LruCache<&str, i32> = LruCache::new(4);
859 cache.set_default_ttl(Some(Duration::from_secs(3600)));
860 cache.insert("a", 1, 8);
861 assert_eq!(cache.get(&"a"), Some(&1));
862 cache.set_default_ttl(None);
863 cache.insert("b", 2, 8);
864 assert_eq!(cache.get(&"b"), Some(&2));
865 }
866
867 #[test]
871 fn test_pinned_entry_survives_eviction() {
872 let mut cache: LruCache<i32, &str> = LruCache::new(3);
873 cache.insert_pinned(1, "pinned", 1);
874 cache.insert(2, "two", 1);
875 cache.insert(3, "three", 1);
876 cache.insert(4, "four", 1);
878 assert!(cache.contains(&1), "pinned entry should survive");
879 assert!(!cache.contains(&2), "unpinned LRU should be evicted");
880 }
881
882 #[test]
884 fn test_insert_pinned() {
885 let mut cache: LruCache<&str, i32> = LruCache::new(4);
886 cache.insert_pinned("critical", 99, 8);
887 assert!(cache.is_pinned(&"critical"));
888 assert_eq!(cache.stats().pinned_count, 1);
889 }
890
891 #[test]
893 fn test_pin_and_unpin() {
894 let mut cache: LruCache<&str, i32> = LruCache::new(4);
895 cache.insert("x", 1, 8);
896 assert!(!cache.is_pinned(&"x"));
897 assert!(cache.pin(&"x"));
898 assert!(cache.is_pinned(&"x"));
899 assert!(cache.unpin(&"x"));
900 assert!(!cache.is_pinned(&"x"));
901 }
902
903 #[test]
905 fn test_pin_absent() {
906 let mut cache: LruCache<&str, i32> = LruCache::new(4);
907 assert!(!cache.pin(&"ghost"));
908 }
909
910 #[test]
912 fn test_all_pinned_evict_returns_none() {
913 let mut cache: LruCache<i32, i32> = LruCache::new(3);
914 cache.insert_pinned(1, 10, 1);
915 cache.insert_pinned(2, 20, 1);
916 cache.insert_pinned(3, 30, 1);
917 assert_eq!(cache.evict_lru(), None);
918 }
919
920 #[test]
922 fn test_pinned_entry_can_be_removed() {
923 let mut cache: LruCache<&str, i32> = LruCache::new(4);
924 cache.insert_pinned("keep", 42, 8);
925 let removed = cache.remove(&"keep");
926 assert_eq!(removed, Some(42));
927 assert_eq!(cache.len(), 0);
928 }
929
930 #[test]
934 fn test_refresh_ttl() {
935 let mut cache: LruCache<&str, i32> =
936 LruCache::with_default_ttl(4, Duration::from_secs(3600));
937 cache.insert("a", 1, 8);
938 assert!(cache.refresh_ttl(&"a"));
939 assert_eq!(cache.get(&"a"), Some(&1));
941 }
942
943 #[test]
945 fn test_refresh_ttl_absent() {
946 let mut cache: LruCache<&str, i32> = LruCache::new(4);
947 assert!(!cache.refresh_ttl(&"ghost"));
948 }
949
950 #[test]
952 fn test_refresh_ttl_no_ttl() {
953 let mut cache: LruCache<&str, i32> = LruCache::new(4);
954 cache.insert("a", 1, 8);
955 assert!(!cache.refresh_ttl(&"a"));
956 }
957
958 #[test]
960 fn test_set_entry_ttl() {
961 let mut cache: LruCache<&str, i32> = LruCache::new(4);
962 cache.insert("a", 1, 8);
963 assert!(cache.set_entry_ttl(&"a", Duration::from_millis(0)));
964 std::thread::sleep(Duration::from_millis(2));
965 assert_eq!(cache.get(&"a"), None, "entry should expire with new TTL");
966 }
967
968 #[test]
970 fn test_set_entry_ttl_absent() {
971 let mut cache: LruCache<&str, i32> = LruCache::new(4);
972 assert!(!cache.set_entry_ttl(&"nope", Duration::from_secs(60)));
973 }
974
975 #[test]
977 fn test_clear_entry_ttl() {
978 let mut cache: LruCache<&str, i32> =
979 LruCache::with_default_ttl(4, Duration::from_millis(1));
980 cache.insert("a", 1, 8);
981 assert!(cache.clear_entry_ttl(&"a"));
982 std::thread::sleep(Duration::from_millis(5));
983 assert_eq!(cache.get(&"a"), Some(&1));
985 }
986
987 #[test]
989 fn test_remaining_ttl() {
990 let mut cache: LruCache<&str, i32> = LruCache::new(4);
991 cache.insert_with_ttl("a", 1, 8, Duration::from_secs(3600));
992 let remaining = cache.remaining_ttl(&"a");
993 assert!(remaining.is_some());
994 let r = remaining.expect("should have remaining TTL");
995 assert!(r.as_secs() > 3590, "remaining TTL should be close to 3600s");
996 }
997
998 #[test]
1000 fn test_remaining_ttl_expired() {
1001 let mut cache: LruCache<&str, i32> = LruCache::new(4);
1002 cache.insert_with_ttl("a", 1, 8, Duration::from_millis(0));
1003 std::thread::sleep(Duration::from_millis(2));
1004 assert!(cache.remaining_ttl(&"a").is_none());
1005 }
1006
1007 #[test]
1009 fn test_remaining_ttl_no_ttl() {
1010 let mut cache: LruCache<&str, i32> = LruCache::new(4);
1011 cache.insert("a", 1, 8);
1012 assert!(cache.remaining_ttl(&"a").is_none());
1013 }
1014
1015 #[test]
1019 fn test_pinned_count() {
1020 let mut cache: LruCache<i32, i32> = LruCache::new(10);
1021 cache.insert_pinned(1, 10, 1);
1022 cache.insert_pinned(2, 20, 1);
1023 cache.insert(3, 30, 1);
1024 assert_eq!(cache.pinned_count(), 2);
1025 }
1026
1027 #[test]
1029 fn test_unpin_all() {
1030 let mut cache: LruCache<i32, i32> = LruCache::new(10);
1031 cache.insert_pinned(1, 10, 1);
1032 cache.insert_pinned(2, 20, 1);
1033 cache.insert_pinned(3, 30, 1);
1034 let unpinned = cache.unpin_all();
1035 assert_eq!(unpinned, 3);
1036 assert_eq!(cache.pinned_count(), 0);
1037 }
1038
1039 #[test]
1041 fn test_unpin_all_none_pinned() {
1042 let mut cache: LruCache<i32, i32> = LruCache::new(10);
1043 cache.insert(1, 10, 1);
1044 assert_eq!(cache.unpin_all(), 0);
1045 }
1046
1047 #[test]
1049 fn test_unpin_all_allows_eviction() {
1050 let mut cache: LruCache<i32, i32> = LruCache::new(3);
1051 cache.insert_pinned(1, 10, 1);
1052 cache.insert_pinned(2, 20, 1);
1053 cache.insert_pinned(3, 30, 1);
1054 assert_eq!(cache.evict_lru(), None); cache.unpin_all();
1056 assert!(cache.evict_lru().is_some());
1057 }
1058
1059 #[test]
1063 fn test_resize_shrink() {
1064 let mut cache: LruCache<i32, i32> = LruCache::new(10);
1065 for i in 0..10 {
1066 cache.insert(i, i * 10, 1);
1067 }
1068 let evicted = cache.resize(5);
1069 assert_eq!(evicted, 5);
1070 assert_eq!(cache.len(), 5);
1071 assert_eq!(cache.capacity(), 5);
1072 }
1073
1074 #[test]
1076 fn test_resize_grow() {
1077 let mut cache: LruCache<i32, i32> = LruCache::new(5);
1078 for i in 0..5 {
1079 cache.insert(i, i, 1);
1080 }
1081 let evicted = cache.resize(20);
1082 assert_eq!(evicted, 0);
1083 assert_eq!(cache.len(), 5);
1084 assert_eq!(cache.capacity(), 20);
1085 }
1086
1087 #[test]
1089 fn test_resize_with_pinned() {
1090 let mut cache: LruCache<i32, i32> = LruCache::new(5);
1091 cache.insert_pinned(1, 10, 1);
1092 cache.insert(2, 20, 1);
1093 cache.insert(3, 30, 1);
1094 cache.insert(4, 40, 1);
1095 cache.insert(5, 50, 1);
1096 let evicted = cache.resize(2);
1097 assert!(evicted >= 3);
1099 assert!(cache.contains(&1), "pinned entry should survive");
1100 }
1101
1102 #[test]
1106 fn test_keys() {
1107 let mut cache: LruCache<i32, i32> = LruCache::new(10);
1108 cache.insert(1, 10, 1);
1109 cache.insert(2, 20, 1);
1110 cache.insert(3, 30, 1);
1111 let mut keys = cache.keys();
1112 keys.sort();
1113 assert_eq!(keys, vec![1, 2, 3]);
1114 }
1115
1116 #[test]
1118 fn test_clear() {
1119 let mut cache: LruCache<i32, i32> = LruCache::new(10);
1120 cache.insert(1, 10, 100);
1121 cache.insert(2, 20, 200);
1122 cache.get(&1);
1123 cache.clear();
1124 assert!(cache.is_empty());
1125 assert_eq!(cache.stats().total_size_bytes, 0);
1126 assert_eq!(cache.len(), 0);
1127 }
1128
1129 #[test]
1131 fn test_access_count() {
1132 let mut cache: LruCache<&str, i32> = LruCache::new(4);
1133 cache.insert("a", 1, 8);
1134 cache.get(&"a");
1135 cache.get(&"a");
1136 cache.get(&"a");
1137 assert_eq!(cache.access_count(&"a"), Some(3));
1138 }
1139
1140 #[test]
1142 fn test_access_count_absent() {
1143 let cache: LruCache<&str, i32> = LruCache::new(4);
1144 assert_eq!(cache.access_count(&"ghost"), None);
1145 }
1146
1147 #[test]
1149 fn test_ttl_stats_counter() {
1150 let mut cache: LruCache<i32, i32> = LruCache::new(10);
1151 cache.insert_with_ttl(1, 10, 8, Duration::from_millis(0));
1152 cache.insert_with_ttl(2, 20, 8, Duration::from_millis(0));
1153 std::thread::sleep(Duration::from_millis(5));
1154 cache.get(&1); cache.get(&2); assert_eq!(cache.stats().ttl_expirations, 2);
1157 }
1158
1159 #[test]
1161 fn test_pinned_entry_with_ttl_expires() {
1162 let mut cache: LruCache<&str, i32> = LruCache::new(4);
1163 cache.insert_with_ttl("pinned_ttl", 42, 8, Duration::from_millis(0));
1164 cache.pin(&"pinned_ttl");
1165 std::thread::sleep(Duration::from_millis(5));
1166 assert_eq!(cache.get(&"pinned_ttl"), None);
1168 }
1169
1170 #[test]
1172 fn test_mixed_ttl_and_pinning_workload() {
1173 let mut cache: LruCache<i32, i32> = LruCache::new(20);
1174 for i in 0..5 {
1176 cache.insert_pinned(i, i * 100, 10);
1177 }
1178 for i in 5..15 {
1180 cache.insert_with_ttl(i, i * 100, 10, Duration::from_secs(3600));
1181 }
1182 for i in 15..20 {
1184 cache.insert(i, i * 100, 10);
1185 }
1186 assert_eq!(cache.len(), 20);
1187 assert_eq!(cache.pinned_count(), 5);
1188
1189 for i in 100..110 {
1191 cache.insert(i, i, 10);
1192 }
1193 for i in 0..5 {
1195 assert!(cache.contains(&i), "pinned entry {i} should survive");
1196 }
1197 }
1198
1199 #[test]
1201 fn test_purge_expired_mixed() {
1202 let mut cache: LruCache<i32, i32> = LruCache::new(10);
1203 cache.insert_with_ttl(1, 10, 8, Duration::from_millis(0)); cache.insert_with_ttl(2, 20, 8, Duration::from_secs(3600)); cache.insert(3, 30, 8); std::thread::sleep(Duration::from_millis(5));
1207 let purged = cache.purge_expired();
1208 assert_eq!(purged, 1);
1209 assert_eq!(cache.len(), 2);
1210 assert!(cache.contains(&2));
1211 assert!(cache.contains(&3));
1212 }
1213}