lfudacaching-0.1.0.0: test/Test/Data/LfudaCache.hs
module Test.Data.LfudaCache
( tests
) where
import Test.Tasty
import Test.Tasty.HUnit
import Control.Monad
import Data.Maybe (isNothing)
import Data.LfudaCache
import Data.Foldable (foldl')
import Prelude hiding (lookup)
import Data.Kind
tests :: TestTree
tests = testGroup "LFUDA Cache Tests"
[ testCase "Basic LFUDA Operations" testLFUDA
, testCase "GDSF Test" testGDSF
, testCase "Insert Eviction Via InsertView" testInsertEviction
, testCase "Contains Doesn't Update Frequency" testLFUDAContains
, testCase "Peek Doesn't Update Frequency" testLFUDAPeek
, testCase "Remove Operation" testLFUDARemove
, testCase "Age Tracking" testLFUDAAge
, testCase "Size Tracking" testLFUDASize
, testCase "LFU Basic Operations" testLFU
, testCase "LFU No Dynamic Aging" testLFUNoAging
, testCase "LFU Frequency-Based Eviction" testLFUFrequencyEviction
, testCase "LFU InsertView" testLFUInsertView
, testCase "GDSF Size-Weighted Eviction" testGDSFSizeEviction
, testCase "GDSF Dynamic Aging" testGDSFAging
, testCase "GDSF Frequency And Size" testGDSFFrequencyAndSize
, testCase "Edge: Re-insert Resets Frequency" testReinsertResetsFrequency
, testCase "Edge: Capacity 1 Corner Cases" testCapacity1
, testCase "Edge: Size Consistency" testSizeConsistency
, testCase "Edge: Purge Preserves Age" testPurgePreservesAge
, testCase "Edge: Operations After Purge" testOpsAfterPurge
, testCase "Edge: InsertView Self-Replace" testInsertViewSelfReplace
, testCase "Edge: InsertView Evicts Correct Entry" testInsertViewEvictsCorrect
, testCase "Edge: Remove Nonexistent" testRemoveNonexistent
, testCase "Edge: Equal Frequency Eviction" testEqualFrequencyEviction
, testCase "Edge: LFUDA Age Accumulation" testAgeAccumulation
, testCase "Edge: Lookup After Insert Same Key" testLookupAfterReinsert
, testCase "Edge: Rapid Insert-Remove Cycles" testInsertRemoveCycles
, testBenchmark "LFUDA Benchmark" benchmarkLFUDA
, testBenchmark "LFUDA Random Benchmark" benchmarkLFUDARand
]
testLFUDA :: Assertion
testLFUDA = do
-- Create cache with proper policy
let initialCache :: LfudaCache Int Int
initialCache = newCache 666 LFUDA
-- Track number of evictions using insertView
let insertAndTrackEvictions :: (LfudaCache Int Int, Int) -> Int -> (LfudaCache Int Int, Int)
insertAndTrackEvictions (cache, count) i =
let (evictedEntry, cache') = insertView i i cache
newCount = case evictedEntry of
Just _ -> count + 1
Nothing -> count
in (cache', newCount)
let finalCacheAndCount = foldl' insertAndTrackEvictions (initialCache, 0) [100..999 :: Int]
finalCache = fst finalCacheAndCount
evictionCount = snd finalCacheAndCount
let len = size finalCache
assertEqual "Cache length should match" 666 len
let keys2 = keys finalCache
assertEqual "Keys length should match cache length" len (length keys2)
-- Check eviction count
assertEqual "Eviction count should match" (234 :: Int) evictionCount
-- Check values that should be in cache
forM_ keys2 $ \k -> do
let result = lookup k finalCache
case result of
Just (v, _) -> assertEqual "Value should match key" k v
Nothing -> assertFailure $ "Key " ++ show k ++ " should be in cache"
-- These should be misses (keys 100-333, since we inserted 900 items in a cache of size 666)
forM_ [100..333 :: Int] $ \i -> do
let result = lookup i finalCache
assertBool ("Key " ++ show i ++ " should not be in cache") (isNothing result)
-- Set a new value and check it
let cacheWithNewVal = insert (256 :: Int) (256 :: Int) finalCache
let result = lookup (256 :: Int) cacheWithNewVal
case result of
Just (v, _) -> assertEqual "Value for key 256 should be 256" (256 :: Int) v
Nothing -> assertFailure "Key 256 should be in cache"
-- Check most frequently used key after updating key 256
let updatedCache = case result of
Just (_, c') -> c'
Nothing -> cacheWithNewVal
let keysAfterUpdate = keys updatedCache
assertBool "Keys should be present after update" (not (null keysAfterUpdate))
-- Purge and verify empty
let purgedCache = purge updatedCache
let lenAfterPurge = size purgedCache
assertEqual "Cache should be empty after purge" 0 lenAfterPurge
let resultAfterPurge = lookup (200 :: Int) purgedCache
assertBool "Cache should contain nothing after purge" (isNothing resultAfterPurge)
testGDSF :: Assertion
testGDSF = do
let initialCache :: LfudaCache Int Int
initialCache = newGDSF 666
-- Insert elements with power of 2 values
let cacheWith10to19 = foldl' (\cache i ->
insert i (2 ^ i) cache
) initialCache [10..19 :: Int]
-- Insert more elements with same key/value
let finalCache = foldl' (\cache i ->
insert i i cache
) cacheWith10to19 [100..999 :: Int]
let len = size finalCache
assertEqual "Cache length should match" 666 len
let keys2 = keys finalCache
assertEqual "Keys length should match cache length" len (length keys2)
-- Check values that should be in cache
forM_ keys2 $ \k -> do
let result = lookup k finalCache
assertBool "Get should return a result" (not (isNothing result))
case result of
Just (v, _) ->
if k >= 10 && k <= 19
then assertEqual "Value should be 2^key for keys 10-19" (2 ^ k) v
else assertEqual "Value should match key for other keys" k v
Nothing -> assertFailure $ "Key " ++ show k ++ " should be in cache"
-- Set a new value and check it
let cacheWithNewVal = insert (256 :: Int) (256 :: Int) finalCache
let result = lookup (256 :: Int) cacheWithNewVal
case result of
Just (v, _) -> assertEqual "Value for key 256 should be 256" (256 :: Int) v
Nothing -> assertFailure "Key 256 should be in cache"
-- Check most frequently used key after updating key 256
let updatedCache = case result of
Just (_, c') -> c'
Nothing -> cacheWithNewVal
let keysAfterUpdate = keys updatedCache
-- Key 256 should have higher frequency due to the get operation above
assertBool "Keys should be present after update" (not (null keysAfterUpdate))
testInsertEviction :: Assertion
testInsertEviction = do
let cache :: LfudaCache Int Int
cache = newCache 1 LFUDA
-- First insert should not evict
let (evicted1, _) = insertView (1 :: Int) (1 :: Int) cache
assertEqual "Should not have evicted" Nothing evicted1
-- Second insert (different key) should evict
let c1 = insert (1 :: Int) (1 :: Int) cache
let (evicted2, _) = insertView (2 :: Int) (2 :: Int) c1
assertBool "Should have evicted" (evicted2 /= Nothing)
testLFUDAContains :: Assertion
testLFUDAContains = do
let initialCache :: LfudaCache Int Int
initialCache = newCache 2 LFUDA
let cache1 = insert (1 :: Int) (1 :: Int) initialCache
let cache2 = insert (2 :: Int) (2 :: Int) cache1
-- Bump hits for key 1
let finalCache1 = foldl' (\c _ ->
case lookup (1 :: Int) c of
Just (_, c') -> c'
Nothing -> c
) cache2 [1..10 :: Int]
let keys1 = keys finalCache1
case keys1 of
(k:_) -> assertEqual "Key 1 should be most frequently used" (1 :: Int) k
[] -> assertFailure "Keys list should not be empty"
-- Contains should not bump hits for key 2
let finalCache2 = foldl' (\c _ -> c) finalCache1 (replicate 20 (contains (2 :: Int) finalCache1))
let keys2 = keys finalCache2
case keys2 of
(k:_) -> assertEqual "Key 1 should still be most frequently used" (1 :: Int) k
[] -> assertFailure "Keys list should not be empty"
testLFUDAPeek :: Assertion
testLFUDAPeek = do
let initialCache :: LfudaCache Int Int
initialCache = newLFUDA 2
let cache1 = insert (1 :: Int) (1 :: Int) initialCache
let cache2 = insert (2 :: Int) (2 :: Int) cache1
-- Peek should not update frequency
let result1 = peek (1 :: Int) cache2
assertEqual "Value for key 1 should be 1" (Just 1) result1
-- Increase frequency of key 2
let cache2' = case lookup (2 :: Int) cache2 of
Just (_, c) -> c
Nothing -> cache2
-- Adding key 3 should evict key 1 (lowest frequency)
let cache3 = insert (3 :: Int) (3 :: Int) cache2'
-- Key 1 should be evicted
let containsKey1 = contains (1 :: Int) cache3
assertBool "Key 1 should have been evicted" (not containsKey1)
testLFUDARemove :: Assertion
testLFUDARemove = do
let initialCache :: LfudaCache Int Int
initialCache = newLFUDA 2
let cache1 = insert (1 :: Int) (1 :: Int) initialCache
let cache2 = insert (2 :: Int) (2 :: Int) cache1
let result1 = lookup (1 :: Int) cache2
case result1 of
Just (v, _) -> assertEqual "Value for key 1 should be 1" (1 :: Int) v
Nothing -> assertFailure "Key 1 should be in cache"
-- Remove key 1
assertBool "Key 1 should be in cache before removal" (contains (1 :: Int) cache2)
let cache2' = remove (1 :: Int) cache2
let result1' = lookup (1 :: Int) cache2'
assertBool "Key 1 should not be in cache after removal" (isNothing result1')
let len = size cache2'
assertEqual "Cache length should be 1 after removal" 1 len
testLFUDAAge :: Assertion
testLFUDAAge = do
let initialCache :: LfudaCache Int Int
initialCache = newLFUDA 1
-- Set key 1 with initial frequency 1
let cache1 = insert (1 :: Int) (1 :: Int) initialCache
-- Bump hits on key 1 to frequency 2
let cache1' = case lookup (1 :: Int) cache1 of
Just (_, c) -> c
Nothing -> cache1
-- Set key 2 - but key 2 will be immediately evicted because
-- it has lower priority (1) than key 1 (2)
let cache2 = insert (2 :: Int) (2 :: Int) cache1'
-- The age should now be 1 (the frequency of the evicted key 2)
let age1 = age cache2
assertEqual "Cache age should be 1" 1 age1
testLFUDASize :: Assertion
testLFUDASize = do
let initialCache :: LfudaCache Int Int
initialCache = newLFUDA 10
-- Insert elements
let finalCache = foldl' (\cache i ->
insert i i cache
) initialCache [10..29 :: Int]
-- Check size
let s = size finalCache
assertEqual "Cache size should be 10" 10 s
-- Purge and check size again
let purgedCache = purge finalCache
let s' = size purgedCache
assertEqual "Cache size should be 0 after purge" 0 s'
-- Edge Case Tests
-- Re-inserting a key resets its frequency to 1, making it vulnerable to eviction
testReinsertResetsFrequency :: Assertion
testReinsertResetsFrequency = do
let cache :: LfudaCache Int Int
cache = newLFUDA 2
-- Insert keys 1 and 2
let c1 = insert (1 :: Int) (10 :: Int) cache
let c2 = insert (2 :: Int) (20 :: Int) c1
-- Lookup key 1 many times to build up its frequency
let c3 = foldl' (\c _ -> case lookup (1 :: Int) c of
Just (_, c') -> c'
Nothing -> c
) c2 [1..10 :: Int]
-- Key 1 now has high frequency. Re-insert it with new value - resets freq to 1!
let c4 = insert (1 :: Int) (100 :: Int) c3
-- Verify the value was updated
assertEqual "Value should be updated to 100" (Just 100) (peek (1 :: Int) c4)
-- Now insert key 3. Key 1 has freq 1 again, key 2 also has freq 1.
-- One of them gets evicted.
let c5 = insert (3 :: Int) (30 :: Int) c4
assertEqual "Size should be 2" 2 (size c5)
-- Capacity 1 cache: every new distinct key causes eviction
testCapacity1 :: Assertion
testCapacity1 = do
let cache :: LfudaCache Int Int
cache = newLFUDA 1
-- First insert: no eviction
let (ev1, _) = insertView (1 :: Int) (10 :: Int) cache
let c1 = insert (1 :: Int) (10 :: Int) cache
assertEqual "First insert should not evict" Nothing ev1
assertEqual "Size should be 1" 1 (size c1)
-- Same key: replaces value, no eviction (size stays 1)
let (ev2, c2) = insertView (1 :: Int) (20 :: Int) c1
assertEqual "Re-insert same key should not evict" Nothing ev2
assertEqual "Size should still be 1" 1 (size c2)
assertEqual "Value should be updated" (Just 20) (peek (1 :: Int) c2)
-- Different key: must evict
let (ev3, c3) = insertView (2 :: Int) (30 :: Int) c2
assertBool "Different key should evict" (ev3 /= Nothing)
assertEqual "Size should still be 1" 1 (size c3)
assertBool "Key 1 should be gone" (not (contains (1 :: Int) c3))
assertEqual "Key 2 should be present" (Just 30) (peek (2 :: Int) c3)
-- Lookup on evicted key
let result = lookup (1 :: Int) c3
assertBool "Lookup evicted key should return Nothing" (isNothing result)
-- Size must always match the actual number of entries in the queue
testSizeConsistency :: Assertion
testSizeConsistency = do
let cache :: LfudaCache Int Int
cache = newLFUDA 3
-- Empty cache
assertEqual "Empty cache size" 0 (size cache)
-- Insert 3
let c1 = insert (1 :: Int) (10 :: Int) cache
assertEqual "After 1 insert" 1 (size c1)
let c2 = insert (2 :: Int) (20 :: Int) c1
let c3 = insert (3 :: Int) (30 :: Int) c2
assertEqual "After 3 inserts" 3 (size c3)
-- Remove 1
let c4 = remove (1 :: Int) c3
assertEqual "After remove" 2 (size c4)
assertEqual "Keys count matches size" (size c4) (length (keys c4))
-- Re-insert key 1 (no eviction, room available)
let (ev, c5) = insertView (1 :: Int) (100 :: Int) c4
assertEqual "Should not evict (room available)" Nothing ev
assertEqual "After re-insert" 3 (size c5)
-- Re-insert existing key 2 (replace, no size change)
let c6 = insert (2 :: Int) (200 :: Int) c5
assertEqual "After replace" 3 (size c6)
assertEqual "Keys count matches size" (size c6) (length (keys c6))
-- Insert 4th key to force eviction
let (ev2, c7) = insertView (4 :: Int) (40 :: Int) c6
assertBool "Should evict" (ev2 /= Nothing)
assertEqual "After eviction" 3 (size c7)
assertEqual "Keys count matches size" (size c7) (length (keys c7))
-- Purge
let c8 = purge c7
assertEqual "After purge" 0 (size c8)
assertEqual "Keys empty after purge" 0 (length (keys c8))
-- Insert after purge
let c9 = insert (5 :: Int) (50 :: Int) c8
assertEqual "After insert post-purge" 1 (size c9)
-- Purge clears entries but does NOT reset age
testPurgePreservesAge :: Assertion
testPurgePreservesAge = do
let cache :: LfudaCache Int Int
cache = newLFUDA 1
let c1 = insert (1 :: Int) (10 :: Int) cache
-- Bump frequency to 3
let c2 = foldl' (\c _ -> case lookup (1 :: Int) c of
Just (_, c') -> c'
Nothing -> c
) c1 [1..2 :: Int]
-- Evict key 1 by inserting key 2
let c3 = insert (2 :: Int) (20 :: Int) c2
let ageBeforePurge = age c3
assertBool "Age should be > 0 after eviction" (ageBeforePurge > 0)
-- Purge
let c4 = purge c3
assertEqual "Age should survive purge" ageBeforePurge (age c4)
assertEqual "Size should be 0" 0 (size c4)
-- Cache should work normally after purge
testOpsAfterPurge :: Assertion
testOpsAfterPurge = do
let cache :: LfudaCache Int Int
cache = newLFUDA 2
-- Fill cache, then purge
let c1 = insert (1 :: Int) (10 :: Int) cache
let c2 = insert (2 :: Int) (20 :: Int) c1
let c3 = purge c2
-- All operations should work on purged cache
assertBool "Contains on purged cache" (not (contains (1 :: Int) c3))
assertEqual "Peek on purged cache" Nothing (peek (1 :: Int) c3)
assertBool "Lookup on purged cache" (isNothing (lookup (1 :: Int) c3))
assertEqual "Keys on purged cache" [] (keys c3)
-- Insert should work
let (ev1, c4) = insertView (3 :: Int) (30 :: Int) c3
assertEqual "Should not evict (empty after purge)" Nothing ev1
assertEqual "Value accessible" (Just 30) (peek (3 :: Int) c4)
let (ev2, c5) = insertView (4 :: Int) (40 :: Int) c4
assertEqual "Should not evict (still room)" Nothing ev2
assertEqual "Size should be 2" 2 (size c5)
-- Eviction should work after purge
let (ev3, c6) = insertView (5 :: Int) (50 :: Int) c5
assertBool "Should evict now (full)" (ev3 /= Nothing)
assertEqual "Size still 2" 2 (size c6)
-- insertView on an existing key should NOT report eviction
testInsertViewSelfReplace :: Assertion
testInsertViewSelfReplace = do
let cache :: LfudaCache Int Int
cache = newLFUDA 2
let c1 = insert (1 :: Int) (10 :: Int) cache
let c2 = insert (2 :: Int) (20 :: Int) c1
-- insertView same key: no eviction, just replacement
let (evicted, c3) = insertView (1 :: Int) (100 :: Int) c2
assertEqual "Should not report eviction for self-replace" Nothing evicted
assertEqual "Value should be updated" (Just 100) (peek (1 :: Int) c3)
assertEqual "Size should be unchanged" 2 (size c3)
-- insertView on capacity-1 cache, same key
let cache1 :: LfudaCache Int Int
cache1 = newLFUDA 1
let c4 = insert (1 :: Int) (10 :: Int) cache1
let (evicted2, c5) = insertView (1 :: Int) (100 :: Int) c4
assertEqual "No eviction for self-replace on cap-1" Nothing evicted2
assertEqual "Size should be 1" 1 (size c5)
-- insertView reports the correct evicted entry
testInsertViewEvictsCorrect :: Assertion
testInsertViewEvictsCorrect = do
let cache :: LfudaCache Int Int
cache = newLFUDA 1
let c1 = insert (1 :: Int) (10 :: Int) cache
-- Insert key 2, should evict key 1 and report it
let (evicted, c2) = insertView (2 :: Int) (20 :: Int) c1
assertEqual "Should report key 1 evicted" (Just (1 :: Int, 10 :: Int)) evicted
assertEqual "Size should be 1" 1 (size c2)
-- Bump key 2 frequency, then insert key 3
let c3 = case lookup (2 :: Int) c2 of
Just (_, c) -> c
Nothing -> c2
let (evicted2, _) = insertView (3 :: Int) (30 :: Int) c3
-- Key 3 has freq 1, key 2 has freq 2, so key 3 gets evicted
assertEqual "Should report key 3 evicted (lower freq)" (Just (3 :: Int, 30 :: Int)) evicted2
-- Remove on empty cache and nonexistent keys
testRemoveNonexistent :: Assertion
testRemoveNonexistent = do
let cache :: LfudaCache Int Int
cache = newLFUDA 5
-- Remove from empty cache
let c1 = remove (1 :: Int) cache
assertEqual "Size should still be 0" 0 (size c1)
-- Insert then remove nonexistent
let c2 = insert (1 :: Int) (10 :: Int) c1
assertBool "Key 999 should not be in cache" (not (contains (999 :: Int) c2))
let c3 = remove (999 :: Int) c2
assertEqual "Size should still be 1" 1 (size c3)
-- Remove then remove same key again
assertBool "Key 1 should be in cache" (contains (1 :: Int) c3)
let c4 = remove (1 :: Int) c3
assertBool "Key 1 should be gone after removal" (not (contains (1 :: Int) c4))
let c5 = remove (1 :: Int) c4
assertEqual "Size should be 0" 0 (size c5)
-- When all entries have the same frequency, eviction should still work
testEqualFrequencyEviction :: Assertion
testEqualFrequencyEviction = do
let cache :: LfudaCache Int Int
cache = newLFUDA 3
-- Insert 3 entries, all with frequency 1
let c1 = insert (1 :: Int) (10 :: Int) cache
let c2 = insert (2 :: Int) (20 :: Int) c1
let c3 = insert (3 :: Int) (30 :: Int) c2
-- Insert 4th, must evict one (all have same freq)
let (ev, c4) = insertView (4 :: Int) (40 :: Int) c3
assertBool "Must evict something" (ev /= Nothing)
assertEqual "Size must be 3" 3 (size c4)
assertEqual "Keys count must be 3" 3 (length (keys c4))
-- All remaining keys should be accessible
let remainingKeys = keys c4
forM_ remainingKeys $ \k ->
assertBool ("Key " ++ show k ++ " should be accessible") (contains k c4)
-- LFUDA age should grow over multiple eviction cycles
testAgeAccumulation :: Assertion
testAgeAccumulation = do
let cache :: LfudaCache Int Int
cache = newLFUDA 1
assertEqual "Initial age" 0 (age cache)
-- Round 1: insert key 1, bump freq to 3, evict with key 2
let c1 = insert (1 :: Int) (10 :: Int) cache
let c2 = foldl' (\c _ -> case lookup (1 :: Int) c of
Just (_, c') -> c'
Nothing -> c
) c1 [1..2 :: Int]
-- key 1 has freq 3
let c3 = insert (2 :: Int) (20 :: Int) c2
-- key 2 (freq 1) was evicted, age should be 1
let age1 = age c3
assertEqual "Age after first eviction cycle" 1 age1
-- Round 2: bump key 1 freq more, evict again
let c4 = foldl' (\c _ -> case lookup (1 :: Int) c of
Just (_, c') -> c'
Nothing -> c
) c3 [1..3 :: Int]
-- key 1 now has freq 6
let c5 = insert (3 :: Int) (30 :: Int) c4
-- key 3 (freq 1) evicted, but its priority was 1 + age1 = 2
-- age should be set to the evicted entry's frequency (1), NOT the priority
let age2 = age c5
assertEqual "Age after second eviction cycle" 1 age2
-- Round 3: create a scenario where an entry with freq > 1 gets evicted.
-- Use a size-2 cache for this.
let cache2 :: LfudaCache Int Int
cache2 = newLFUDA 2
let d1 = insert (10 :: Int) (100 :: Int) cache2
let d2 = insert (20 :: Int) (200 :: Int) d1
-- Bump key 20 freq to 3
let d3 = foldl' (\c _ -> case lookup (20 :: Int) c of
Just (_, c') -> c'
Nothing -> c
) d2 [1..2 :: Int]
-- key 10 has freq 1, key 20 has freq 3
-- Insert key 30: evicts key 10 (freq 1), age becomes 1
let d4 = insert (30 :: Int) (300 :: Int) d3
assertEqual "Age after evicting freq-1 entry" 1 (age d4)
-- Now bump key 30 freq to 4
let d5 = foldl' (\c _ -> case lookup (30 :: Int) c of
Just (_, c') -> c'
Nothing -> c
) d4 [1..3 :: Int]
-- key 20 has freq 3, stale priority = 3 (set when age was 0, never recalculated)
-- key 30 has freq 4, priority = 4 + 1 = 5 (recalculated on each lookup)
-- Insert key 40: priority = 1 + age(1) = 2
-- Eviction order by priority: key 40 (2) < key 20 (3) < key 30 (5)
-- key 40 gets evicted immediately (lowest priority), age = freq(40) = 1
let d6 = insert (40 :: Int) (400 :: Int) d5
assertEqual "Age stays 1 (key 40 evicted, not key 20)" 1 (age d6)
-- To actually evict a high-freq entry, we need to lookup key 20 first
-- to refresh its priority with the current age
let d7 = case lookup (20 :: Int) d6 of
Just (_, c') -> c' -- key 20 freq becomes 4, priority = 4 + 1 = 5
Nothing -> d6
-- key 20 now has freq 4, priority 5. key 30 has freq 4, priority 5.
-- Insert key 50: priority = 1 + 1 = 2. Still lowest, key 50 evicted.
let d8 = insert (50 :: Int) (500 :: Int) d7
assertEqual "Age still 1 (new entry evicted again)" 1 (age d8)
-- After re-inserting a key, lookup should return the new value
-- but with reset frequency (making it vulnerable to eviction)
testLookupAfterReinsert :: Assertion
testLookupAfterReinsert = do
let cache :: LfudaCache Int Int
cache = newLFUDA 2
let c1 = insert (1 :: Int) (10 :: Int) cache
let c2 = insert (2 :: Int) (20 :: Int) c1
-- Bump key 1 frequency high
let c3 = foldl' (\c _ -> case lookup (1 :: Int) c of
Just (_, c') -> c'
Nothing -> c
) c2 [1..5 :: Int]
-- Re-insert key 1 with new value (resets freq to 1!)
let c4 = insert (1 :: Int) (999 :: Int) c3
-- Lookup should return new value
case lookup (1 :: Int) c4 of
Just (v, _) -> assertEqual "Should get new value" (999 :: Int) v
Nothing -> assertFailure "Key 1 should be in cache"
-- But now key 1 has low freq again.
-- Bump key 2 freq so key 1 becomes eviction target
let c5 = case lookup (2 :: Int) c4 of
Just (_, c') -> c'
Nothing -> c4
-- Insert key 3: should evict key 1 (lowest freq after re-insert)
let c6 = insert (3 :: Int) (30 :: Int) c5
assertBool "Key 1 should be evicted (freq was reset)" (not (contains (1 :: Int) c6))
assertBool "Key 2 should survive" (contains (2 :: Int) c6)
-- Rapid insert-remove cycles should keep size correct
testInsertRemoveCycles :: Assertion
testInsertRemoveCycles = do
let cache :: LfudaCache Int Int
cache = newLFUDA 5
-- Insert and remove 100 keys rapidly
let finalCache = foldl' (\c i ->
let c1 = insert i i c
c2 = remove i c1
in c2
) cache [1..100 :: Int]
assertEqual "Size should be 0 after insert-remove cycles" 0 (size finalCache)
assertEqual "Keys should be empty" [] (keys finalCache)
-- Insert-remove with some surviving (use capacity 10 to avoid eviction interference)
let cache10 :: LfudaCache Int Int
cache10 = newLFUDA 10
let finalCache2 = foldl' (\c i ->
let c1 = insert i i c
in if even i
then remove i c1
else c1
) cache10 [1..10 :: Int]
assertEqual "Size should be 5 (odd keys survive)" 5 (size finalCache2)
forM_ [1, 3, 5, 7, 9 :: Int] $ \i ->
assertBool ("Key " ++ show i ++ " should be present") (contains i finalCache2)
forM_ [2, 4, 6, 8, 10 :: Int] $ \i ->
assertBool ("Key " ++ show i ++ " should be absent") (not (contains i finalCache2))
-- Verify that with tight capacity, eviction CAN steal surviving entries
-- Capacity 5, insert keys 1-10, keep odd, remove even.
-- At i=10, cache has [1,3,5,7,9] (full), inserting 10 evicts an odd key first!
let finalCache3 = foldl' (\c i ->
let c1 = insert i i c
in if even i
then remove i c1
else c1
) cache [1..10 :: Int]
assertEqual "Size should be 4 (one odd key was evicted by insert 10)" 4 (size finalCache3)
-- LFU Tests
testLFU :: Assertion
testLFU = do
let initialCache :: LfudaCache Int Int
initialCache = newLFU 5
-- Insert 5 elements
let cache1 = foldl' (\c i -> insert i (i * 10) c) initialCache [1..5 :: Int]
assertEqual "Cache size should be 5" 5 (size cache1)
-- All 5 elements should be present
forM_ [1..5 :: Int] $ \i -> do
let result = peek i cache1
assertEqual ("Value for key " ++ show i) (Just (i * 10)) result
-- Insert 6th element, should evict one
let (evicted6, cache2) = insertView (6 :: Int) (60 :: Int) cache1
assertBool "Should have evicted" (evicted6 /= Nothing)
assertEqual "Cache size should still be 5" 5 (size cache2)
-- Key 6 should be present
let result6 = peek (6 :: Int) cache2
assertEqual "Key 6 should be present" (Just 60) result6
testLFUNoAging :: Assertion
testLFUNoAging = do
let initialCache :: LfudaCache Int Int
initialCache = newLFU 1
-- Insert key 1, then bump its frequency
let cache1 = insert (1 :: Int) (10 :: Int) initialCache
let cache1' = case lookup (1 :: Int) cache1 of
Just (_, c) -> c
Nothing -> cache1
-- Age should be 0 before any eviction
assertEqual "Age should be 0 initially" 0 (age cache1')
-- Insert key 2, which evicts key 2 (lower priority) since key 1 has freq 2
let cache2 = insert (2 :: Int) (20 :: Int) cache1'
-- Age should STILL be 0 for LFU (no dynamic aging)
assertEqual "Age should remain 0 for LFU" 0 (age cache2)
-- Key 1 should survive (higher frequency)
assertBool "Key 1 should still be in cache" (contains (1 :: Int) cache2)
testLFUFrequencyEviction :: Assertion
testLFUFrequencyEviction = do
let initialCache :: LfudaCache Int Int
initialCache = newLFU 3
-- Insert 3 elements
let cache1 = insert (1 :: Int) (10 :: Int) initialCache
let cache2 = insert (2 :: Int) (20 :: Int) cache1
let cache3 = insert (3 :: Int) (30 :: Int) cache2
-- Bump frequency of key 1 (3 extra lookups -> freq 4)
let cache3' = foldl' (\c _ ->
case lookup (1 :: Int) c of
Just (_, c') -> c'
Nothing -> c
) cache3 [1..3 :: Int]
-- Bump frequency of key 3 (1 extra lookup -> freq 2)
let cache3'' = case lookup (3 :: Int) cache3' of
Just (_, c) -> c
Nothing -> cache3'
-- Key 2 still has freq 1 (lowest), so inserting key 4 should evict key 2
let cache4 = insert (4 :: Int) (40 :: Int) cache3''
-- Key 2 should be gone (lowest frequency)
assertBool "Key 2 should be evicted" (not (contains (2 :: Int) cache4))
-- Keys 1 and 3 should still be present
assertBool "Key 1 should survive" (contains (1 :: Int) cache4)
assertBool "Key 3 should survive" (contains (3 :: Int) cache4)
-- Key 4 should be present
assertBool "Key 4 should be present" (contains (4 :: Int) cache4)
testLFUInsertView :: Assertion
testLFUInsertView = do
let cache :: LfudaCache Int Int
cache = newLFU 2
-- First insert returns Nothing (no eviction)
let (evicted1, cache1) = insertView (1 :: Int) (10 :: Int) cache
assertEqual "No eviction on first insert" Nothing evicted1
-- Second insert returns Nothing (still room)
let (evicted2, cache2) = insertView (2 :: Int) (20 :: Int) cache1
assertEqual "No eviction on second insert" Nothing evicted2
-- Third insert should evict the lowest frequency entry
let (evicted3, cache3) = insertView (3 :: Int) (30 :: Int) cache2
assertBool "Should have evicted something" (evicted3 /= Nothing)
assertEqual "Cache size should be 2" 2 (size cache3)
-- GDSF Tests
testGDSFSizeEviction :: Assertion
testGDSFSizeEviction = do
-- GDSF priority = frequency + age * size
-- All new entries have frequency 1 and size 1, so initially priority = 1 + 0*1 = 1
-- After eviction, age increases, making new entries have higher priority
let initialCache :: LfudaCache Int Int
initialCache = newGDSF 2
let cache1 = insert (1 :: Int) (10 :: Int) initialCache
let cache2 = insert (2 :: Int) (20 :: Int) cache1
-- Bump key 1 frequency to 2
let cache2' = case lookup (1 :: Int) cache2 of
Just (_, c) -> c
Nothing -> cache2
-- Insert key 3, should evict key 2 (freq 1 < key 1's freq 2)
let cache3 = insert (3 :: Int) (30 :: Int) cache2'
assertBool "Key 2 should be evicted" (not (contains (2 :: Int) cache3))
assertBool "Key 1 should survive" (contains (1 :: Int) cache3)
testGDSFAging :: Assertion
testGDSFAging = do
let initialCache :: LfudaCache Int Int
initialCache = newGDSF 1
-- Insert key 1, bump frequency to 3
let cache1 = insert (1 :: Int) (10 :: Int) initialCache
let cache1' = foldl' (\c _ ->
case lookup (1 :: Int) c of
Just (_, c') -> c'
Nothing -> c
) cache1 [1..2 :: Int]
assertEqual "Age should be 0 before eviction" 0 (age cache1')
-- Insert key 2, it will be evicted (freq 1 < key 1's freq 3)
let cache2 = insert (2 :: Int) (20 :: Int) cache1'
-- Age should now be 1 (frequency of the evicted key)
assertEqual "Age should be 1 after eviction" 1 (age cache2)
assertBool "Key 1 should survive" (contains (1 :: Int) cache2)
testGDSFFrequencyAndSize :: Assertion
testGDSFFrequencyAndSize = do
-- Test that GDSF uses both frequency and age*size in priority
let initialCache :: LfudaCache Int Int
initialCache = newGDSF 3
-- Insert 3 entries
let cache1 = insert (1 :: Int) (10 :: Int) initialCache
let cache2 = insert (2 :: Int) (20 :: Int) cache1
let cache3 = insert (3 :: Int) (30 :: Int) cache2
-- Bump key 2's frequency to 3
let cache3' = foldl' (\c _ ->
case lookup (2 :: Int) c of
Just (_, c') -> c'
Nothing -> c
) cache3 [1..2 :: Int]
-- Bump key 3's frequency to 2
let cache3'' = case lookup (3 :: Int) cache3' of
Just (_, c) -> c
Nothing -> cache3'
-- Key 1 has freq 1, Key 2 has freq 3, Key 3 has freq 2
-- Insert key 4, should evict key 1 (lowest frequency)
let cache4 = insert (4 :: Int) (40 :: Int) cache3''
assertBool "Key 1 should be evicted (lowest freq)" (not (contains (1 :: Int) cache4))
assertBool "Key 2 should survive" (contains (2 :: Int) cache4)
assertBool "Key 3 should survive" (contains (3 :: Int) cache4)
assertBool "Key 4 should be present" (contains (4 :: Int) cache4)
-- Verify age was updated after eviction
let ageAfter = age cache4
assertBool "Age should have increased" (ageAfter > 0)
-- Pure implementation of random number generation for testing
type SimpleRandom :: Type
data SimpleRandom :: Type where
SimpleRandom :: Int -> SimpleRandom
nextRandom :: SimpleRandom -> (Int, SimpleRandom)
nextRandom (SimpleRandom seed) =
let newSeed = (seed * 1103515245 + 12345) `mod` 2147483647
value = newSeed `mod` 32768
in (value, SimpleRandom newSeed)
rangeRandom :: Int -> Int -> SimpleRandom -> (Int, SimpleRandom)
rangeRandom low high rand =
let (val, rand') = nextRandom rand
scaled = low + (val `mod` (high - low + 1))
in (scaled, rand')
generateTrace :: Int -> SimpleRandom -> ([Int], SimpleRandom)
generateTrace n rand =
go n rand []
where
go :: Int -> SimpleRandom -> [Int] -> ([Int], SimpleRandom)
go 0 r acc = (reverse acc, r)
go i r acc =
let evenOdd = i `mod` 2 == 0
(val, r') = if evenOdd
then rangeRandom 0 16383 r
else rangeRandom 0 32767 r
in go (i-1) r' (val:acc)
-- Benchmark tests (simplified versions for HUnit)
testBenchmark :: String -> Assertion -> TestTree
testBenchmark name benchmark = testCase name benchmark
benchmarkLFUDA :: Assertion
benchmarkLFUDA = do
let initialCache :: LfudaCache Int Int
initialCache = newLFUDA 1000 -- Smaller cache size to ensure evictions
-- Generate deterministic random trace
let traceSeed = SimpleRandom 42
(trace, _) = generateTrace 2000 traceSeed
-- Split the trace: first half for setting, second half for getting
let setTrace = take 1000 trace
queryTrace = drop 1000 trace
-- Set operations
let cacheAfterSet = foldl' (\cache i ->
insert i i cache
) initialCache setTrace
-- Get operations and count hits/misses
let hitsAndMisses = foldl' (\(h, m) i ->
case lookup i cacheAfterSet of
Just _ -> (h + 1, m)
Nothing -> (h, m + 1)
) (0 :: Int, 0 :: Int) queryTrace
hits = fst hitsAndMisses
misses = snd hitsAndMisses
-- With our cache size of 1000 and different query items,
-- we should have both hits and misses
assertBool "Should have some hits" (hits > (0 :: Int))
assertBool "Should have some misses" (misses > (0 :: Int))
benchmarkLFUDARand :: Assertion
benchmarkLFUDARand = do
let initialCache :: LfudaCache Int Int
initialCache = newLFUDA 1000 -- Smaller cache size to ensure evictions
-- Generate deterministic random trace
let traceSeed = SimpleRandom 24
(trace, _) = generateTrace 2000 traceSeed
-- Process the trace in a pure way
let processItem :: (LfudaCache Int Int, Int, Int) -> (Int, Int) -> (LfudaCache Int Int, Int, Int)
processItem (cache, h, m) (idx, val) =
-- Set on even indices
let cache' = if even idx
then insert val val cache
else cache
-- Check get result
getResult = lookup val cache'
h' = case getResult of { Just _ -> h + 1; Nothing -> h }
m' = case getResult of { Just _ -> m; Nothing -> m + 1 }
-- Extra hits/misses for idx mod 7 = 0
extras =
if idx `mod` 7 == 0
then foldl' (\(eh, em) _ ->
if contains val cache'
then (eh + 1, em)
else (eh, em + 1)
) (0 :: Int, 0 :: Int) [1..19 :: Int]
else (0, 0)
-- Update cache if we got a hit
cache'' = case getResult of
Just (_, c) -> c
Nothing -> cache'
in (cache'', h' + fst extras, m' + snd extras)
let fullResult = foldl' processItem (initialCache, 0 :: Int, 0 :: Int) (zip [0 :: Int ..] trace)
-- Check results
case fullResult of
(_, hits, misses) -> do
assertBool "Should have some hits" (hits > (0 :: Int))
assertBool "Should have some misses" (misses > (0 :: Int))