## Comprehensive Tests for LRU Cache ## ## This test suite verifies: ## - Basic cache operations (get, put, delete, clear) ## - LRU eviction policy correctness ## - Cache statistics tracking ## - Thread-safe operations ## - Edge cases and boundary conditions import unittest import options import ../src/nip/resolver/lru_cache suite "LRU Cache Construction": test "Create cache with valid capacity": let cache = newLRUCache[string, int](capacity = 10) check cache.len == 0 check cache.capacity == 10 check not cache.isFull test "Create cache with capacity 1": let cache = newLRUCache[string, int](capacity = 1) check cache.capacity == 1 test "Create thread-safe cache": let cache = newLRUCache[string, int](capacity = 10, threadSafe = true) check cache.capacity == 10 suite "Basic Cache Operations": test "Put and get single entry": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) let value = cache.get("key1") check value.isSome check value.get == 100 check cache.len == 1 test "Put multiple entries": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) check cache.len == 3 check cache.get("key1").get == 100 check cache.get("key2").get == 200 check cache.get("key3").get == 300 test "Get non-existent key returns None": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) let value = cache.get("missing") check value.isNone test "Update existing key": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) cache.put("key1", 200) check cache.len == 1 check cache.get("key1").get == 200 test "Update existing key multiple times": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) cache.put("key1", 200) cache.put("key1", 300) check cache.len == 1 check cache.get("key1").get == 300 test "Contains check": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) cache.put("key2", 200) check "key1" in cache check "key2" in cache check "missing" notin cache test "Delete existing entry": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) cache.put("key2", 200) check cache.delete("key1") check cache.len == 1 check "key1" notin cache check "key2" in cache test "Delete non-existent entry": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) check not cache.delete("missing") check cache.len == 1 test "Clear empty cache": let cache = newLRUCache[string, int](capacity = 5) cache.clear() check cache.len == 0 test "Clear non-empty cache": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) cache.clear() check cache.len == 0 check "key1" notin cache check "key2" notin cache check "key3" notin cache suite "LRU Eviction Policy": test "No eviction when under capacity": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) check cache.len == 3 check "key1" in cache check "key2" in cache check "key3" in cache test "Evict least recently used when at capacity": let cache = newLRUCache[string, int](capacity = 3) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) cache.put("key4", 400) # Should evict key1 check cache.len == 3 check "key1" notin cache check "key2" in cache check "key3" in cache check "key4" in cache test "Evict multiple entries when adding multiple": let cache = newLRUCache[string, int](capacity = 3) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) cache.put("key4", 400) # Evict key1 cache.put("key5", 500) # Evict key2 check cache.len == 3 check "key1" notin cache check "key2" notin cache check "key3" in cache check "key4" in cache check "key5" in cache test "Access updates LRU order": let cache = newLRUCache[string, int](capacity = 3) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) # Access key1 to make it most recently used discard cache.get("key1") # Add key4, should evict key2 (least recently used) cache.put("key4", 400) check "key1" in cache check "key2" notin cache check "key3" in cache check "key4" in cache test "Multiple accesses update LRU order": let cache = newLRUCache[string, int](capacity = 3) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) # Access key1 and key2 discard cache.get("key1") discard cache.get("key2") # Add key4, should evict key3 (least recently used) cache.put("key4", 400) check "key1" in cache check "key2" in cache check "key3" notin cache check "key4" in cache test "Update preserves entry and updates LRU": let cache = newLRUCache[string, int](capacity = 3) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) # Update key1 (makes it most recently used) cache.put("key1", 150) # Add key4, should evict key2 cache.put("key4", 400) check "key1" in cache check cache.get("key1").get == 150 check "key2" notin cache check "key3" in cache check "key4" in cache test "Capacity 1 cache evicts immediately": let cache = newLRUCache[string, int](capacity = 1) cache.put("key1", 100) cache.put("key2", 200) check cache.len == 1 check "key1" notin cache check "key2" in cache test "Capacity 1 cache with updates": let cache = newLRUCache[string, int](capacity = 1) cache.put("key1", 100) cache.put("key1", 200) check cache.len == 1 check cache.get("key1").get == 200 suite "Cache Statistics": test "Track hits": let cache = newLRUCacheWithStats[string, int](capacity = 5) cache.put("key1", 100) cache.put("key2", 200) discard cache.get("key1") discard cache.get("key2") discard cache.get("key1") let stats = cache.getStats() check stats.hits == 3 check stats.misses == 0 test "Track misses": let cache = newLRUCacheWithStats[string, int](capacity = 5) cache.put("key1", 100) discard cache.get("missing1") discard cache.get("missing2") discard cache.get("key1") let stats = cache.getStats() check stats.hits == 1 check stats.misses == 2 test "Track hits and misses": let cache = newLRUCacheWithStats[string, int](capacity = 5) cache.put("key1", 100) discard cache.get("key1") # Hit discard cache.get("missing") # Miss discard cache.get("key1") # Hit discard cache.get("missing") # Miss let stats = cache.getStats() check stats.hits == 2 check stats.misses == 2 check cache.hitRate() == 0.5 test "Track evictions": let cache = newLRUCacheWithStats[string, int](capacity = 2) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) # Eviction cache.put("key4", 400) # Eviction let stats = cache.getStats() check stats.evictions == 2 test "No eviction on update": let cache = newLRUCacheWithStats[string, int](capacity = 2) cache.put("key1", 100) cache.put("key2", 200) cache.put("key1", 150) # Update, not eviction let stats = cache.getStats() check stats.evictions == 0 test "Hit rate calculation": let cache = newLRUCacheWithStats[string, int](capacity = 5) cache.put("key1", 100) discard cache.get("key1") # Hit discard cache.get("key1") # Hit discard cache.get("key1") # Hit discard cache.get("missing") # Miss check cache.hitRate() == 0.75 test "Hit rate with no accesses": let cache = newLRUCacheWithStats[string, int](capacity = 5) check cache.hitRate() == 0.0 test "Reset statistics": let cache = newLRUCacheWithStats[string, int](capacity = 5) cache.put("key1", 100) discard cache.get("key1") discard cache.get("missing") cache.resetStats() let stats = cache.getStats() check stats.hits == 0 check stats.misses == 0 check stats.evictions == 0 test "Statistics after clear": let cache = newLRUCacheWithStats[string, int](capacity = 5) cache.put("key1", 100) cache.put("key2", 200) discard cache.get("key1") let statsBefore = cache.getStats() check statsBefore.size == 2 cache.clear() let statsAfter = cache.getStats() check statsAfter.size == 0 check statsAfter.hits == statsBefore.hits # Stats not reset suite "Iteration": test "Iterate over empty cache": let cache = newLRUCache[string, int](capacity = 5) var count = 0 for (key, value) in cache.items: count += 1 check count == 0 test "Iterate over entries": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) var count = 0 var sum = 0 for (key, value) in cache.items: count += 1 sum += value check count == 3 check sum == 600 test "Iterate in LRU order": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) var keys: seq[string] for (key, value) in cache.itemsLRU: keys.add(key) # Most recent first (key3), least recent last (key1) check keys.len == 3 check keys[0] == "key3" check keys[1] == "key2" check keys[2] == "key1" test "LRU order after access": let cache = newLRUCache[string, int](capacity = 5) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) # Access key1 to make it most recently used discard cache.get("key1") var keys: seq[string] for (key, value) in cache.itemsLRU: keys.add(key) # key1 should now be first (most recent) check keys[0] == "key1" test "Iteration doesn't affect LRU order": let cache = newLRUCache[string, int](capacity = 3) cache.put("key1", 100) cache.put("key2", 200) cache.put("key3", 300) # Iterate (shouldn't affect order) for (key, value) in cache.items: discard # Add key4, should still evict key1 cache.put("key4", 400) check "key1" notin cache suite "Edge Cases": test "Empty cache operations": let cache = newLRUCache[string, int](capacity = 5) check cache.len == 0 check cache.get("missing").isNone check "missing" notin cache check not cache.delete("missing") test "Single entry cache": let cache = newLRUCache[string, int](capacity = 1) cache.put("key1", 100) check cache.len == 1 check cache.isFull check cache.get("key1").get == 100 test "Large capacity cache": let cache = newLRUCache[string, int](capacity = 1000) for i in 0..<500: cache.put("key" & $i, i) check cache.len == 500 check not cache.isFull test "Fill cache to exact capacity": let cache = newLRUCache[string, int](capacity = 5) for i in 0..<5: cache.put("key" & $i, i) check cache.len == 5 check cache.isFull test "Repeatedly fill and clear": let cache = newLRUCache[string, int](capacity = 3) for round in 0..<3: for i in 0..<3: cache.put("key" & $i, i) check cache.len == 3 cache.clear() check cache.len == 0 suite "Complex Scenarios": test "Interleaved puts and gets": let cache = newLRUCache[string, int](capacity = 3) cache.put("key1", 100) discard cache.get("key1") cache.put("key2", 200) discard cache.get("key1") cache.put("key3", 300) discard cache.get("key2") cache.put("key4", 400) # Should evict key3 check "key1" in cache check "key2" in cache check "key3" notin cache check "key4" in cache test "Alternating updates and new entries": let cache = newLRUCache[string, int](capacity = 3) cache.put("key1", 100) cache.put("key2", 200) cache.put("key1", 150) # Update cache.put("key3", 300) cache.put("key2", 250) # Update cache.put("key4", 400) # Should evict key3 check "key1" in cache check "key2" in cache check "key3" notin cache check "key4" in cache test "Delete and re-add": let cache = newLRUCache[string, int](capacity = 3) cache.put("key1", 100) cache.put("key2", 200) discard cache.delete("key1") cache.put("key1", 150) # Re-add cache.put("key3", 300) cache.put("key4", 400) # Should evict key2 check "key1" in cache check "key2" notin cache check "key3" in cache check "key4" in cache