517 lines
14 KiB
Nim
517 lines
14 KiB
Nim
## Tests for Binary Serialization and Cache Key Calculation
|
|
##
|
|
## This test suite verifies:
|
|
## - Deterministic MessagePack serialization
|
|
## - Correct deserialization (round-trip)
|
|
## - Cache key determinism
|
|
## - Cache invalidation on metadata changes
|
|
|
|
import unittest
|
|
import tables
|
|
import ../src/nip/resolver/serialization
|
|
import ../src/nip/resolver/types
|
|
|
|
suite "MessagePack Serialization":
|
|
test "Empty graph serialization":
|
|
let graph = DependencyGraph(
|
|
rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
|
|
nodes: @[],
|
|
timestamp: 1234567890
|
|
)
|
|
|
|
let binary = toMessagePack(graph)
|
|
let reconstructed = fromMessagePack(binary)
|
|
|
|
check reconstructed.rootPackage.name == "test"
|
|
check reconstructed.rootPackage.version == "1.0"
|
|
check reconstructed.rootPackage.variant == "default"
|
|
check reconstructed.nodes.len == 0
|
|
check reconstructed.timestamp == 1234567890
|
|
|
|
test "Single node graph serialization":
|
|
let graph = DependencyGraph(
|
|
rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "ssl"),
|
|
nodes: @[
|
|
DependencyNode(
|
|
packageId: PackageId(name: "nginx", version: "1.24.0", variant: "ssl"),
|
|
dependencies: @[],
|
|
buildHash: "xxh3-abc123",
|
|
metadata: {"source": "official"}.toTable
|
|
)
|
|
],
|
|
timestamp: 1700000000
|
|
)
|
|
|
|
let binary = toMessagePack(graph)
|
|
let reconstructed = fromMessagePack(binary)
|
|
|
|
check reconstructed.nodes.len == 1
|
|
check reconstructed.nodes[0].packageId.name == "nginx"
|
|
check reconstructed.nodes[0].buildHash == "xxh3-abc123"
|
|
check reconstructed.nodes[0].metadata["source"] == "official"
|
|
|
|
test "Complex graph with dependencies":
|
|
let graph = DependencyGraph(
|
|
rootPackage: PackageId(name: "app", version: "1.0", variant: "default"),
|
|
nodes: @[
|
|
DependencyNode(
|
|
packageId: PackageId(name: "app", version: "1.0", variant: "default"),
|
|
dependencies: @[
|
|
PackageId(name: "libssl", version: "3.0", variant: "default"),
|
|
PackageId(name: "zlib", version: "1.2.13", variant: "default")
|
|
],
|
|
buildHash: "xxh3-app123",
|
|
metadata: {"type": "application"}.toTable
|
|
),
|
|
DependencyNode(
|
|
packageId: PackageId(name: "libssl", version: "3.0", variant: "default"),
|
|
dependencies: @[],
|
|
buildHash: "xxh3-ssl456",
|
|
metadata: {"type": "library"}.toTable
|
|
),
|
|
DependencyNode(
|
|
packageId: PackageId(name: "zlib", version: "1.2.13", variant: "default"),
|
|
dependencies: @[],
|
|
buildHash: "xxh3-zlib789",
|
|
metadata: {"type": "library"}.toTable
|
|
)
|
|
],
|
|
timestamp: 1700000000
|
|
)
|
|
|
|
let binary = toMessagePack(graph)
|
|
let reconstructed = fromMessagePack(binary)
|
|
|
|
check reconstructed.nodes.len == 3
|
|
check reconstructed.nodes[0].dependencies.len == 2
|
|
|
|
# Verify dependencies are preserved
|
|
let appNode = reconstructed.nodes[0]
|
|
check appNode.dependencies[0].name in ["libssl", "zlib"]
|
|
check appNode.dependencies[1].name in ["libssl", "zlib"]
|
|
|
|
suite "Serialization Determinism":
|
|
test "Same graph produces identical binary":
|
|
let graph1 = DependencyGraph(
|
|
rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
|
|
nodes: @[
|
|
DependencyNode(
|
|
packageId: PackageId(name: "dep1", version: "2.0", variant: "default"),
|
|
dependencies: @[],
|
|
buildHash: "hash1",
|
|
metadata: {"key": "value"}.toTable
|
|
)
|
|
],
|
|
timestamp: 1234567890
|
|
)
|
|
|
|
let graph2 = graph1 # Identical graph
|
|
|
|
let binary1 = toMessagePack(graph1)
|
|
let binary2 = toMessagePack(graph2)
|
|
|
|
check binary1 == binary2
|
|
|
|
test "Node order doesn't affect binary (sorted)":
|
|
let graph1 = DependencyGraph(
|
|
rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
|
|
nodes: @[
|
|
DependencyNode(
|
|
packageId: PackageId(name: "aaa", version: "1.0", variant: "default"),
|
|
dependencies: @[],
|
|
buildHash: "hash1",
|
|
metadata: initTable[string, string]()
|
|
),
|
|
DependencyNode(
|
|
packageId: PackageId(name: "zzz", version: "1.0", variant: "default"),
|
|
dependencies: @[],
|
|
buildHash: "hash2",
|
|
metadata: initTable[string, string]()
|
|
)
|
|
],
|
|
timestamp: 1234567890
|
|
)
|
|
|
|
let graph2 = DependencyGraph(
|
|
rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
|
|
nodes: @[
|
|
DependencyNode(
|
|
packageId: PackageId(name: "zzz", version: "1.0", variant: "default"),
|
|
dependencies: @[],
|
|
buildHash: "hash2",
|
|
metadata: initTable[string, string]()
|
|
),
|
|
DependencyNode(
|
|
packageId: PackageId(name: "aaa", version: "1.0", variant: "default"),
|
|
dependencies: @[],
|
|
buildHash: "hash1",
|
|
metadata: initTable[string, string]()
|
|
)
|
|
],
|
|
timestamp: 1234567890
|
|
)
|
|
|
|
let binary1 = toMessagePack(graph1)
|
|
let binary2 = toMessagePack(graph2)
|
|
|
|
# Should be identical because nodes are sorted by packageId
|
|
check binary1 == binary2
|
|
|
|
test "Dependency order doesn't affect binary (sorted)":
|
|
let graph1 = DependencyGraph(
|
|
rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
|
|
nodes: @[
|
|
DependencyNode(
|
|
packageId: PackageId(name: "app", version: "1.0", variant: "default"),
|
|
dependencies: @[
|
|
PackageId(name: "aaa", version: "1.0", variant: "default"),
|
|
PackageId(name: "zzz", version: "1.0", variant: "default")
|
|
],
|
|
buildHash: "hash1",
|
|
metadata: initTable[string, string]()
|
|
)
|
|
],
|
|
timestamp: 1234567890
|
|
)
|
|
|
|
let graph2 = DependencyGraph(
|
|
rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
|
|
nodes: @[
|
|
DependencyNode(
|
|
packageId: PackageId(name: "app", version: "1.0", variant: "default"),
|
|
dependencies: @[
|
|
PackageId(name: "zzz", version: "1.0", variant: "default"),
|
|
PackageId(name: "aaa", version: "1.0", variant: "default")
|
|
],
|
|
buildHash: "hash1",
|
|
metadata: initTable[string, string]()
|
|
)
|
|
],
|
|
timestamp: 1234567890
|
|
)
|
|
|
|
let binary1 = toMessagePack(graph1)
|
|
let binary2 = toMessagePack(graph2)
|
|
|
|
# Should be identical because dependencies are sorted
|
|
check binary1 == binary2
|
|
|
|
suite "Cache Key Calculation":
|
|
test "Cache key is deterministic":
|
|
let key1 = CacheKey(
|
|
rootPackage: "nginx",
|
|
rootConstraint: ">=1.24.0",
|
|
repoStateHash: "repo-hash-123",
|
|
variantDemand: VariantDemand(
|
|
useFlags: @["ssl", "http2"],
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @["-O2", "-march=native"]
|
|
)
|
|
)
|
|
|
|
let key2 = key1 # Identical key
|
|
|
|
let hash1 = calculateCacheKey(key1)
|
|
let hash2 = calculateCacheKey(key2)
|
|
|
|
check hash1 == hash2
|
|
check hash1.len == 32 # xxh3_128 produces 32-character hex string
|
|
|
|
test "Different packages produce different keys":
|
|
let key1 = CacheKey(
|
|
rootPackage: "nginx",
|
|
rootConstraint: ">=1.24.0",
|
|
repoStateHash: "repo-hash-123",
|
|
variantDemand: VariantDemand(
|
|
useFlags: @[],
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @[]
|
|
)
|
|
)
|
|
|
|
let key2 = CacheKey(
|
|
rootPackage: "apache", # Different package
|
|
rootConstraint: ">=2.4.0",
|
|
repoStateHash: "repo-hash-123",
|
|
variantDemand: VariantDemand(
|
|
useFlags: @[],
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @[]
|
|
)
|
|
)
|
|
|
|
let hash1 = calculateCacheKey(key1)
|
|
let hash2 = calculateCacheKey(key2)
|
|
|
|
check hash1 != hash2
|
|
|
|
test "Different USE flags produce different keys":
|
|
let key1 = CacheKey(
|
|
rootPackage: "nginx",
|
|
rootConstraint: ">=1.24.0",
|
|
repoStateHash: "repo-hash-123",
|
|
variantDemand: VariantDemand(
|
|
useFlags: @["ssl"],
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @[]
|
|
)
|
|
)
|
|
|
|
let key2 = CacheKey(
|
|
rootPackage: "nginx",
|
|
rootConstraint: ">=1.24.0",
|
|
repoStateHash: "repo-hash-123",
|
|
variantDemand: VariantDemand(
|
|
useFlags: @["ssl", "http2"], # Different USE flags
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @[]
|
|
)
|
|
)
|
|
|
|
let hash1 = calculateCacheKey(key1)
|
|
let hash2 = calculateCacheKey(key2)
|
|
|
|
check hash1 != hash2
|
|
|
|
test "USE flag order doesn't affect key (sorted)":
|
|
let key1 = CacheKey(
|
|
rootPackage: "nginx",
|
|
rootConstraint: ">=1.24.0",
|
|
repoStateHash: "repo-hash-123",
|
|
variantDemand: VariantDemand(
|
|
useFlags: @["ssl", "http2", "brotli"],
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @[]
|
|
)
|
|
)
|
|
|
|
let key2 = CacheKey(
|
|
rootPackage: "nginx",
|
|
rootConstraint: ">=1.24.0",
|
|
repoStateHash: "repo-hash-123",
|
|
variantDemand: VariantDemand(
|
|
useFlags: @["brotli", "http2", "ssl"], # Different order
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @[]
|
|
)
|
|
)
|
|
|
|
let hash1 = calculateCacheKey(key1)
|
|
let hash2 = calculateCacheKey(key2)
|
|
|
|
# Should be identical because USE flags are sorted
|
|
check hash1 == hash2
|
|
|
|
test "Different repo state produces different keys":
|
|
let key1 = CacheKey(
|
|
rootPackage: "nginx",
|
|
rootConstraint: ">=1.24.0",
|
|
repoStateHash: "repo-hash-123",
|
|
variantDemand: VariantDemand(
|
|
useFlags: @[],
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @[]
|
|
)
|
|
)
|
|
|
|
let key2 = CacheKey(
|
|
rootPackage: "nginx",
|
|
rootConstraint: ">=1.24.0",
|
|
repoStateHash: "repo-hash-456", # Different repo state
|
|
variantDemand: VariantDemand(
|
|
useFlags: @[],
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @[]
|
|
)
|
|
)
|
|
|
|
let hash1 = calculateCacheKey(key1)
|
|
let hash2 = calculateCacheKey(key2)
|
|
|
|
check hash1 != hash2
|
|
|
|
suite "Global Repo State Hash":
|
|
test "Empty repositories produce deterministic hash":
|
|
let repos: seq[Repository] = @[]
|
|
|
|
let hash1 = calculateGlobalRepoStateHash(repos)
|
|
let hash2 = calculateGlobalRepoStateHash(repos)
|
|
|
|
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"}.toTable
|
|
)
|
|
]
|
|
)
|
|
]
|
|
|
|
let repos2 = repos1 # Identical
|
|
|
|
let hash1 = calculateGlobalRepoStateHash(repos1)
|
|
let hash2 = calculateGlobalRepoStateHash(repos2)
|
|
|
|
check hash1 == hash2
|
|
|
|
test "Different metadata 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 "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
|
|
|
|
suite "Variant Demand Canonicalization":
|
|
test "Canonical form is deterministic":
|
|
let demand1 = VariantDemand(
|
|
useFlags: @["ssl", "http2"],
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @["-O2", "-march=native"]
|
|
)
|
|
|
|
let demand2 = demand1 # Identical
|
|
|
|
let canon1 = canonicalizeVariantDemand(demand1)
|
|
let canon2 = canonicalizeVariantDemand(demand2)
|
|
|
|
check canon1 == canon2
|
|
|
|
test "USE flag order doesn't affect canonical form":
|
|
let demand1 = VariantDemand(
|
|
useFlags: @["ssl", "http2", "brotli"],
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @[]
|
|
)
|
|
|
|
let demand2 = VariantDemand(
|
|
useFlags: @["brotli", "http2", "ssl"], # Different order
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @[]
|
|
)
|
|
|
|
let canon1 = canonicalizeVariantDemand(demand1)
|
|
let canon2 = canonicalizeVariantDemand(demand2)
|
|
|
|
check canon1 == canon2
|
|
|
|
test "Build flag order doesn't affect canonical form":
|
|
let demand1 = VariantDemand(
|
|
useFlags: @[],
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @["-O2", "-march=native", "-flto"]
|
|
)
|
|
|
|
let demand2 = VariantDemand(
|
|
useFlags: @[],
|
|
libc: "musl",
|
|
allocator: "jemalloc",
|
|
targetArch: "x86_64",
|
|
buildFlags: @["-flto", "-march=native", "-O2"] # Different order
|
|
)
|
|
|
|
let canon1 = canonicalizeVariantDemand(demand1)
|
|
let canon2 = canonicalizeVariantDemand(demand2)
|
|
|
|
check canon1 == canon2
|