cachekit/policy/nru.rs
1//! NRU (Not Recently Used) cache replacement policy.
2//!
3//! Implements the Not Recently Used algorithm, which uses a single reference bit
4//! per entry to approximate LRU with minimal overhead. The reference bit provides
5//! a coarse distinction between "recently used" and "not recently used" items.
6//!
7//! ## Architecture
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────────────────────────┐
11//! │ NruCache<K, V> Layout │
12//! │ │
13//! │ ┌─────────────────────────────────────────────────────────────────────┐ │
14//! │ │ map: HashMap<K, Entry<V>> keys: Vec<K> │ │
15//! │ │ key → (index, value, ref) dense array of keys │ │
16//! │ │ │ │
17//! │ │ ┌──────────┬─────────────────┐ ┌─────┬─────┬─────┬─────┐ │ │
18//! │ │ │ Key │ Entry │ │ 0 │ 1 │ 2 │ 3 │ │ │
19//! │ │ ├──────────┼─────────────────┤ ├─────┼─────┼─────┼─────┤ │ │
20//! │ │ │ "page1" │(0, v1, ref=1) │──┐ │ p1 │ p2 │ p3 │ p4 │ │ │
21//! │ │ │ "page2" │(1, v2, ref=0) │──┼──►└─────┴─────┴─────┴─────┘ │ │
22//! │ │ │ "page3" │(2, v3, ref=1) │──┘ │ │
23//! │ │ │ "page4" │(3, v4, ref=0) │ ← Eviction candidate (ref=0) │ │
24//! │ │ └──────────┴─────────────────┘ │ │
25//! │ └─────────────────────────────────────────────────────────────────────┘ │
26//! │ │
27//! │ ┌─────────────────────────────────────────────────────────────────────┐ │
28//! │ │ NRU Eviction (O(n) worst case) │ │
29//! │ │ │ │
30//! │ │ 1. Scan keys vec for first entry with ref=0 │ │
31//! │ │ 2. If found: evict that entry │ │
32//! │ │ 3. If not found: clear all ref bits, then evict first entry │ │
33//! │ │ │ │
34//! │ │ epoch_counter: Tracks when to do bulk ref bit clearing │ │
35//! │ │ epoch_threshold: Number of accesses before clearing all bits │ │
36//! │ └─────────────────────────────────────────────────────────────────────┘ │
37//! │ │
38//! └─────────────────────────────────────────────────────────────────────────────┘
39//!
40//! Access Flow
41//! ──────────────────────
42//!
43//! get("key"):
44//! 1. Lookup entry in map
45//! 2. Set entry.referenced = true
46//! 3. Return &value
47//!
48//! Insert Flow (new key)
49//! ──────────────────────
50//!
51//! insert("new_key", value):
52//! 1. Check map - not found
53//! 2. Evict if at capacity
54//! 3. Get next index: idx = keys.len()
55//! 4. Push key to keys vec
56//! 5. Insert Entry{idx, value, referenced: false} into map
57//!
58//! Eviction Flow
59//! ─────────────
60//!
61//! evict_nru():
62//! 1. Scan keys for first entry with referenced=false
63//! 2. If found: remove that entry (swap-remove)
64//! 3. If not found (all referenced):
65//! a. Clear all reference bits
66//! b. Evict first entry (now all have ref=false)
67//! ```
68//!
69//! ## Algorithm
70//!
71//! ```text
72//! GET(key):
73//! 1. Look up entry in hash map
74//! 2. Set referenced = true
75//! 3. Return value
76//! Cost: O(1) - just a hash lookup and bit set
77//!
78//! INSERT(key, value):
79//! 1. If key exists: update value, set referenced = true
80//! 2. If at capacity: run eviction
81//! 3. Insert entry with referenced = true
82//!
83//! EVICT():
84//! // Phase 1: Try to find unreferenced entry
85//! for each entry in keys:
86//! if entry.referenced == false:
87//! remove entry
88//! return
89//!
90//! // Phase 2: All referenced - clear and pick first
91//! for each entry:
92//! entry.referenced = false
93//! remove first entry
94//! ```
95//!
96//! ## Performance Characteristics
97//!
98//! | Operation | Time | Notes |
99//! |-----------|---------|-----------------------------------|
100//! | `get` | O(1) | Hash lookup + bit set |
101//! | `insert` | O(n)* | *Worst case if all entries ref'd |
102//! | `contains`| O(1) | Hash lookup only |
103//! | `remove` | O(1) | Hash lookup + swap-remove |
104//!
105//! ## Trade-offs
106//!
107//! | Aspect | NRU | Clock | LRU |
108//! |---------------|--------------------------|-------------------------|-------------------------|
109//! | Access cost | O(1) bit set | O(1) bit set | O(1) list move |
110//! | Eviction cost | O(n) worst case | O(n) worst case | O(1) |
111//! | Granularity | Binary (used/not used) | Binary with hand sweep | Full order |
112//! | Overhead | 1 bit per entry | 1 bit per entry + hand | 16 bytes per entry |
113//!
114//! ## When to Use
115//!
116//! **Use NRU when:**
117//! - You need simple, coarse eviction tracking
118//! - Memory for full LRU list is too expensive
119//! - You can tolerate O(n) eviction in worst case
120//! - Access patterns have temporal locality
121//!
122//! **Avoid NRU when:**
123//! - You need strict LRU ordering (use [`ConcurrentLruCache`](super::lru::ConcurrentLruCache))
124//! - You need O(1) eviction guarantees (use [`ClockCache`](super::clock::ClockCache))
125//! - You need scan resistance (use [`S3FifoCache`](super::s3_fifo::S3FifoCache),
126//! [`LrukCache`](super::lru_k::LrukCache))
127//!
128//! ## Example Usage
129//!
130//! ```
131//! use cachekit::policy::nru::NruCache;
132//! use cachekit::traits::Cache;
133//!
134//! let mut cache = NruCache::new(100);
135//!
136//! // Insert items
137//! cache.insert("page1", "content1");
138//! cache.insert("page2", "content2");
139//!
140//! // Access sets reference bit
141//! assert_eq!(cache.get(&"page1"), Some(&"content1"));
142//!
143//! // Unreferenced items are evicted first
144//! // Referenced items protected until next epoch
145//! ```
146//!
147//! ## Implementation
148//!
149//! This implementation uses:
150//! - `HashMap<K, Entry<V>>` for O(1) lookup (stores index, value, referenced bit)
151//! - `Vec<K>` for dense key storage and eviction scanning
152//! - Swap-remove technique for O(1) removal (updates index in moved entry)
153//! - Lazy clearing of reference bits (only when needed during eviction)
154//!
155//! ## Thread Safety
156//!
157//! - [`NruCache`]: Not thread-safe, designed for single-threaded use
158//! - For concurrent access, wrap in external synchronization (e.g., `Mutex`)
159//!
160//! ## References
161//!
162//! - Wikipedia: Cache replacement policies
163
164use crate::traits::Cache;
165use rustc_hash::FxHashMap;
166use std::fmt::{Debug, Formatter};
167use std::hash::Hash;
168
169#[cfg(feature = "metrics")]
170use crate::metrics::metrics_impl::NruMetrics;
171#[cfg(feature = "metrics")]
172use crate::metrics::snapshot::NruMetricsSnapshot;
173#[cfg(feature = "metrics")]
174use crate::metrics::traits::MetricsSnapshotProvider;
175
176/// Entry in the NRU cache containing value, index, and reference bit.
177#[derive(Debug, Clone)]
178struct Entry<V> {
179 /// Index in the keys vector
180 index: usize,
181 /// Cached value
182 value: V,
183 /// Reference bit - set on access, cleared during epoch reset
184 referenced: bool,
185}
186
187/// NRU (Not Recently Used) cache implementation.
188///
189/// Uses a single reference bit per entry to distinguish between recently used
190/// and not recently used items. Provides O(1) access but O(n) worst-case eviction.
191///
192/// # Type Parameters
193///
194/// - `K`: Key type, must be `Clone + Eq + Hash`
195/// - `V`: Value type
196///
197/// # Example
198///
199/// ```
200/// use cachekit::policy::nru::NruCache;
201/// use cachekit::traits::Cache;
202///
203/// let mut cache = NruCache::new(100);
204///
205/// cache.insert(1, "value1");
206/// cache.insert(2, "value2");
207///
208/// // Access sets reference bit
209/// assert_eq!(cache.get(&1), Some(&"value1"));
210///
211/// // When cache is full, unreferenced items are evicted first
212/// for i in 3..=110 {
213/// cache.insert(i, "value");
214/// }
215///
216/// assert_eq!(cache.len(), 100);
217/// ```
218///
219/// # Eviction Behavior
220///
221/// When capacity is exceeded:
222/// 1. Scans for first entry with `referenced = false`
223/// 2. If all entries are referenced, clears all reference bits then evicts first entry
224///
225/// # Implementation
226///
227/// Uses HashMap + Vec for O(1) access with swap-remove for eviction.
228pub struct NruCache<K, V> {
229 /// HashMap for O(1) key lookup
230 map: FxHashMap<K, Entry<V>>,
231 /// Dense array of keys for eviction scanning
232 keys: Vec<K>,
233 /// Maximum capacity
234 capacity: usize,
235 #[cfg(feature = "metrics")]
236 metrics: NruMetrics,
237}
238
239impl<K, V> NruCache<K, V>
240where
241 K: Clone + Eq + Hash,
242{
243 /// Creates a new NRU cache with the specified capacity.
244 ///
245 /// # Example
246 ///
247 /// ```
248 /// use cachekit::policy::nru::NruCache;
249 /// use cachekit::traits::Cache;
250 ///
251 /// let cache: NruCache<String, i32> = NruCache::new(100);
252 /// assert_eq!(cache.capacity(), 100);
253 /// assert!(cache.is_empty());
254 /// ```
255 #[inline]
256 pub fn new(capacity: usize) -> Self {
257 Self {
258 map: FxHashMap::default(),
259 keys: Vec::with_capacity(capacity),
260 capacity,
261 #[cfg(feature = "metrics")]
262 metrics: NruMetrics::default(),
263 }
264 }
265
266 /// Returns `true` if the cache is empty.
267 ///
268 /// # Example
269 ///
270 /// ```
271 /// use cachekit::policy::nru::NruCache;
272 /// use cachekit::traits::Cache;
273 ///
274 /// let mut cache = NruCache::<&str, i32>::new(10);
275 /// assert!(cache.is_empty());
276 ///
277 /// cache.insert("a", 1);
278 /// assert!(!cache.is_empty());
279 /// ```
280 #[inline]
281 pub fn is_empty(&self) -> bool {
282 self.map.is_empty()
283 }
284
285 /// Returns an iterator over all key-value pairs in the cache.
286 ///
287 /// Iteration order is unspecified.
288 ///
289 /// # Example
290 ///
291 /// ```
292 /// use cachekit::policy::nru::NruCache;
293 /// use cachekit::traits::Cache;
294 ///
295 /// let mut cache = NruCache::new(10);
296 /// cache.insert("a", 1);
297 /// cache.insert("b", 2);
298 ///
299 /// let pairs: Vec<_> = cache.iter().collect();
300 /// assert_eq!(pairs.len(), 2);
301 /// ```
302 #[inline]
303 pub fn iter(&self) -> Iter<'_, K, V> {
304 Iter {
305 inner: self.map.iter(),
306 }
307 }
308
309 /// Returns a mutable iterator over all key-value pairs in the cache.
310 ///
311 /// Only values are mutable; keys and eviction metadata are not exposed.
312 /// Iteration order is unspecified.
313 ///
314 /// # Example
315 ///
316 /// ```
317 /// use cachekit::policy::nru::NruCache;
318 /// use cachekit::traits::Cache;
319 ///
320 /// let mut cache = NruCache::new(10);
321 /// cache.insert("a", 1);
322 /// cache.insert("b", 2);
323 ///
324 /// for (_key, value) in cache.iter_mut() {
325 /// *value += 10;
326 /// }
327 /// ```
328 #[inline]
329 pub fn iter_mut(&mut self) -> IterMut<'_, K, V> {
330 IterMut {
331 inner: self.map.iter_mut(),
332 }
333 }
334
335 /// Evicts an entry using NRU policy.
336 ///
337 /// First tries to find an unreferenced entry. If all entries are referenced,
338 /// clears all reference bits and evicts the first entry.
339 ///
340 /// Returns the evicted (key, value) pair.
341 fn evict_one(&mut self) -> Option<(K, V)> {
342 if self.keys.is_empty() {
343 return None;
344 }
345
346 // Phase 1: Try to find an unreferenced entry
347 for (idx, key) in self.keys.iter().enumerate() {
348 #[cfg(feature = "metrics")]
349 {
350 self.metrics.sweep_steps += 1;
351 }
352 if let Some(entry) = self.map.get(key) {
353 if !entry.referenced {
354 let victim_key = self.keys.swap_remove(idx);
355
356 if idx < self.keys.len() {
357 let swapped_key = &self.keys[idx];
358 if let Some(swapped_entry) = self.map.get_mut(swapped_key) {
359 swapped_entry.index = idx;
360 }
361 }
362
363 let victim_entry = self.map.remove(&victim_key)?;
364 return Some((victim_key, victim_entry.value));
365 }
366 }
367 }
368
369 // Phase 2: All entries are referenced - clear all bits and evict first
370 for key in &self.keys {
371 if let Some(entry) = self.map.get_mut(key) {
372 if entry.referenced {
373 entry.referenced = false;
374 #[cfg(feature = "metrics")]
375 {
376 self.metrics.ref_bit_resets += 1;
377 }
378 }
379 }
380 }
381
382 if !self.keys.is_empty() {
383 let victim_key = self.keys.swap_remove(0);
384
385 if !self.keys.is_empty() {
386 let swapped_key = &self.keys[0];
387 if let Some(swapped_entry) = self.map.get_mut(swapped_key) {
388 swapped_entry.index = 0;
389 }
390 }
391
392 let victim_entry = self.map.remove(&victim_key)?;
393 return Some((victim_key, victim_entry.value));
394 }
395
396 None
397 }
398}
399
400impl<K, V> Cache<K, V> for NruCache<K, V>
401where
402 K: Clone + Eq + Hash,
403{
404 #[inline]
405 fn contains(&self, key: &K) -> bool {
406 self.map.contains_key(key)
407 }
408
409 #[inline]
410 fn len(&self) -> usize {
411 self.map.len()
412 }
413
414 #[inline]
415 fn capacity(&self) -> usize {
416 self.capacity
417 }
418
419 #[inline]
420 fn peek(&self, key: &K) -> Option<&V> {
421 self.map.get(key).map(|e| &e.value)
422 }
423
424 #[inline]
425 fn get(&mut self, key: &K) -> Option<&V> {
426 if let Some(entry) = self.map.get_mut(key) {
427 entry.referenced = true;
428 #[cfg(feature = "metrics")]
429 {
430 self.metrics.get_calls += 1;
431 self.metrics.get_hits += 1;
432 }
433 Some(&entry.value)
434 } else {
435 #[cfg(feature = "metrics")]
436 {
437 self.metrics.get_calls += 1;
438 self.metrics.get_misses += 1;
439 }
440 None
441 }
442 }
443
444 #[inline]
445 fn insert(&mut self, key: K, value: V) -> Option<V> {
446 #[cfg(feature = "metrics")]
447 {
448 self.metrics.insert_calls += 1;
449 }
450
451 if self.capacity == 0 {
452 return None;
453 }
454 if let Some(entry) = self.map.get_mut(&key) {
455 #[cfg(feature = "metrics")]
456 {
457 self.metrics.insert_updates += 1;
458 }
459 let old_value = std::mem::replace(&mut entry.value, value);
460 entry.referenced = true;
461 return Some(old_value);
462 }
463
464 #[cfg(feature = "metrics")]
465 {
466 self.metrics.insert_new += 1;
467 }
468
469 if self.map.len() >= self.capacity {
470 #[cfg(feature = "metrics")]
471 {
472 self.metrics.evict_calls += 1;
473 }
474 if self.evict_one().is_some() {
475 #[cfg(feature = "metrics")]
476 {
477 self.metrics.evicted_entries += 1;
478 }
479 }
480 }
481
482 let index = self.keys.len();
483 self.keys.push(key.clone());
484 self.map.insert(
485 key,
486 Entry {
487 index,
488 value,
489 referenced: false,
490 },
491 );
492
493 None
494 }
495
496 #[inline]
497 fn remove(&mut self, key: &K) -> Option<V> {
498 let entry = self.map.remove(key)?;
499 let idx = entry.index;
500
501 self.keys.swap_remove(idx);
502
503 if idx < self.keys.len() {
504 let swapped_key = &self.keys[idx];
505 if let Some(swapped_entry) = self.map.get_mut(swapped_key) {
506 swapped_entry.index = idx;
507 }
508 }
509
510 Some(entry.value)
511 }
512
513 fn clear(&mut self) {
514 self.map.clear();
515 self.keys.clear();
516 #[cfg(feature = "metrics")]
517 {
518 use crate::metrics::traits::CoreMetricsRecorder;
519 self.metrics.record_clear();
520 }
521 }
522}
523
524#[cfg(feature = "metrics")]
525impl<K, V> NruCache<K, V>
526where
527 K: Clone + Eq + Hash,
528{
529 /// Returns a snapshot of cache metrics.
530 ///
531 /// # Example
532 ///
533 /// ```
534 /// use cachekit::policy::nru::NruCache;
535 /// use cachekit::traits::Cache;
536 ///
537 /// let mut cache = NruCache::new(10);
538 /// cache.insert("a", 1);
539 /// let _ = cache.get(&"a");
540 ///
541 /// let snap = cache.metrics_snapshot();
542 /// assert_eq!(snap.get_hits, 1);
543 /// ```
544 pub fn metrics_snapshot(&self) -> NruMetricsSnapshot {
545 NruMetricsSnapshot {
546 get_calls: self.metrics.get_calls,
547 get_hits: self.metrics.get_hits,
548 get_misses: self.metrics.get_misses,
549 insert_calls: self.metrics.insert_calls,
550 insert_updates: self.metrics.insert_updates,
551 insert_new: self.metrics.insert_new,
552 evict_calls: self.metrics.evict_calls,
553 evicted_entries: self.metrics.evicted_entries,
554 sweep_steps: self.metrics.sweep_steps,
555 ref_bit_resets: self.metrics.ref_bit_resets,
556 cache_len: self.map.len(),
557 capacity: self.capacity,
558 }
559 }
560}
561
562#[cfg(feature = "metrics")]
563impl<K, V> MetricsSnapshotProvider<NruMetricsSnapshot> for NruCache<K, V>
564where
565 K: Clone + Eq + Hash,
566{
567 fn snapshot(&self) -> NruMetricsSnapshot {
568 self.metrics_snapshot()
569 }
570}
571
572impl<K, V> Debug for NruCache<K, V>
573where
574 K: Clone + Eq + Hash + std::fmt::Debug,
575 V: std::fmt::Debug,
576{
577 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
578 f.debug_struct("NruCache")
579 .field("capacity", &self.capacity)
580 .field("len", &self.map.len())
581 .field("keys", &self.keys)
582 .finish()
583 }
584}
585
586impl<K, V> Clone for NruCache<K, V>
587where
588 K: Clone + Eq + Hash,
589 V: Clone,
590{
591 fn clone(&self) -> Self {
592 Self {
593 map: self.map.clone(),
594 keys: self.keys.clone(),
595 capacity: self.capacity,
596 #[cfg(feature = "metrics")]
597 metrics: self.metrics,
598 }
599 }
600}
601
602// ---------------------------------------------------------------------------
603// Iterators
604// ---------------------------------------------------------------------------
605
606/// Iterator over shared references to key-value pairs in an [`NruCache`].
607///
608/// Created by [`NruCache::iter`].
609pub struct Iter<'a, K, V> {
610 inner: std::collections::hash_map::Iter<'a, K, Entry<V>>,
611}
612
613impl<'a, K, V> Iterator for Iter<'a, K, V> {
614 type Item = (&'a K, &'a V);
615
616 #[inline]
617 fn next(&mut self) -> Option<Self::Item> {
618 self.inner.next().map(|(k, e)| (k, &e.value))
619 }
620
621 #[inline]
622 fn size_hint(&self) -> (usize, Option<usize>) {
623 self.inner.size_hint()
624 }
625}
626
627impl<K, V> ExactSizeIterator for Iter<'_, K, V> {}
628
629impl<K, V> std::iter::FusedIterator for Iter<'_, K, V> {}
630
631/// Iterator over mutable references to values (with shared key references)
632/// in an [`NruCache`].
633///
634/// Created by [`NruCache::iter_mut`].
635pub struct IterMut<'a, K, V> {
636 inner: std::collections::hash_map::IterMut<'a, K, Entry<V>>,
637}
638
639impl<'a, K, V> Iterator for IterMut<'a, K, V> {
640 type Item = (&'a K, &'a mut V);
641
642 #[inline]
643 fn next(&mut self) -> Option<Self::Item> {
644 self.inner.next().map(|(k, e)| (k, &mut e.value))
645 }
646
647 #[inline]
648 fn size_hint(&self) -> (usize, Option<usize>) {
649 self.inner.size_hint()
650 }
651}
652
653impl<K, V> ExactSizeIterator for IterMut<'_, K, V> {}
654
655impl<K, V> std::iter::FusedIterator for IterMut<'_, K, V> {}
656
657/// Owning iterator over key-value pairs from an [`NruCache`].
658///
659/// Created by the `IntoIterator` implementation on `NruCache`.
660pub struct IntoIter<K, V> {
661 inner: std::collections::hash_map::IntoIter<K, Entry<V>>,
662}
663
664impl<K, V> Iterator for IntoIter<K, V> {
665 type Item = (K, V);
666
667 #[inline]
668 fn next(&mut self) -> Option<Self::Item> {
669 self.inner.next().map(|(k, e)| (k, e.value))
670 }
671
672 #[inline]
673 fn size_hint(&self) -> (usize, Option<usize>) {
674 self.inner.size_hint()
675 }
676}
677
678impl<K, V> ExactSizeIterator for IntoIter<K, V> {}
679
680impl<K, V> std::iter::FusedIterator for IntoIter<K, V> {}
681
682// ---------------------------------------------------------------------------
683// IntoIterator
684// ---------------------------------------------------------------------------
685
686impl<K, V> IntoIterator for NruCache<K, V>
687where
688 K: Clone + Eq + Hash,
689{
690 type Item = (K, V);
691 type IntoIter = IntoIter<K, V>;
692
693 fn into_iter(self) -> Self::IntoIter {
694 IntoIter {
695 inner: self.map.into_iter(),
696 }
697 }
698}
699
700impl<'a, K, V> IntoIterator for &'a NruCache<K, V>
701where
702 K: Clone + Eq + Hash,
703{
704 type Item = (&'a K, &'a V);
705 type IntoIter = Iter<'a, K, V>;
706
707 fn into_iter(self) -> Self::IntoIter {
708 self.iter()
709 }
710}
711
712impl<'a, K, V> IntoIterator for &'a mut NruCache<K, V>
713where
714 K: Clone + Eq + Hash,
715{
716 type Item = (&'a K, &'a mut V);
717 type IntoIter = IterMut<'a, K, V>;
718
719 fn into_iter(self) -> Self::IntoIter {
720 self.iter_mut()
721 }
722}
723
724// ---------------------------------------------------------------------------
725// Extend
726// ---------------------------------------------------------------------------
727
728impl<K, V> Extend<(K, V)> for NruCache<K, V>
729where
730 K: Clone + Eq + Hash,
731{
732 fn extend<I: IntoIterator<Item = (K, V)>>(&mut self, iter: I) {
733 for (k, v) in iter {
734 self.insert(k, v);
735 }
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use crate::traits::Cache;
743
744 #[allow(dead_code)]
745 const _: () = {
746 fn assert_send<T: Send>() {}
747 fn assert_sync<T: Sync>() {}
748 fn check() {
749 assert_send::<NruCache<String, i32>>();
750 assert_sync::<NruCache<String, i32>>();
751 }
752 };
753
754 #[test]
755 fn test_new() {
756 let cache: NruCache<i32, i32> = NruCache::new(10);
757 assert_eq!(cache.capacity(), 10);
758 assert_eq!(cache.len(), 0);
759 assert!(cache.is_empty());
760 }
761
762 #[test]
763 fn test_insert_and_get() {
764 let mut cache = NruCache::new(3);
765
766 cache.insert(1, 100);
767 cache.insert(2, 200);
768 cache.insert(3, 300);
769
770 assert_eq!(cache.get(&1), Some(&100));
771 assert_eq!(cache.get(&2), Some(&200));
772 assert_eq!(cache.get(&3), Some(&300));
773 assert_eq!(cache.len(), 3);
774 }
775
776 #[test]
777 fn test_update_existing() {
778 let mut cache = NruCache::new(3);
779
780 cache.insert(1, 100);
781 let old = cache.insert(1, 999);
782
783 assert_eq!(old, Some(100));
784 assert_eq!(cache.get(&1), Some(&999));
785 assert_eq!(cache.len(), 1);
786 }
787
788 #[test]
789 fn test_eviction_unreferenced() {
790 let mut cache = NruCache::new(3);
791
792 // Insert 3 items
793 cache.insert(1, 100);
794 cache.insert(2, 200);
795 cache.insert(3, 300);
796
797 // Access only 1 and 3
798 let _ = cache.get(&1);
799 let _ = cache.get(&3);
800
801 // Insert 4th item - should evict 2 (unreferenced)
802 cache.insert(4, 400);
803
804 assert_eq!(cache.len(), 3);
805 assert!(cache.contains(&1));
806 assert!(!cache.contains(&2)); // 2 was evicted
807 assert!(cache.contains(&3));
808 assert!(cache.contains(&4));
809 }
810
811 #[test]
812 fn test_eviction_all_referenced() {
813 let mut cache = NruCache::new(3);
814
815 // Insert and access all 3 items
816 cache.insert(1, 100);
817 cache.insert(2, 200);
818 cache.insert(3, 300);
819 let _ = cache.get(&1);
820 let _ = cache.get(&2);
821 let _ = cache.get(&3);
822
823 // Insert 4th item - all are referenced, so clears bits and evicts one
824 cache.insert(4, 400);
825
826 assert_eq!(cache.len(), 3);
827 assert!(cache.contains(&4));
828 }
829
830 #[test]
831 fn test_remove() {
832 let mut cache = NruCache::new(3);
833
834 cache.insert(1, 100);
835 cache.insert(2, 200);
836 cache.insert(3, 300);
837
838 let removed = cache.remove(&2);
839 assert_eq!(removed, Some(200));
840 assert_eq!(cache.len(), 2);
841 assert!(!cache.contains(&2));
842 assert!(cache.contains(&1));
843 assert!(cache.contains(&3));
844 }
845
846 #[test]
847 fn test_clear() {
848 let mut cache = NruCache::new(3);
849
850 cache.insert(1, 100);
851 cache.insert(2, 200);
852 cache.insert(3, 300);
853
854 cache.clear();
855
856 assert_eq!(cache.len(), 0);
857 assert!(cache.is_empty());
858 assert!(!cache.contains(&1));
859 }
860
861 #[test]
862 fn test_contains_does_not_set_reference() {
863 let mut cache = NruCache::new(2);
864
865 cache.insert(1, 100);
866 cache.insert(2, 200);
867
868 // contains() doesn't set reference bit
869 assert!(cache.contains(&1));
870
871 // Manually clear reference bit by checking internals
872 // (In real usage, this would happen during eviction phase)
873 if let Some(entry) = cache.map.get_mut(&1) {
874 entry.referenced = false;
875 }
876
877 // Now 1 should be evictable
878 cache.insert(3, 300);
879
880 assert!(!cache.contains(&1)); // 1 was evicted
881 assert!(cache.contains(&2));
882 assert!(cache.contains(&3));
883 }
884
885 #[test]
886 fn test_zero_capacity() {
887 let mut cache = NruCache::new(0);
888 assert_eq!(cache.capacity(), 0);
889
890 cache.insert(1, 100);
891 assert!(!cache.contains(&1));
892 }
893}