cache_rs/lru.rs
1//! Least Recently Used (LRU) Cache Implementation
2//!
3//! This module provides a memory-efficient LRU cache implementation with O(1) operations
4//! for all common cache operations. LRU is one of the most widely used cache eviction
5//! algorithms due to its simplicity and good performance for workloads with temporal locality.
6//!
7//! # Algorithm
8//!
9//! The LRU cache maintains items in order of recency of use, evicting the least recently
10//! used item when capacity is reached. This works on the principle of temporal locality:
11//! items that have been accessed recently are likely to be accessed again soon.
12//!
13//! # Performance Characteristics
14//!
15//! - **Time Complexity**:
16//! - Get: O(1)
17//! - Put: O(1)
18//! - Remove: O(1)
19//!
20//! - **Space Complexity**:
21//! - O(n) where n is the capacity of the cache
22//! - Memory overhead is approximately 48 bytes per entry plus the size of keys and values
23//!
24//! # When to Use
25//!
26//! LRU caches are ideal for:
27//! - General-purpose caching where access patterns exhibit temporal locality
28//! - Simple implementation with predictable performance
29//! - Caching with a fixed memory budget
30//!
31//! They are less suitable for:
32//! - Workloads where frequency of access is more important than recency
33//! - Scanning patterns where a large set of items is accessed once in sequence
34//!
35//! # Thread Safety
36//!
37//! This implementation is not thread-safe. For concurrent access, you should
38//! wrap the cache with a synchronization primitive such as `Mutex` or `RwLock`.
39
40extern crate alloc;
41
42use crate::config::LruCacheConfig;
43use crate::list::{Entry, List};
44use crate::metrics::{CacheMetrics, LruCacheMetrics};
45use alloc::boxed::Box;
46use alloc::collections::BTreeMap;
47use alloc::string::String;
48use core::borrow::Borrow;
49use core::hash::{BuildHasher, Hash};
50use core::mem;
51use core::num::NonZeroUsize;
52
53#[cfg(feature = "hashbrown")]
54use hashbrown::hash_map::DefaultHashBuilder;
55#[cfg(feature = "hashbrown")]
56use hashbrown::HashMap;
57
58#[cfg(not(feature = "hashbrown"))]
59use std::collections::hash_map::RandomState as DefaultHashBuilder;
60#[cfg(not(feature = "hashbrown"))]
61use std::collections::HashMap;
62
63/// An implementation of a Least Recently Used (LRU) cache.
64///
65/// The cache has a fixed capacity and supports O(1) operations for
66/// inserting, retrieving, and updating entries. When the cache reaches capacity,
67/// the least recently used entry will be evicted to make room for new entries.
68///
69/// # Examples
70///
71/// ```
72/// use cache_rs::LruCache;
73/// use core::num::NonZeroUsize;
74///
75/// let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
76///
77/// // Add items to the cache
78/// cache.put("apple", 1);
79/// cache.put("banana", 2);
80///
81/// // Accessing items updates their recency
82/// assert_eq!(cache.get(&"apple"), Some(&1));
83///
84/// // Adding beyond capacity evicts the least recently used item
85/// // In this case "banana" is evicted because "apple" was just accessed
86/// cache.put("cherry", 3);
87/// assert_eq!(cache.get(&"banana"), None);
88/// assert_eq!(cache.get(&"apple"), Some(&1));
89/// assert_eq!(cache.get(&"cherry"), Some(&3));
90/// ```
91///
92/// # Memory Usage
93///
94/// The memory usage of this cache is approximately:
95/// - 16 bytes for the cache struct itself
96/// - (32 + size_of(K) + size_of(V)) bytes per entry
97/// - Plus HashMap overhead
98///
99/// # Examples
100///
101/// ```
102/// use cache_rs::lru::LruCache;
103/// use core::num::NonZeroUsize;
104///
105/// let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
106///
107/// // Insert some items
108/// cache.put("apple", 1);
109/// cache.put("banana", 2);
110///
111/// // Most recently used is banana, then apple
112/// assert_eq!(cache.get(&"apple"), Some(&1));
113/// // Now most recently used is apple, then banana
114///
115/// // Add a new item, evicting the least recently used (banana)
116/// cache.put("cherry", 3);
117///
118/// // Banana was evicted
119/// assert_eq!(cache.get(&"banana"), None);
120/// assert_eq!(cache.get(&"apple"), Some(&1));
121/// assert_eq!(cache.get(&"cherry"), Some(&3));
122/// ```
123#[derive(Debug)]
124pub struct LruCache<K, V, S = DefaultHashBuilder> {
125 /// Configuration for the LRU cache
126 config: LruCacheConfig,
127
128 /// The internal list holding the key-value pairs in LRU order.
129 list: List<(K, V)>,
130
131 /// A hash map mapping keys to pointers to the list entries.
132 map: HashMap<K, *mut Entry<(K, V)>, S>,
133
134 /// Metrics tracking for this cache instance.
135 metrics: LruCacheMetrics,
136}
137
138impl<K: Hash + Eq, V: Clone, S: BuildHasher> LruCache<K, V, S> {
139 /// Creates a new LRU cache that holds at most `cap` items using the specified hash builder.
140 ///
141 /// # Examples
142 ///
143 /// ```
144 /// use cache_rs::lru::LruCache;
145 /// use core::num::NonZeroUsize;
146 /// use std::collections::hash_map::RandomState;
147 ///
148 /// let cache: LruCache<&str, u32, _> = LruCache::with_hasher(
149 /// NonZeroUsize::new(10).unwrap(),
150 /// RandomState::new()
151 /// );
152 /// ```
153 pub fn with_hasher(cap: NonZeroUsize, hash_builder: S) -> Self {
154 // Default to a reasonable estimate: 1KB average object size
155 Self::with_hasher_and_size(cap, hash_builder, cap.get() as u64 * 1024)
156 }
157
158 /// Creates a new LRU cache with specified capacity, hash builder, and size limit in bytes.
159 pub fn with_hasher_and_size(cap: NonZeroUsize, hash_builder: S, max_size_bytes: u64) -> Self {
160 let map_capacity = cap.get().next_power_of_two();
161 let config = LruCacheConfig::new(cap);
162 LruCache {
163 config,
164 list: List::new(cap),
165 map: HashMap::with_capacity_and_hasher(map_capacity, hash_builder),
166 metrics: LruCacheMetrics::new(max_size_bytes),
167 }
168 }
169
170 /// Returns the maximum number of key-value pairs the cache can hold.
171 ///
172 /// # Examples
173 ///
174 /// ```
175 /// use cache_rs::lru::LruCache;
176 /// use core::num::NonZeroUsize;
177 ///
178 /// let cache: LruCache<&str, u32> = LruCache::new(NonZeroUsize::new(10).unwrap());
179 /// assert_eq!(cache.cap().get(), 10);
180 /// ```
181 pub fn cap(&self) -> NonZeroUsize {
182 self.config.capacity()
183 }
184
185 /// Returns the number of key-value pairs in the cache.
186 ///
187 /// # Examples
188 ///
189 /// ```
190 /// use cache_rs::lru::LruCache;
191 /// use core::num::NonZeroUsize;
192 ///
193 /// let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
194 /// assert_eq!(cache.len(), 0);
195 ///
196 /// cache.put("apple", 1);
197 /// assert_eq!(cache.len(), 1);
198 ///
199 /// cache.put("banana", 2);
200 /// assert_eq!(cache.len(), 2);
201 /// ```
202 pub fn len(&self) -> usize {
203 self.map.len()
204 }
205
206 /// Returns `true` if the cache contains no key-value pairs.
207 ///
208 /// # Examples
209 ///
210 /// ```
211 /// use cache_rs::lru::LruCache;
212 /// use core::num::NonZeroUsize;
213 ///
214 /// let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
215 /// assert!(cache.is_empty());
216 ///
217 /// cache.put("apple", 1);
218 /// assert!(!cache.is_empty());
219 /// ```
220 pub fn is_empty(&self) -> bool {
221 self.map.is_empty()
222 }
223
224 /// Estimates the size of a key-value pair in bytes for metrics tracking
225 fn estimate_object_size(&self, _key: &K, _value: &V) -> u64 {
226 // Simple estimation: key size + value size + overhead for pointers and metadata
227 mem::size_of::<K>() as u64 + mem::size_of::<V>() as u64 + 64
228 }
229
230 /// Returns a reference to the value corresponding to the key.
231 ///
232 /// The key may be any borrowed form of the cache's key type, but
233 /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
234 /// the key type.
235 ///
236 /// If a value is returned, that key-value pair becomes the most recently used pair in the cache.
237 ///
238 /// # Examples
239 ///
240 /// ```
241 /// use cache_rs::lru::LruCache;
242 /// use core::num::NonZeroUsize;
243 ///
244 /// let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
245 /// cache.put("apple", 1);
246 /// cache.put("banana", 2);
247 ///
248 /// assert_eq!(cache.get(&"apple"), Some(&1));
249 /// assert_eq!(cache.get(&"banana"), Some(&2));
250 /// assert_eq!(cache.get(&"cherry"), None);
251 /// ```
252 pub fn get<Q>(&mut self, key: &Q) -> Option<&V>
253 where
254 K: Borrow<Q>,
255 Q: ?Sized + Hash + Eq,
256 {
257 if let Some(node) = self.map.get(key).copied() {
258 // Cache hit
259 unsafe {
260 // SAFETY: node comes from our map, so it's a valid pointer to an entry in our list
261 self.list.move_to_front(node);
262 let (k, v) = (*node).get_value();
263
264 // Record hit with estimated object size
265 let object_size = self.estimate_object_size(k, v);
266 self.metrics.core.record_hit(object_size);
267
268 Some(v)
269 }
270 } else {
271 // Cache miss - we can't estimate size without the actual object
272 // The simulation system will need to call record_miss separately
273 None
274 }
275 }
276
277 /// Records a cache miss for metrics tracking (to be called by simulation system)
278 pub fn record_miss(&mut self, object_size: u64) {
279 self.metrics.core.record_miss(object_size);
280 }
281
282 /// Returns a mutable reference to the value corresponding to the key.
283 ///
284 /// The key may be any borrowed form of the cache's key type, but
285 /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
286 /// the key type.
287 ///
288 /// If a value is returned, that key-value pair becomes the most recently used pair in the cache.
289 ///
290 /// # Examples
291 ///
292 /// ```
293 /// use cache_rs::lru::LruCache;
294 /// use core::num::NonZeroUsize;
295 ///
296 /// let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
297 /// cache.put("apple", 1);
298 ///
299 /// if let Some(v) = cache.get_mut(&"apple") {
300 /// *v = 4;
301 /// }
302 ///
303 /// assert_eq!(cache.get(&"apple"), Some(&4));
304 /// ```
305 pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
306 where
307 K: Borrow<Q>,
308 Q: ?Sized + Hash + Eq,
309 {
310 let node = self.map.get(key).copied()?;
311
312 // Move the node to the front of the list to mark it as most recently used
313 // SAFETY: node comes from our map, so it's a valid pointer to an entry in our list
314 unsafe {
315 self.list.move_to_front(node);
316 let (k, v) = (*node).get_value_mut();
317
318 // Record hit with estimated object size
319 let object_size = self.estimate_object_size(k, v);
320 self.metrics.core.record_hit(object_size);
321
322 Some(v)
323 }
324 }
325}
326
327impl<K: Hash + Eq + Clone, V, S: BuildHasher> LruCache<K, V, S> {
328 /// Inserts a key-value pair into the cache.
329 ///
330 /// If the cache already contained this key, the old value is replaced and returned.
331 /// Otherwise, if the cache is at capacity, the least recently used
332 /// key-value pair will be evicted and returned.
333 ///
334 /// The inserted key-value pair becomes the most recently used pair in the cache.
335 ///
336 /// # Examples
337 ///
338 /// ```
339 /// use cache_rs::lru::LruCache;
340 /// use core::num::NonZeroUsize;
341 ///
342 /// let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
343 ///
344 /// assert_eq!(cache.put("apple", 1), None); // No previous value
345 /// assert_eq!(cache.put("apple", 4).unwrap().1, 1); // Replaced existing value
346 ///
347 /// // At capacity, adding new item evicts least recently used
348 /// cache.put("banana", 2);
349 /// assert_eq!(cache.put("cherry", 3).unwrap().1, 4); // Evicted "apple"
350 /// assert_eq!(cache.get(&"apple"), None); // "apple" was evicted
351 /// ```
352 pub fn put(&mut self, key: K, value: V) -> Option<(K, V)>
353 where
354 V: Clone,
355 {
356 let mut evicted = None;
357 let object_size = self.estimate_object_size(&key, &value);
358
359 // If key is already in the cache
360 if let Some(&node) = self.map.get(&key) {
361 // SAFETY: node comes from our map, so it's a valid pointer to an entry in our list
362 unsafe {
363 self.list.move_to_front(node);
364 let (k, old_value) = self.list.update(node, (key, value), true).0?;
365 // This was an update, not a new insertion, so just track the size change
366 return Some((k, old_value));
367 }
368 }
369
370 // If we're at capacity, remove the least recently used item from the cache
371 if self.map.len() >= self.cap().get() {
372 if let Some(old_entry) = self.list.remove_last() {
373 // Extract the key and remove it from the map
374 unsafe {
375 // SAFETY: old_entry comes from list.remove_last(), so it's a valid Box
376 // that we own. Converting to raw pointer is safe for temporary access.
377 let entry_ptr = Box::into_raw(old_entry);
378 let key_ref = &(*entry_ptr).get_value().0;
379 self.map.remove(key_ref);
380 let key = (*entry_ptr).get_value().0.clone();
381 let value = (*entry_ptr).get_value().1.clone();
382
383 // Record eviction
384 let evicted_size = self.estimate_object_size(&key, &value);
385 self.metrics.core.record_eviction(evicted_size);
386
387 evicted = Some((key, value));
388 // SAFETY: Reconstructing Box from the same raw pointer to properly free memory
389 let _ = Box::from_raw(entry_ptr);
390 }
391 }
392 }
393
394 // Insert the new key-value pair at the front of the list
395 if let Some(node) = self.list.add((key.clone(), value)) {
396 // SAFETY: node comes from our list.add() call, so it's a valid pointer
397 self.map.insert(key, node);
398
399 // Record the insertion (increase in stored data)
400 self.metrics.core.record_insertion(object_size);
401 }
402
403 evicted
404 }
405
406 /// Removes a key from the cache, returning the value at the key if the key was previously in the cache.
407 ///
408 /// The key may be any borrowed form of the cache's key type, but
409 /// [`Hash`] and [`Eq`] on the borrowed form *must* match those for
410 /// the key type.
411 ///
412 /// # Examples
413 ///
414 /// ```
415 /// use cache_rs::lru::LruCache;
416 /// use core::num::NonZeroUsize;
417 ///
418 /// let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
419 /// cache.put("apple", 1);
420 ///
421 /// assert_eq!(cache.remove(&"apple"), Some(1));
422 /// assert_eq!(cache.remove(&"apple"), None);
423 /// ```
424 pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
425 where
426 K: Borrow<Q>,
427 Q: ?Sized + Hash + Eq,
428 V: Clone,
429 {
430 let node = self.map.remove(key)?;
431
432 unsafe {
433 // SAFETY: node comes from our map and was just removed, so it's a valid pointer to an entry in our list
434 // Get the key-value pair before removing
435 let (k, v) = (*node).get_value();
436 let object_size = self.estimate_object_size(k, v);
437 let value = v.clone();
438
439 // Detach the node from the list
440 self.list.remove(node);
441
442 // Record the removal (decrease in stored data)
443 self.metrics.core.record_eviction(object_size);
444
445 // Return the value (second element) from the tuple
446 Some(value)
447 }
448 }
449
450 /// Clears the cache, removing all key-value pairs.
451 ///
452 /// # Examples
453 ///
454 /// ```
455 /// use cache_rs::lru::LruCache;
456 /// use core::num::NonZeroUsize;
457 ///
458 /// let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
459 /// cache.put("apple", 1);
460 /// cache.put("banana", 2);
461 ///
462 /// assert!(!cache.is_empty());
463 /// cache.clear();
464 /// assert!(cache.is_empty());
465 /// ```
466 pub fn clear(&mut self) {
467 // Reset cache size to zero
468 self.metrics.core.cache_size_bytes = 0;
469
470 self.map.clear();
471 self.list.clear();
472 }
473
474 /// Returns an iterator over the cache's key-value pairs in least-recently-used to most-recently-used order.
475 ///
476 /// # Examples
477 ///
478 /// ```ignore
479 /// use cache_rs::lru::LruCache;
480 /// use core::num::NonZeroUsize;
481 /// use std::prelude::v1::*;
482 ///
483 /// let mut cache = LruCache::new(NonZeroUsize::new(3).unwrap());
484 /// cache.put("a", 1);
485 /// cache.put("b", 2);
486 /// cache.put("c", 3);
487 ///
488 /// // Access "a" to move it to the front
489 /// assert_eq!(cache.get(&"a"), Some(&1));
490 ///
491 /// let items: Vec<_> = cache.iter().collect();
492 /// assert_eq!(items, [(&"b", &2), (&"c", &3), (&"a", &1)]);
493 /// ```
494 pub fn iter(&self) -> Iter<'_, K, V> {
495 // Not implemented here - this requires a more complex implementation to traverse
496 // the linked list in reverse order
497 unimplemented!("Iteration not yet implemented")
498 }
499
500 /// Returns a mutable iterator over the cache's key-value pairs in least-recently-used to most-recently-used order.
501 ///
502 /// # Examples
503 ///
504 /// ```ignore
505 /// use cache_rs::lru::LruCache;
506 /// use core::num::NonZeroUsize;
507 /// use std::prelude::v1::*;
508 ///
509 /// let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
510 /// cache.put("a", 1);
511 /// cache.put("b", 2);
512 ///
513 /// for (_, v) in cache.iter_mut() {
514 /// *v += 10;
515 /// }
516 ///
517 /// assert_eq!(cache.get(&"a"), Some(&11));
518 /// assert_eq!(cache.get(&"b"), Some(&12));
519 /// ```
520 pub fn iter_mut(&mut self) -> IterMut<'_, K, V> {
521 // Not implemented here - this requires a more complex implementation to traverse
522 // the linked list in reverse order
523 unimplemented!("Mutable iteration not yet implemented")
524 }
525}
526
527impl<K: Hash + Eq, V> LruCache<K, V>
528where
529 V: Clone,
530{
531 /// Creates a new LRU cache that holds at most `cap` items.
532 ///
533 /// # Examples
534 ///
535 /// ```
536 /// use cache_rs::lru::LruCache;
537 /// use core::num::NonZeroUsize;
538 ///
539 /// let cache: LruCache<&str, u32> = LruCache::new(NonZeroUsize::new(10).unwrap());
540 /// ```
541 pub fn new(cap: NonZeroUsize) -> LruCache<K, V, DefaultHashBuilder> {
542 LruCache::with_hasher(cap, DefaultHashBuilder::default())
543 }
544}
545
546impl<K: Hash + Eq, V, S: BuildHasher> CacheMetrics for LruCache<K, V, S> {
547 fn metrics(&self) -> BTreeMap<String, f64> {
548 self.metrics.metrics()
549 }
550
551 fn algorithm_name(&self) -> &'static str {
552 self.metrics.algorithm_name()
553 }
554}
555
556/// An iterator over the entries of a `LruCache` in least-recently-used to most-recently-used order.
557///
558/// This struct is created by the [`LruCache::iter`] method.
559pub struct Iter<'a, K, V> {
560 _marker: core::marker::PhantomData<&'a (K, V)>,
561}
562
563/// A mutable iterator over the entries of a `LruCache` in least-recently-used to most-recently-used order.
564///
565/// This struct is created by the [`LruCache::iter_mut`] method.
566pub struct IterMut<'a, K, V> {
567 _marker: core::marker::PhantomData<&'a mut (K, V)>,
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573 use alloc::string::String;
574
575 #[test]
576 fn test_lru_get_put() {
577 let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
578
579 // Insert items
580 assert_eq!(cache.put("apple", 1), None);
581 assert_eq!(cache.put("banana", 2), None);
582
583 // Get existing items
584 assert_eq!(cache.get(&"apple"), Some(&1));
585 assert_eq!(cache.get(&"banana"), Some(&2));
586
587 // Get non-existent item
588 assert_eq!(cache.get(&"cherry"), None);
589
590 // Replace existing item
591 assert_eq!(cache.put("apple", 3).unwrap().1, 1);
592 assert_eq!(cache.get(&"apple"), Some(&3));
593
594 // Add a third item, should evict the least recently used (banana)
595 assert_eq!(cache.put("cherry", 4).unwrap().1, 2);
596
597 // Banana should be gone, apple and cherry should exist
598 assert_eq!(cache.get(&"banana"), None);
599 assert_eq!(cache.get(&"apple"), Some(&3));
600 assert_eq!(cache.get(&"cherry"), Some(&4));
601 }
602
603 #[test]
604 fn test_lru_get_mut() {
605 let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
606
607 cache.put("apple", 1);
608 cache.put("banana", 2);
609
610 // Modify value via get_mut
611 if let Some(v) = cache.get_mut(&"apple") {
612 *v = 3;
613 }
614
615 assert_eq!(cache.get(&"apple"), Some(&3));
616
617 // Add a third item, should evict the least recently used (banana)
618 // because apple was accessed most recently via get_mut
619 cache.put("cherry", 4);
620
621 assert_eq!(cache.get(&"banana"), None);
622 assert_eq!(cache.get(&"apple"), Some(&3));
623 assert_eq!(cache.get(&"cherry"), Some(&4));
624 }
625
626 #[test]
627 fn test_lru_remove() {
628 let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
629
630 cache.put("apple", 1);
631 cache.put("banana", 2);
632
633 // Get existing items
634 assert_eq!(cache.get(&"apple"), Some(&1));
635 assert_eq!(cache.get(&"banana"), Some(&2));
636 assert_eq!(cache.get(&"cherry"), None);
637
638 // Remove item
639 assert_eq!(cache.remove(&"apple"), Some(1));
640 assert_eq!(cache.get(&"apple"), None);
641 assert_eq!(cache.len(), 1);
642
643 // Remove non-existent item
644 assert_eq!(cache.remove(&"cherry"), None);
645
646 // We should be able to add a new item now
647 let evicted = cache.put("cherry", 3);
648 assert_eq!(evicted, None);
649 assert_eq!(cache.get(&"banana"), Some(&2));
650 assert_eq!(cache.get(&"cherry"), Some(&3));
651 }
652
653 #[test]
654 fn test_lru_clear() {
655 let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
656
657 cache.put("apple", 1);
658 cache.put("banana", 2);
659
660 assert_eq!(cache.len(), 2);
661 cache.clear();
662 assert_eq!(cache.len(), 0);
663 assert!(cache.is_empty());
664
665 // Should be able to add new items after clearing
666 cache.put("cherry", 3);
667 assert_eq!(cache.get(&"cherry"), Some(&3));
668 }
669
670 #[test]
671 fn test_lru_capacity_limits() {
672 let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
673
674 cache.put("apple", 1);
675 cache.put("banana", 2);
676 cache.put("cherry", 3);
677
678 // Should only have the 2 most recently used items
679 assert_eq!(cache.len(), 2);
680 assert_eq!(cache.get(&"apple"), None); // evicted
681 assert_eq!(cache.get(&"banana"), Some(&2));
682 assert_eq!(cache.get(&"cherry"), Some(&3));
683 }
684
685 #[test]
686 fn test_lru_string_keys() {
687 let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
688
689 let key1 = String::from("apple");
690 let key2 = String::from("banana");
691
692 cache.put(key1.clone(), 1);
693 cache.put(key2.clone(), 2);
694
695 assert_eq!(cache.get(&key1), Some(&1));
696 assert_eq!(cache.get(&key2), Some(&2));
697
698 // String slice lookup should work too
699 assert_eq!(cache.get("apple"), Some(&1));
700 assert_eq!(cache.get("banana"), Some(&2));
701 }
702
703 #[derive(Debug, Clone, Eq, PartialEq)]
704 struct ComplexValue {
705 val: i32,
706 description: String,
707 }
708
709 #[test]
710 fn test_lru_complex_values() {
711 let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
712
713 let key1 = String::from("apple");
714 let key2 = String::from("banana");
715
716 let fruit1 = ComplexValue {
717 val: 1,
718 description: String::from("First fruit"),
719 };
720 let fruit2 = ComplexValue {
721 val: 2,
722 description: String::from("Second fruit"),
723 };
724 let fruit3 = ComplexValue {
725 val: 3,
726 description: String::from("Third fruit"),
727 };
728
729 cache.put(key1.clone(), fruit1.clone());
730 cache.put(key2.clone(), fruit2.clone());
731
732 assert_eq!(cache.get(&key1).unwrap().val, fruit1.val);
733 assert_eq!(cache.get(&key2).unwrap().val, fruit2.val);
734
735 // Lets add another item
736 let evicted = cache.put(String::from("cherry"), fruit3.clone());
737 let evicted_fruit = evicted.unwrap();
738 assert_eq!(evicted_fruit.1, fruit1);
739
740 // lets remove the first item
741 let removed = cache.remove(&key1);
742 assert_eq!(removed, None);
743 }
744
745 #[test]
746 fn test_lru_metrics() {
747 use crate::metrics::CacheMetrics;
748
749 let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
750
751 // Test initial metrics
752 let metrics = cache.metrics();
753 assert_eq!(metrics.get("requests").unwrap(), &0.0);
754 assert_eq!(metrics.get("cache_hits").unwrap(), &0.0);
755 assert_eq!(metrics.get("cache_misses").unwrap(), &0.0);
756
757 // Add some items
758 cache.put("apple", 1);
759 cache.put("banana", 2);
760
761 // Test cache hits
762 cache.get(&"apple");
763 cache.get(&"banana");
764
765 let metrics = cache.metrics();
766 assert_eq!(metrics.get("cache_hits").unwrap(), &2.0);
767
768 // Test cache miss
769 cache.record_miss(64); // Simulate a miss
770 let metrics = cache.metrics();
771 assert_eq!(metrics.get("cache_misses").unwrap(), &1.0);
772 assert_eq!(metrics.get("requests").unwrap(), &3.0);
773
774 // Test eviction by adding a third item
775 cache.put("cherry", 3);
776 let metrics = cache.metrics();
777 assert_eq!(metrics.get("evictions").unwrap(), &1.0);
778
779 // Test bytes_written_to_cache tracking
780 assert!(metrics.get("bytes_written_to_cache").unwrap() > &0.0);
781
782 // Test algorithm name
783 assert_eq!(cache.algorithm_name(), "LRU");
784 }
785}