697 lines
17 KiB
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
|