use std::sync::Arc;
use std::thread;
use priority_lfu::{Cache, CacheBuilder, CacheKey, CachePolicy, DeepSizeOf};
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct StringKey(String);
impl CacheKey for StringKey {
type Value = StringValue;
fn policy(&self) -> CachePolicy {
CachePolicy::Standard
}
}
#[derive(Clone, Debug, PartialEq, DeepSizeOf)]
struct StringValue {
data: String,
}
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct IntKey(u64);
impl CacheKey for IntKey {
type Value = IntValue;
fn policy(&self) -> CachePolicy {
CachePolicy::Standard
}
}
#[derive(Clone, Debug, PartialEq, DeepSizeOf)]
struct IntValue(i64);
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct VolatileKey(u64);
impl CacheKey for VolatileKey {
type Value = IntValue;
fn policy(&self) -> CachePolicy {
CachePolicy::Volatile
}
}
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct StandardKey(u64);
impl CacheKey for StandardKey {
type Value = IntValue;
fn policy(&self) -> CachePolicy {
CachePolicy::Standard
}
}
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct CriticalKey(u64);
impl CacheKey for CriticalKey {
type Value = IntValue;
fn policy(&self) -> CachePolicy {
CachePolicy::Critical
}
}
#[test]
fn test_basic_operations() {
let cache = Cache::new(10240);
let key = StringKey("test".to_string());
let value = StringValue {
data: "hello world".to_string(),
};
cache.insert(key.clone(), value.clone());
assert!(cache.contains(&key));
{
let guard = cache.get(&key).expect("key should exist");
assert_eq!(*guard, value);
}
let cloned = cache.get_clone(&key).expect("key should exist");
assert_eq!(cloned, value);
let removed = cache.remove(&key).expect("key should exist");
assert_eq!(removed, value);
assert!(!cache.contains(&key));
}
#[test]
fn test_heterogeneous_types() {
let cache = Cache::new(10240);
let str_key = StringKey("foo".to_string());
let str_val = StringValue {
data: "bar".to_string(),
};
let int_key = IntKey(42);
let int_val = IntValue(99);
cache.insert(str_key.clone(), str_val.clone());
cache.insert(int_key.clone(), int_val.clone());
assert_eq!(cache.len(), 2);
let str_retrieved = cache.get_clone(&str_key).expect("str_key should exist");
assert_eq!(str_retrieved, str_val);
let int_retrieved = cache.get_clone(&int_key).expect("int_key should exist");
assert_eq!(int_retrieved, int_val);
}
#[test]
fn test_update_existing() {
let cache = Cache::new(10240);
let key = IntKey(1);
let value1 = IntValue(100);
let value2 = IntValue(200);
let old = cache.insert(key.clone(), value1.clone());
assert!(old.is_none());
let old = cache.insert(key.clone(), value2.clone());
assert!(old.is_some());
assert_eq!(old.expect("old value should exist"), value1);
let current = cache.get_clone(&key).expect("key should exist");
assert_eq!(current, value2);
}
#[test]
fn test_eviction_on_capacity() {
let cache = Cache::new(300);
for i in 0..50 {
let key = IntKey(i);
let value = IntValue(i as i64);
cache.insert(key, value);
}
assert!(cache.len() < 50, "Expected fewer than 50 entries, got {}", cache.len());
assert!(cache.size() <= 320, "Expected size <= 320, got {}", cache.size());
}
#[test]
fn test_frequency_based_eviction() {
let cache = Cache::new(600);
for i in 1..=5 {
cache.insert(CriticalKey(i), IntValue(i as i64));
}
for _ in 0..20 {
let _ = cache.get_clone(&CriticalKey(1));
let _ = cache.get_clone(&CriticalKey(2));
}
for i in 10..60 {
cache.insert(StandardKey(i), IntValue(i as i64));
}
let accessed_survived = cache.contains(&CriticalKey(1)) || cache.contains(&CriticalKey(2));
let unaccessed_survived = (3..=5).filter(|&i| cache.contains(&CriticalKey(i))).count();
assert!(
accessed_survived || unaccessed_survived <= 2,
"Frequently accessed entries should have better survival"
);
}
#[test]
fn test_concurrent_reads() {
let cache = Arc::new(Cache::new(10240));
for i in 0..100 {
cache.insert(IntKey(i), IntValue(i as i64));
}
let mut handles = vec![];
for _ in 0..4 {
let cache = cache.clone();
handles.push(thread::spawn(move || {
for i in 0..100 {
let key = IntKey(i);
if let Some(value) = cache.get_clone(&key) {
assert_eq!(value.0, i as i64);
}
}
}));
}
for handle in handles {
handle.join().expect("thread should not panic");
}
}
#[test]
fn test_concurrent_writes() {
let cache = Arc::new(Cache::new(10240));
let mut handles = vec![];
for t in 0..4 {
let cache = cache.clone();
handles.push(thread::spawn(move || {
for i in 0..25 {
let key = IntKey(t * 25 + i);
let value = IntValue((t * 25 + i) as i64);
cache.insert(key, value);
}
}));
}
for handle in handles {
handle.join().expect("thread should not panic");
}
assert_eq!(cache.len(), 100);
}
#[test]
fn test_concurrent_mixed_operations() {
let cache = Arc::new(Cache::new(10240));
for i in 0..50 {
cache.insert(IntKey(i), IntValue(i as i64));
}
let mut handles = vec![];
for _ in 0..2 {
let cache = cache.clone();
handles.push(thread::spawn(move || {
for i in 0..100 {
let key = IntKey(i % 50);
let _ = cache.get_clone(&key);
}
}));
}
for t in 0..2 {
let cache = cache.clone();
handles.push(thread::spawn(move || {
for i in 0..25 {
let key = IntKey(50 + t * 25 + i);
let value = IntValue((50 + t * 25 + i) as i64);
cache.insert(key, value);
}
}));
}
for handle in handles {
handle.join().expect("thread should not panic");
}
assert!(!cache.is_empty());
}
#[test]
fn test_clear() {
let cache = Cache::new(10240);
for i in 0..10 {
cache.insert(IntKey(i), IntValue(i as i64));
}
assert_eq!(cache.len(), 10);
cache.clear();
assert_eq!(cache.len(), 0);
assert_eq!(cache.size(), 0);
assert!(cache.is_empty());
}
#[test]
fn test_builder() {
let cache = CacheBuilder::new(1024).shards(32).build();
cache.insert(IntKey(1), IntValue(100));
assert!(cache.contains(&IntKey(1)));
}
#[test]
fn test_large_values() {
let cache = Cache::new(1024 * 1024);
let key = StringKey("large".to_string());
let value = StringValue {
data: "x".repeat(100_000), };
cache.insert(key.clone(), value);
let retrieved = cache.get_clone(&key).expect("key should exist");
assert_eq!(retrieved.data.len(), 100_000);
}
#[test]
fn test_policy_based_eviction() {
let cache = Cache::new(1000);
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct CriticalKey(u64);
impl CacheKey for CriticalKey {
type Value = CriticalValue;
fn policy(&self) -> CachePolicy {
CachePolicy::Critical
}
}
#[derive(Clone, Debug, PartialEq, DeepSizeOf)]
struct CriticalValue;
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct VolatileKey(u64);
impl CacheKey for VolatileKey {
type Value = VolatileValue;
fn policy(&self) -> CachePolicy {
CachePolicy::Volatile
}
}
#[derive(Clone, Debug, PartialEq, DeepSizeOf)]
struct VolatileValue;
cache.insert(CriticalKey(1), CriticalValue);
cache.insert(VolatileKey(2), VolatileValue);
for i in 10..20 {
cache.insert(IntKey(i), IntValue(i as i64));
}
}
#[test]
fn test_policy_eviction_order_strict() {
let cache = Cache::new(600);
for i in 1..=10 {
cache.insert(CriticalKey(i), IntValue(i as i64));
}
for i in 20..=30 {
cache.insert(VolatileKey(i), IntValue(i as i64));
}
for i in 40..=50 {
cache.insert(StandardKey(i), IntValue(i as i64));
}
for i in 100..200 {
cache.insert(IntKey(i), IntValue(i as i64));
}
assert!(!cache.is_empty(), "Cache should not be empty");
assert!(cache.size() <= 600, "Cache should maintain size limit");
}
#[test]
fn test_same_policy_frequency_tiebreaker() {
let cache = Cache::new(600);
for i in 1..=15 {
cache.insert(StandardKey(i), IntValue(i as i64));
}
for _ in 0..10 {
for i in 1..=5 {
let _ = cache.get_clone(&StandardKey(i));
}
}
for i in 30..120 {
cache.insert(IntKey(i), IntValue(i as i64));
}
assert!(cache.size() <= 600, "Cache should maintain size limit");
let remaining = (1..=15).filter(|&i| cache.contains(&StandardKey(i))).count();
assert!(remaining < 15, "Some entries should be evicted");
}
#[test]
fn test_large_volatile_vs_small_critical() {
let cache = Cache::new(700);
for i in 1..=10 {
cache.insert(VolatileKey(i), IntValue(i as i64));
}
for i in 20..=30 {
cache.insert(CriticalKey(i), IntValue(i as i64));
}
for _ in 0..10 {
for i in 20..=30 {
let _ = cache.get_clone(&CriticalKey(i));
}
}
for i in 100..200 {
cache.insert(StandardKey(i), IntValue(i as i64));
}
assert!(cache.size() <= 700, "Cache should maintain size limit");
assert!(!cache.is_empty(), "Cache should have entries");
}
#[test]
fn test_access_pattern_survival() {
let cache = Cache::new(500);
for i in 1..=20 {
cache.insert(StandardKey(i), IntValue(i as i64));
}
for _ in 0..10 {
for i in 1..=10 {
let _ = cache.get_clone(&StandardKey(i));
}
}
for i in 30..100 {
cache.insert(IntKey(i), IntValue(i as i64));
}
assert!(cache.size() <= 500, "Cache should maintain size limit");
assert!(!cache.is_empty(), "Cache should have entries");
let remaining = (1..=20).filter(|&i| cache.contains(&StandardKey(i))).count();
assert!(remaining < 20, "Some entries should be evicted under pressure");
}
#[test]
fn test_all_critical_still_evicts() {
let cache = Cache::new(400);
for i in 1..=15 {
cache.insert(CriticalKey(i), IntValue(i as i64));
}
for _ in 0..10 {
for i in 1..=5 {
let _ = cache.get_clone(&CriticalKey(i));
}
}
for i in 30..80 {
cache.insert(CriticalKey(i), IntValue(i as i64));
}
assert!(cache.size() <= 400, "Cache size should be within limit: {}", cache.size());
assert!(!cache.is_empty(), "Cache should have entries");
let total_inserted = 15 + 50; assert!(cache.len() < total_inserted, "Some entries must be evicted");
}
#[test]
fn test_policy_change_on_reinsert() {
let cache = Cache::new(600);
for i in 1..=10 {
cache.insert(StandardKey(i), IntValue(i as i64));
}
for i in 1..=5 {
cache.insert(StandardKey(i), IntValue(i as i64 * 10));
}
if let Some(val) = cache.get_clone(&StandardKey(1)) {
assert!(val.0 == 10 || val.0 == 1);
}
for i in 20..100 {
cache.insert(IntKey(i), IntValue(i as i64));
}
assert!(cache.size() <= 600, "Cache should maintain size limit");
assert!(!cache.is_empty(), "Cache should have entries");
}
#[test]
fn test_concurrent_access_affects_eviction() {
let cache = Arc::new(Cache::new(500));
for i in 1..=20 {
cache.insert(StandardKey(i), IntValue(i as i64));
}
let mut handles = vec![];
for t in 0..4 {
let cache = cache.clone();
handles.push(thread::spawn(move || {
for _ in 0..20 {
for i in (1 + t * 5)..=(5 + t * 5) {
if i <= 20 {
let _ = cache.get_clone(&StandardKey(i));
}
}
}
}));
}
for handle in handles {
handle.join().expect("thread should not panic");
}
for i in 30..70 {
cache.insert(IntKey(i), IntValue(i as i64));
}
let accessed_survive = (1..=20).filter(|&i| cache.contains(&StandardKey(i))).count();
assert!(
accessed_survive > 0,
"Some frequently accessed entries should survive concurrent eviction"
);
}
#[test]
fn test_exhausted_bucket_moves_to_next() {
let cache = Cache::new(500);
for i in 1..=10 {
cache.insert(VolatileKey(i), IntValue(i as i64));
}
for _ in 0..10 {
for i in 1..=5 {
let _ = cache.get_clone(&VolatileKey(i));
}
}
for i in 20..=30 {
cache.insert(StandardKey(i), IntValue(i as i64));
}
for i in 40..=50 {
cache.insert(CriticalKey(i), IntValue(i as i64));
}
for i in 100..200 {
cache.insert(IntKey(i), IntValue(i as i64));
}
assert!(cache.size() <= 500, "Cache should maintain size limit");
assert!(!cache.is_empty(), "Cache should have entries");
}
#[test]
fn test_clock_bit_clearing() {
let cache = Cache::new(300);
for i in 1..=5 {
cache.insert(StandardKey(i), IntValue(i as i64));
}
for i in 1..=5 {
let _ = cache.get_clone(&StandardKey(i));
}
for i in 10..40 {
cache.insert(IntKey(i), IntValue(i as i64));
}
let _remaining = (1..=5).filter(|&i| cache.contains(&StandardKey(i))).count();
assert!(cache.size() <= 300, "Cache should maintain size limit through clock eviction");
}
#[test]
fn test_frequency_decays_during_sweep() {
let cache = Cache::new(600);
for i in 1..=20 {
cache.insert(StandardKey(i), IntValue(i as i64));
}
for _ in 0..15 {
for i in 1..=10 {
let _ = cache.get_clone(&StandardKey(i));
}
}
for i in 30..150 {
cache.insert(IntKey(i), IntValue(i as i64));
}
assert!(cache.size() <= 600, "Cache should maintain size limit");
assert!(!cache.is_empty(), "Cache should have entries");
let remaining = (1..=20).filter(|&i| cache.contains(&StandardKey(i))).count();
assert!(remaining < 20, "Some StandardKey entries should be evicted");
}
#[cfg(feature = "metrics")]
#[test]
fn test_metrics_hit_miss_counters() {
let cache = Cache::new(1024);
let metrics = cache.metrics();
assert_eq!(metrics.hits, 0);
assert_eq!(metrics.misses, 0);
assert_eq!(metrics.hit_rate(), 0.0);
cache.insert(IntKey(1), IntValue(100));
cache.insert(IntKey(2), IntValue(200));
assert!(cache.get_clone(&IntKey(1)).is_some());
assert!(cache.get_clone(&IntKey(2)).is_some());
assert!(cache.get_clone(&IntKey(3)).is_none());
assert!(cache.get_clone(&IntKey(4)).is_none());
let metrics = cache.metrics();
assert_eq!(metrics.hits, 2, "Should have 2 hits");
assert_eq!(metrics.misses, 2, "Should have 2 misses");
assert_eq!(metrics.hit_rate(), 0.5, "Hit rate should be 50%");
assert_eq!(metrics.total_accesses(), 4);
}
#[cfg(feature = "metrics")]
#[test]
fn test_metrics_insert_update_counters() {
let cache = Cache::new(1024);
cache.insert(IntKey(1), IntValue(100));
cache.insert(IntKey(2), IntValue(200));
cache.insert(IntKey(3), IntValue(300));
let metrics = cache.metrics();
assert_eq!(metrics.inserts, 3, "Should have 3 inserts");
assert_eq!(metrics.updates, 0, "Should have 0 updates");
assert_eq!(metrics.total_writes(), 3);
cache.insert(IntKey(1), IntValue(101));
cache.insert(IntKey(2), IntValue(201));
let metrics = cache.metrics();
assert_eq!(metrics.inserts, 3, "Inserts should remain 3");
assert_eq!(metrics.updates, 2, "Should have 2 updates");
assert_eq!(metrics.total_writes(), 5);
}
#[cfg(feature = "metrics")]
#[test]
fn test_metrics_eviction_counter() {
let cache = Cache::with_shards(300, 4);
let metrics = cache.metrics();
assert_eq!(metrics.evictions, 0, "Initially no evictions");
for i in 0..50 {
cache.insert(IntKey(i), IntValue(i as i64));
}
let metrics = cache.metrics();
assert!(
metrics.evictions > 0,
"Should have evictions when over capacity (evictions: {}, inserts: {}, entries: {})",
metrics.evictions,
metrics.inserts,
metrics.entry_count
);
assert!(
metrics.inserts > metrics.entry_count as u64,
"More inserts than current entries means evictions occurred"
);
}
#[cfg(feature = "metrics")]
#[test]
fn test_metrics_removal_counter() {
let cache = Cache::new(1024);
cache.insert(IntKey(1), IntValue(100));
cache.insert(IntKey(2), IntValue(200));
cache.insert(IntKey(3), IntValue(300));
let metrics = cache.metrics();
assert_eq!(metrics.removals, 0, "No removals yet");
cache.remove(&IntKey(1));
cache.remove(&IntKey(2));
let metrics = cache.metrics();
assert_eq!(metrics.removals, 2, "Should have 2 removals");
cache.remove(&IntKey(999));
let metrics = cache.metrics();
assert_eq!(metrics.removals, 2, "Removals should still be 2");
}
#[cfg(feature = "metrics")]
#[test]
fn test_metrics_size_and_utilization() {
let capacity = 1024usize;
let cache = Cache::new(capacity);
let metrics = cache.metrics();
assert_eq!(metrics.capacity_bytes, capacity);
assert_eq!(metrics.current_size_bytes, 0);
assert_eq!(metrics.utilization(), 0.0);
cache.insert(IntKey(1), IntValue(100));
cache.insert(IntKey(2), IntValue(200));
let metrics = cache.metrics();
assert!(metrics.current_size_bytes > 0, "Size should increase after insert");
assert!(metrics.utilization() > 0.0, "Utilization should be > 0");
assert!(metrics.utilization() <= 1.0, "Utilization should be <= 1.0");
}
#[cfg(feature = "metrics")]
#[test]
fn test_metrics_entry_count() {
let cache = Cache::new(1024);
let metrics = cache.metrics();
assert_eq!(metrics.entry_count, 0);
for i in 0..10 {
cache.insert(IntKey(i), IntValue(i as i64));
}
let metrics = cache.metrics();
assert_eq!(metrics.entry_count, 10);
cache.remove(&IntKey(0));
cache.remove(&IntKey(1));
let metrics = cache.metrics();
assert_eq!(metrics.entry_count, 8);
}
#[cfg(feature = "metrics")]
#[test]
fn test_metrics_clear_resets_counters() {
let cache = Cache::new(1024);
cache.insert(IntKey(1), IntValue(100));
cache.insert(IntKey(2), IntValue(200));
cache.get_clone(&IntKey(1));
cache.get_clone(&IntKey(3)); cache.remove(&IntKey(2));
let metrics = cache.metrics();
assert!(metrics.inserts > 0);
assert!(metrics.hits > 0);
assert!(metrics.misses > 0);
assert!(metrics.removals > 0);
cache.clear();
let metrics = cache.metrics();
assert_eq!(metrics.hits, 0, "Hits should be reset");
assert_eq!(metrics.misses, 0, "Misses should be reset");
assert_eq!(metrics.inserts, 0, "Inserts should be reset");
assert_eq!(metrics.updates, 0, "Updates should be reset");
assert_eq!(metrics.evictions, 0, "Evictions should be reset");
assert_eq!(metrics.removals, 0, "Removals should be reset");
assert_eq!(metrics.entry_count, 0, "Entry count should be reset");
assert_eq!(metrics.current_size_bytes, 0, "Size should be reset");
}
#[cfg(feature = "metrics")]
#[test]
fn test_metrics_computed_methods() {
let cache = Cache::new(1024);
let metrics = cache.metrics();
assert_eq!(metrics.hit_rate(), 0.0);
cache.insert(IntKey(1), IntValue(100));
cache.get_clone(&IntKey(1)); cache.get_clone(&IntKey(1)); cache.get_clone(&IntKey(1)); cache.get_clone(&IntKey(2));
let metrics = cache.metrics();
assert_eq!(metrics.hit_rate(), 0.75); assert_eq!(metrics.total_accesses(), 4);
assert!(metrics.utilization() >= 0.0);
assert!(metrics.utilization() <= 1.0);
cache.insert(IntKey(2), IntValue(200)); cache.insert(IntKey(1), IntValue(101));
let metrics = cache.metrics();
assert_eq!(metrics.total_writes(), 3); }
#[cfg(feature = "metrics")]
#[test]
fn test_metrics_with_get_method() {
let cache = Cache::new(1024);
cache.insert(IntKey(1), IntValue(100));
{
let _guard = cache.get(&IntKey(1));
}
let metrics = cache.metrics();
assert_eq!(metrics.hits, 1);
{
let _guard = cache.get(&IntKey(999));
}
let metrics = cache.metrics();
assert_eq!(metrics.misses, 1);
}
#[cfg(feature = "metrics")]
#[test]
fn test_metrics_concurrent_updates() {
let cache = Arc::new(Cache::new(10240));
let mut handles = vec![];
for t in 0..4 {
let cache = cache.clone();
handles.push(thread::spawn(move || {
for i in 0..100 {
let key = IntKey(t * 100 + i);
cache.insert(key.clone(), IntValue(i as i64));
cache.get_clone(&key);
}
}));
}
for handle in handles {
handle.join().expect("thread should not panic");
}
let metrics = cache.metrics();
assert_eq!(metrics.inserts, 400);
assert_eq!(metrics.hits, 400);
assert_eq!(metrics.misses, 0);
assert_eq!(metrics.hit_rate(), 1.0);
}