use oxistore_cache::{ArcCache, Cache, LruCache};
fn access<K, V, F>(cache: &mut ArcCache<K, V>, key: K, loader: F) -> bool
where
K: Eq + std::hash::Hash + Clone,
V: Clone,
F: FnOnce(&K) -> V,
{
if cache.get(&key).is_some() {
return true;
}
let value = loader(&key);
cache.put(key, value);
false
}
#[test]
fn t1_to_t2_promotion() {
let mut arc: ArcCache<u32, u32> = ArcCache::new(4);
access(&mut arc, 1, |&k| k * 10);
access(&mut arc, 2, |&k| k * 10);
access(&mut arc, 3, |&k| k * 10);
access(&mut arc, 4, |&k| k * 10);
let hit = arc.get(&2);
assert_eq!(hit, Some(&20), "second access to key 2 must be a cache hit");
assert!(arc.len() <= 4, "cache must not exceed capacity");
}
#[test]
fn multiple_promotions() {
let mut arc: ArcCache<u32, u32> = ArcCache::new(4);
for k in 1u32..=3 {
access(&mut arc, k, |&k| k);
}
for k in 1u32..=3 {
let v = arc.get(&k);
assert_eq!(v, Some(&k), "re-access of key {k} should hit");
}
access(&mut arc, 99, |&k| k);
for k in 1u32..=3 {
assert!(arc.get(&k).is_some(), "hot key {k} should still be cached");
}
}
#[test]
fn ghost_list_p_adapts_on_b1_hit() {
let cap = 4usize;
let mut arc: ArcCache<u32, u32> = ArcCache::new(cap);
for k in 1u32..=(cap as u32) {
arc.put(k, k * 10);
}
let p_initial = arc.p();
for k in (cap as u32 + 1)..=(2 * cap as u32) {
arc.put(k, k * 10);
}
let miss = arc.get(&1); assert!(miss.is_none(), "key 1 should be a ghost (evicted from t1)");
let p_after_b1_hit = arc.p();
assert!(
p_after_b1_hit > p_initial || p_after_b1_hit == cap,
"p should have increased (or capped at cap) after b1 ghost hit; was {p_initial}, now {p_after_b1_hit}"
);
}
#[test]
fn ghost_list_p_adapts_on_b2_hit() {
let cap = 4usize;
let mut arc: ArcCache<u32, u32> = ArcCache::new(cap);
for k in 1u32..=(cap as u32) {
arc.put(k, k * 10);
}
for k in 1u32..=(cap as u32) {
arc.get(&k); }
for k in (cap as u32 + 1)..=(3 * cap as u32) {
arc.put(k, k * 10);
}
let p_before = arc.p();
let _ = arc.get(&1); let p_after = arc.p();
assert!(
p_after <= p_before.max(cap),
"p should not exceed cap; got {p_after}"
);
}
fn run_mixed_workload<C>(cache: &mut C, hot_size: u32, scan_size: u32, rounds: u32) -> u32
where
C: Cache<u32, u32>,
{
let mut hits = 0u32;
for round in 0..rounds {
for k in 0..hot_size {
if cache.get(&k).is_some() {
hits += 1;
} else {
cache.put(k, k);
}
}
let base = hot_size + round * scan_size;
for k in base..(base + scan_size) {
if cache.get(&k).is_some() {
hits += 1;
} else {
cache.put(k, k);
}
}
}
hits
}
#[test]
fn arc_better_than_lru_on_scan_plus_hot_workload() {
let cap = 8usize;
let hot = 4u32;
let scan = 6u32;
let rounds = 10u32;
let mut lru: LruCache<u32, u32> = LruCache::new(cap);
let mut arc: ArcCache<u32, u32> = ArcCache::new(cap);
let lru_hits = run_mixed_workload(&mut lru, hot, scan, rounds);
let arc_hits = run_mixed_workload(&mut arc, hot, scan, rounds);
assert!(
arc_hits > lru_hits,
"ARC ({arc_hits} hits) must strictly outperform LRU ({lru_hits} hits) on scan+hot workload"
);
let mut hot_hits = 0u32;
for k in 0..hot {
if arc.get(&k).is_some() {
hot_hits += 1;
}
}
assert!(
hot_hits >= 2,
"ARC should preserve at least 2/{hot} hot-set entries; got {hot_hits}"
);
}
#[test]
fn arc_basic_hit_miss() {
let mut arc: ArcCache<u32, u32> = ArcCache::new(3);
assert!(arc.get(&1).is_none());
arc.put(1, 10);
assert_eq!(arc.get(&1), Some(&10));
arc.put(2, 20);
arc.put(3, 30);
assert_eq!(arc.len(), 3);
arc.put(4, 40); assert!(arc.len() <= 3);
}
#[test]
fn arc_len_and_cap() {
let mut arc: ArcCache<u32, u32> = ArcCache::new(5);
assert_eq!(arc.cap(), 5);
assert_eq!(arc.len(), 0);
assert!(arc.is_empty());
for i in 0..5 {
arc.put(i, i);
}
assert_eq!(arc.len(), 5);
arc.put(99, 99);
assert!(arc.len() <= 5);
}
#[test]
fn arc_update_existing_key() {
let mut arc: ArcCache<u32, u32> = ArcCache::new(3);
arc.put(1, 10);
arc.put(1, 11); assert_eq!(arc.get(&1), Some(&11));
assert_eq!(arc.len(), 1);
}