nip/tests/test_cache_invalidation.nim

697 lines
17 KiB
Nim

## Cache Invalidation Strategy Tests
##
## This test suite verifies that the GlobalRepoStateHash correctly triggers
## cache invalidation when repository metadata changes. This is the keystone
## of the caching system's correctness.
##
## **Test Strategy:**
## - Verify hash changes on metadata modifications
## - Verify cache invalidation on hash changes
## - Verify cache remains valid when hash unchanged
## - Test various metadata change scenarios
import unittest
import tables
import ../src/nip/resolver/serialization
import ../src/nip/resolver/resolution_cache
import ../src/nip/resolver/types
import ../src/nip/cas/storage
suite "Global Repo State Hash Calculation":
test "Empty repositories produce deterministic hash":
let repos1: seq[Repository] = @[]
let repos2: seq[Repository] = @[]
let hash1 = calculateGlobalRepoStateHash(repos1)
let hash2 = calculateGlobalRepoStateHash(repos2)
check hash1 == hash2
check hash1.len == 32 # xxh3_128 produces 32-character hex string
test "Same repositories produce identical hash":
let repos1 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official", "arch": "x86_64"}.toTable
),
PackageMetadata(
name: "zlib",
version: "1.2.13",
metadata: {"source": "official"}.toTable
)
]
)
]
let repos2 = repos1 # Identical
let hash1 = calculateGlobalRepoStateHash(repos1)
let hash2 = calculateGlobalRepoStateHash(repos2)
check hash1 == hash2
test "Different package version produces different hash":
let repos1 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let repos2 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.1", # Different version
metadata: {"source": "official"}.toTable
)
]
)
]
let hash1 = calculateGlobalRepoStateHash(repos1)
let hash2 = calculateGlobalRepoStateHash(repos2)
check hash1 != hash2
test "Different package metadata produces different hash":
let repos1 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official", "arch": "x86_64"}.toTable
)
]
)
]
let repos2 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official", "arch": "aarch64"}.toTable # Different arch
)
]
)
]
let hash1 = calculateGlobalRepoStateHash(repos1)
let hash2 = calculateGlobalRepoStateHash(repos2)
check hash1 != hash2
test "Adding package produces different hash":
let repos1 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let repos2 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
),
PackageMetadata(
name: "apache",
version: "2.4.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let hash1 = calculateGlobalRepoStateHash(repos1)
let hash2 = calculateGlobalRepoStateHash(repos2)
check hash1 != hash2
test "Removing package produces different hash":
let repos1 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
),
PackageMetadata(
name: "apache",
version: "2.4.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let repos2 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let hash1 = calculateGlobalRepoStateHash(repos1)
let hash2 = calculateGlobalRepoStateHash(repos2)
check hash1 != hash2
test "Package order doesn't affect hash (sorted)":
let repos1 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "aaa",
version: "1.0",
metadata: initTable[string, string]()
),
PackageMetadata(
name: "zzz",
version: "1.0",
metadata: initTable[string, string]()
)
]
)
]
let repos2 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "zzz",
version: "1.0",
metadata: initTable[string, string]()
),
PackageMetadata(
name: "aaa",
version: "1.0",
metadata: initTable[string, string]()
)
]
)
]
let hash1 = calculateGlobalRepoStateHash(repos1)
let hash2 = calculateGlobalRepoStateHash(repos2)
# Should be identical because metadata hashes are sorted
check hash1 == hash2
test "Multiple repositories combined correctly":
let repos1 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
)
]
),
Repository(
name: "testing",
packages: @[
PackageMetadata(
name: "apache",
version: "2.4.0",
metadata: {"source": "testing"}.toTable
)
]
)
]
let repos2 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let hash1 = calculateGlobalRepoStateHash(repos1)
let hash2 = calculateGlobalRepoStateHash(repos2)
check hash1 != hash2
suite "Cache Invalidation on Metadata Changes":
test "Cache invalidated when package version changes":
let cas = newCASStorage("/tmp/test-cas-inv-1")
let cache = newResolutionCache(cas)
# Initial repository state
let repos1 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let repoHash1 = calculateGlobalRepoStateHash(repos1)
cache.updateRepoHash(repoHash1)
# Cache a resolution result
let key = CacheKey(
rootPackage: "nginx",
rootConstraint: ">=1.24.0",
repoStateHash: repoHash1,
variantDemand: VariantDemand(
useFlags: @[],
libc: "musl",
allocator: "jemalloc",
targetArch: "x86_64",
buildFlags: @[]
)
)
let graph = DependencyGraph(
rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
nodes: @[],
timestamp: 1700000000
)
cache.put(key, graph)
check cache.get(key).value.isSome
# Update repository (new package version)
let repos2 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.1", # Version changed
metadata: {"source": "official"}.toTable
)
]
)
]
let repoHash2 = calculateGlobalRepoStateHash(repos2)
check repoHash1 != repoHash2 # Hash should change
cache.updateRepoHash(repoHash2)
# Cache should be invalidated
check cache.get(key).value.isNone
test "Cache invalidated when package added":
let cas = newCASStorage("/tmp/test-cas-inv-2")
let cache = newResolutionCache(cas)
# Initial repository state
let repos1 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let repoHash1 = calculateGlobalRepoStateHash(repos1)
cache.updateRepoHash(repoHash1)
# Cache a resolution result
let key = CacheKey(
rootPackage: "nginx",
rootConstraint: ">=1.24.0",
repoStateHash: repoHash1,
variantDemand: VariantDemand(
useFlags: @[],
libc: "musl",
allocator: "jemalloc",
targetArch: "x86_64",
buildFlags: @[]
)
)
let graph = DependencyGraph(
rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
nodes: @[],
timestamp: 1700000000
)
cache.put(key, graph)
check cache.get(key).value.isSome
# Add new package to repository
let repos2 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
),
PackageMetadata(
name: "apache",
version: "2.4.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let repoHash2 = calculateGlobalRepoStateHash(repos2)
check repoHash1 != repoHash2 # Hash should change
cache.updateRepoHash(repoHash2)
# Cache should be invalidated
check cache.get(key).value.isNone
test "Cache invalidated when package metadata changes":
let cas = newCASStorage("/tmp/test-cas-inv-3")
let cache = newResolutionCache(cas)
# Initial repository state
let repos1 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official", "arch": "x86_64"}.toTable
)
]
)
]
let repoHash1 = calculateGlobalRepoStateHash(repos1)
cache.updateRepoHash(repoHash1)
# Cache a resolution result
let key = CacheKey(
rootPackage: "nginx",
rootConstraint: ">=1.24.0",
repoStateHash: repoHash1,
variantDemand: VariantDemand(
useFlags: @[],
libc: "musl",
allocator: "jemalloc",
targetArch: "x86_64",
buildFlags: @[]
)
)
let graph = DependencyGraph(
rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
nodes: @[],
timestamp: 1700000000
)
cache.put(key, graph)
check cache.get(key).value.isSome
# Update package metadata
let repos2 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official", "arch": "aarch64"}.toTable # Arch changed
)
]
)
]
let repoHash2 = calculateGlobalRepoStateHash(repos2)
check repoHash1 != repoHash2 # Hash should change
cache.updateRepoHash(repoHash2)
# Cache should be invalidated
check cache.get(key).value.isNone
test "Cache remains valid when repo state unchanged":
let cas = newCASStorage("/tmp/test-cas-inv-4")
let cache = newResolutionCache(cas)
# Initial repository state
let repos = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let repoHash = calculateGlobalRepoStateHash(repos)
cache.updateRepoHash(repoHash)
# Cache a resolution result
let key = CacheKey(
rootPackage: "nginx",
rootConstraint: ">=1.24.0",
repoStateHash: repoHash,
variantDemand: VariantDemand(
useFlags: @[],
libc: "musl",
allocator: "jemalloc",
targetArch: "x86_64",
buildFlags: @[]
)
)
let graph = DependencyGraph(
rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
nodes: @[],
timestamp: 1700000000
)
cache.put(key, graph)
check cache.get(key).value.isSome
# Update with same hash (no metadata change)
cache.updateRepoHash(repoHash)
# Cache should still be valid
check cache.get(key).value.isSome
check cache.get(key).source == L1Hit
suite "Cache Invalidation Edge Cases":
test "Multiple cached entries all invalidated":
let cas = newCASStorage("/tmp/test-cas-inv-5")
let cache = newResolutionCache(cas)
# Initial repository state
let repos1 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
),
PackageMetadata(
name: "apache",
version: "2.4.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let repoHash1 = calculateGlobalRepoStateHash(repos1)
cache.updateRepoHash(repoHash1)
# Cache multiple resolution results
let key1 = CacheKey(
rootPackage: "nginx",
rootConstraint: ">=1.24.0",
repoStateHash: repoHash1,
variantDemand: VariantDemand(
useFlags: @[],
libc: "musl",
allocator: "jemalloc",
targetArch: "x86_64",
buildFlags: @[]
)
)
let key2 = CacheKey(
rootPackage: "apache",
rootConstraint: ">=2.4.0",
repoStateHash: repoHash1,
variantDemand: VariantDemand(
useFlags: @[],
libc: "musl",
allocator: "jemalloc",
targetArch: "x86_64",
buildFlags: @[]
)
)
let graph1 = DependencyGraph(
rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
nodes: @[],
timestamp: 1700000000
)
let graph2 = DependencyGraph(
rootPackage: PackageId(name: "apache", version: "2.4.0", variant: "default"),
nodes: @[],
timestamp: 1700000000
)
cache.put(key1, graph1)
cache.put(key2, graph2)
check cache.get(key1).value.isSome
check cache.get(key2).value.isSome
# Update repository (change metadata)
let repos2 = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.1", # Version changed
metadata: {"source": "official"}.toTable
),
PackageMetadata(
name: "apache",
version: "2.4.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let repoHash2 = calculateGlobalRepoStateHash(repos2)
cache.updateRepoHash(repoHash2)
# All cached entries should be invalidated
check cache.get(key1).value.isNone
check cache.get(key2).value.isNone
test "Cache survives multiple updates with same hash":
let cas = newCASStorage("/tmp/test-cas-inv-6")
let cache = newResolutionCache(cas)
let repos = @[
Repository(
name: "main",
packages: @[
PackageMetadata(
name: "nginx",
version: "1.24.0",
metadata: {"source": "official"}.toTable
)
]
)
]
let repoHash = calculateGlobalRepoStateHash(repos)
cache.updateRepoHash(repoHash)
let key = CacheKey(
rootPackage: "nginx",
rootConstraint: ">=1.24.0",
repoStateHash: repoHash,
variantDemand: VariantDemand(
useFlags: @[],
libc: "musl",
allocator: "jemalloc",
targetArch: "x86_64",
buildFlags: @[]
)
)
let graph = DependencyGraph(
rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
nodes: @[],
timestamp: 1700000000
)
cache.put(key, graph)
# Multiple updates with same hash
for i in 0..<10:
cache.updateRepoHash(repoHash)
check cache.get(key).value.isSome
test "Empty repository hash is deterministic":
let repos1: seq[Repository] = @[]
let repos2: seq[Repository] = @[]
let hash1 = calculateGlobalRepoStateHash(repos1)
let hash2 = calculateGlobalRepoStateHash(repos2)
check hash1 == hash2
let cas = newCASStorage("/tmp/test-cas-inv-7")
let cache = newResolutionCache(cas)
cache.updateRepoHash(hash1)
cache.updateRepoHash(hash2)
# Should not trigger invalidation (same hash)
let metrics = cache.getMetrics()
check metrics.l1Size == 0 # No entries cached yet