nip/tests/test_serialization.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