subms-block-cache 0.10.0

submillisecond.com cookbook recipe - memory: subms-block-cache. Clock-sweep block cache with constant-time eviction.
Documentation
use super::*;

#[test]
fn get_returns_inserted_value() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(4);
    c.put(1, 10);
    c.put(2, 20);
    assert_eq!(c.get(&1).copied(), Some(10));
    assert_eq!(c.get(&2).copied(), Some(20));
    assert_eq!(c.get(&999), None);
}

#[test]
fn update_in_place() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(4);
    c.put(1, 10);
    let evicted = c.put(1, 11);
    assert!(evicted.is_none());
    assert_eq!(c.get(&1).copied(), Some(11));
    assert_eq!(c.len(), 1);
}

#[test]
fn evicts_when_full() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(3);
    c.put(1, 10);
    c.put(2, 20);
    c.put(3, 30);
    // All freshly inserted -> all referenced. Inserting 4 will sweep and
    // clear refs, eventually evicting one.
    let evicted = c.put(4, 40);
    assert!(evicted.is_some(), "should have evicted on insert at full");
    assert_eq!(c.len(), 3);
    assert_eq!(c.get(&4).copied(), Some(40));
}

#[test]
fn touched_key_survives_next_eviction() {
    // After the first sweep clears all referenced bits, touching a key
    // sets its bit again. The next insert at full capacity is then forced
    // to skip that touched key on its sweep.
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(3);
    c.put(1, 10);
    c.put(2, 20);
    c.put(3, 30);
    let _ = c.put(4, 40); // First eviction sweep (evicts key 1).
    let _ = c.get(&4); // Re-touch key 4 so its ref bit is set.
    let _ = c.put(5, 50); // Second sweep; key 4 should survive.
    assert!(c.get(&4).is_some(), "key 4 was just touched, should stay");
}

#[test]
fn capacity_floor_is_one() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(0);
    assert_eq!(c.capacity(), 1);
    c.put(1, 10);
    assert_eq!(c.get(&1).copied(), Some(10));
}

#[test]
fn len_and_is_empty() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(4);
    assert!(c.is_empty());
    c.put(1, 10);
    c.put(2, 20);
    assert_eq!(c.len(), 2);
    assert!(!c.is_empty());
}

#[test]
fn get_on_missing_returns_none() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(4);
    assert!(c.get(&42).is_none());
    c.put(1, 1);
    assert!(c.get(&42).is_none());
}

#[test]
fn evictions_match_insert_count_beyond_capacity() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(3);
    let mut evictions = 0;
    for i in 0..10u32 {
        if c.put(i, i).is_some() {
            evictions += 1;
        }
    }
    // 10 inserts into capacity 3 -> 7 evictions.
    assert_eq!(evictions, 7);
    assert_eq!(c.len(), 3);
}

#[test]
fn capacity_one_evicts_on_every_insert() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(1);
    c.put(1, 10);
    let ev = c.put(2, 20);
    assert!(ev.is_some());
    assert_eq!(ev.unwrap(), (1, 10));
    assert!(c.get(&1).is_none());
    assert_eq!(c.get(&2).copied(), Some(20));
}

#[test]
fn mixed_workload_preserves_invariants() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(5);
    for i in 0..20u32 {
        c.put(i, i * 10);
        if i % 3 == 0 {
            let _ = c.get(&(i / 2));
        }
    }
    assert_eq!(c.len(), 5);
}

#[test]
fn clock_sweep_clears_refs_then_evicts_and_reuses_slot() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(3);
    // Fill the three empty slots.
    c.put(1, 10);
    c.put(2, 20);
    c.put(3, 30);
    assert_eq!(c.len(), 3);
    // All three are referenced, so the sweep must clear ref bits (the
    // skip/continue arm) before it finds an evictable slot to replace.
    let ev = c.put(4, 40);
    assert!(ev.is_some(), "insert at full capacity must evict");
    assert_eq!(c.get(&4).copied(), Some(40));
    // Re-touch a survivor and evict again to re-run the skip path.
    let _ = c.get(&4);
    let _ = c.put(5, 50);
    let _ = c.put(6, 60);
    assert_eq!(c.len(), 3);
    // The most-recently-touched key should still be resident.
    assert!(c.get(&6).is_some());
}

#[test]
fn remove_returns_value_and_frees_the_slot() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(3);
    c.put(1, 10);
    c.put(2, 20);
    c.put(3, 30);
    assert_eq!(c.remove(&2), Some(20));
    assert_eq!(c.len(), 2);
    assert!(c.get(&2).is_none());
    // The vacated slot is reusable: the next insert takes the fill path
    // rather than sweeping, so nothing is evicted.
    assert!(c.put(4, 40).is_none(), "a hole absorbs the insert");
    assert_eq!(c.len(), 3);
    assert_eq!(c.get(&1).copied(), Some(10));
    assert_eq!(c.get(&4).copied(), Some(40));
}

#[test]
fn remove_absent_key_is_a_no_op() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(2);
    c.put(1, 10);
    assert_eq!(c.remove(&9), None);
    assert_eq!(c.len(), 1);
    assert_eq!(c.remove(&1), Some(10));
    assert_eq!(c.remove(&1), None, "a second remove finds nothing");
    assert!(c.is_empty());
}

#[test]
fn clear_empties_the_cache_and_keeps_capacity() {
    let mut c: BlockCache<u32, u32> = BlockCache::with_capacity(3);
    for k in 1..=5u32 {
        c.put(k, k * 10);
    }
    assert_eq!(c.len(), 3);
    c.clear();
    assert!(c.is_empty());
    assert_eq!(c.len(), 0);
    assert_eq!(c.capacity(), 3, "clear does not resize");
    assert!(c.get(&5).is_none());
    // The cache is usable again and refills from empty without evicting.
    for k in 10..=12u32 {
        assert!(c.put(k, k).is_none());
    }
    assert_eq!(c.len(), 3);
}