Skip to main content

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}