nip/tests/test_manifest_hash_determin...

374 lines
12 KiB
Nim

## Property-Based Test: Manifest Hash Determinism
##
## **Feature:** 01-nip-unified-storage-and-formats
## **Property 9:** Manifest Hash Determinism
## **Validates:** Requirements 6.4, 7.5
##
## **Property Statement:**
## For any manifest, calculating the hash twice SHALL produce identical results
##
## **Test Strategy:**
## 1. Generate random manifests with valid data (all three formats: NPK, NIP, NEXTER)
## 2. Calculate hash twice for each manifest
## 3. Verify hashes are identical (determinism)
## 4. Verify hash format is valid (xxh3-<hex>)
## 5. Verify different manifests produce different hashes (collision resistance)
## 6. Verify field order doesn't affect hash (sorted internally)
import std/[unittest, times, options, random, strutils, sets]
import nip/manifest_parser
# ============================================================================
# Test Generators
# ============================================================================
proc genSemanticVersion(): SemanticVersion =
## Generate random semantic version
SemanticVersion(
major: rand(0..10),
minor: rand(0..20),
patch: rand(0..50),
prerelease: if rand(1) == 0: "alpha" & $rand(10) else: "",
build: if rand(1) == 0: "build" & $rand(100) else: ""
)
proc genDependencySpec(): DependencySpec =
## Generate random dependency specification
DependencySpec(
name: "dep-" & $rand(1000),
versionConstraint: VersionConstraint(
operator: [OpExact, OpGreater, OpGreaterEq, OpTilde, OpCaret][rand(4)],
version: genSemanticVersion()
),
optional: rand(1) == 0,
features: if rand(1) == 0: @["feature" & $rand(5)] else: @[]
)
proc genPackageManifest(format: FormatType): PackageManifest =
## Generate random package manifest
var manifest = PackageManifest(
format: format,
name: "package-" & $rand(1000),
version: genSemanticVersion(),
license: ["MIT", "GPL-3.0", "Apache-2.0", "BSD-3-Clause"][rand(3)]
)
# Optional fields (randomly include)
if rand(1) == 0:
manifest.description = some("Description " & $rand(100))
if rand(1) == 0:
manifest.homepage = some("https://example.com/" & $rand(100))
if rand(1) == 0:
manifest.author = some("Author " & $rand(50))
if rand(1) == 0:
manifest.timestamp = some($now())
# Dependencies (random count)
let depCount = rand(0..5)
for i in 0..<depCount:
manifest.dependencies.add(genDependencySpec())
# Build dependencies
let buildDepCount = rand(0..3)
for i in 0..<buildDepCount:
manifest.buildDependencies.add(genDependencySpec())
# Optional dependencies
let optDepCount = rand(0..3)
for i in 0..<optDepCount:
var dep = genDependencySpec()
dep.optional = true
manifest.optionalDependencies.add(dep)
# Build configuration
if rand(1) == 0:
manifest.buildSystem = some(["cmake", "meson", "autotools"][rand(2)])
let flagCount = rand(0..5)
for i in 0..<flagCount:
manifest.buildFlags.add("--flag" & $rand(10))
let configCount = rand(0..5)
for i in 0..<configCount:
manifest.configureFlags.add("--with-" & $rand(10))
# Runtime configuration
if rand(1) == 0:
manifest.libc = some(["musl", "glibc"][rand(1)])
if rand(1) == 0:
manifest.allocator = some(["jemalloc", "tcmalloc", "default"][rand(2)])
# Platform constraints
let osCount = rand(1..3)
for i in 0..<osCount:
let os = ["linux", "freebsd", "openbsd", "netbsd"][rand(3)]
if os notin manifest.supportedOS:
manifest.supportedOS.add(os)
let archCount = rand(1..3)
for i in 0..<archCount:
let arch = ["x86_64", "aarch64", "riscv64"][rand(2)]
if arch notin manifest.supportedArchitectures:
manifest.supportedArchitectures.add(arch)
let capCount = rand(0..3)
for i in 0..<capCount:
manifest.requiredCapabilities.add("cap-" & $rand(10))
# Integrity hashes
manifest.buildHash = "xxh3-" & $rand(high(int))
manifest.sourceHash = "xxh3-" & $rand(high(int))
manifest.artifactHash = "xxh3-" & $rand(high(int))
# Metadata
let tagCount = rand(0..5)
for i in 0..<tagCount:
manifest.tags.add("tag" & $rand(20))
let maintainerCount = rand(0..3)
for i in 0..<maintainerCount:
manifest.maintainers.add("maintainer" & $rand(50))
# UTCP support
if rand(1) == 0:
manifest.utcpEndpoint = some("https://utcp.example.com/" & $rand(100))
if rand(1) == 0:
manifest.utcpVersion = some("1.0." & $rand(10))
return manifest
# ============================================================================
# Property-Based Tests
# ============================================================================
suite "Manifest Hash Determinism Property Tests":
test "Property 9: Hash Determinism - Same manifest produces same hash":
## **Feature:** 01-nip-unified-storage-and-formats, Property 9: Manifest Hash Determinism
## **Validates:** Requirements 6.4, 7.5
##
## For any manifest, calculating the hash twice should produce identical results.
var passCount = 0
var failCount = 0
var errors: seq[string] = @[]
# Test all three format types
for format in [NPK, NIP, NEXTER]:
for i in 0..<33: # 33 iterations per format = 99 total (close to 100)
try:
# Generate random manifest
let manifest = genPackageManifest(format)
# Calculate hash twice
let hash1 = calculateManifestHash(manifest)
let hash2 = calculateManifestHash(manifest)
# Verify they're identical
if hash1 == hash2:
passCount.inc()
else:
failCount.inc()
errors.add($format & " iteration " & $i & ": Hashes differ: " & hash1 & " vs " & hash2)
except CatchableError as e:
failCount.inc()
errors.add($format & " iteration " & $i & ": " & e.msg)
# Report results
echo "\nProperty Test Results (Determinism):"
echo " Passed: ", passCount, "/99"
echo " Failed: ", failCount, "/99"
if errors.len > 0:
echo "\nFirst 5 errors:"
for i in 0..<min(5, errors.len):
echo " ", errors[i]
# Property should hold for all cases
check passCount == 99
test "Property 9: Hash Format - Valid xxh3 format":
## Verify that calculated hashes have valid xxh3 format
var passCount = 0
var failCount = 0
for format in [NPK, NIP, NEXTER]:
for i in 0..<33:
try:
let manifest = genPackageManifest(format)
let hash = calculateManifestHash(manifest)
# Check format
if hash.startsWith("xxh3-") and hash.len > 5:
passCount.inc()
else:
failCount.inc()
echo $format, " iteration ", i, ": Invalid hash format: ", hash
except CatchableError as e:
failCount.inc()
echo $format, " iteration ", i, ": ", e.msg
echo "\nHash Format Test Results:"
echo " Passed: ", passCount, "/99"
echo " Failed: ", failCount, "/99"
check passCount == 99
test "Property 9: Collision Resistance - Different manifests produce different hashes":
## Verify that different manifests produce different hashes
## (collision resistance property)
var hashes: HashSet[string]
var collisionCount = 0
var totalCount = 0
for format in [NPK, NIP, NEXTER]:
for i in 0..<33:
let manifest = genPackageManifest(format)
let hash = calculateManifestHash(manifest)
if hash in hashes:
collisionCount.inc()
echo "Collision detected: ", hash
else:
hashes.incl(hash)
totalCount.inc()
echo "\nCollision Resistance Test Results:"
echo " Total manifests: ", totalCount
echo " Unique hashes: ", hashes.len
echo " Collisions: ", collisionCount
# With 99 random manifests, we should have 99 unique hashes
# (collision probability with xxh3-128 is < 2^-100)
check collisionCount == 0
check hashes.len == totalCount
test "Property 9: Field Order Independence - Sorted fields produce consistent hash":
## Verify that field order doesn't affect hash
## (internal sorting ensures determinism)
var passCount = 0
var failCount = 0
for i in 0..<100:
try:
# Create manifest with dependencies in random order
var manifest1 = genPackageManifest(NPK)
manifest1.dependencies = @[
DependencySpec(name: "dep-c", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]),
DependencySpec(name: "dep-a", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]),
DependencySpec(name: "dep-b", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[])
]
# Create same manifest with dependencies in different order
var manifest2 = manifest1
manifest2.dependencies = @[
DependencySpec(name: "dep-a", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]),
DependencySpec(name: "dep-b", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]),
DependencySpec(name: "dep-c", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[])
]
# Hashes should be identical (sorted internally)
let hash1 = calculateManifestHash(manifest1)
let hash2 = calculateManifestHash(manifest2)
if hash1 == hash2:
passCount.inc()
else:
failCount.inc()
echo "Iteration ", i, ": Field order affected hash"
except CatchableError as e:
failCount.inc()
echo "Iteration ", i, ": ", e.msg
echo "\nField Order Independence Test Results:"
echo " Passed: ", passCount, "/100"
echo " Failed: ", failCount, "/100"
check passCount == 100
test "Property 9: Hash Verification - verifyManifestHash works correctly":
## Test the hash verification function
var passCount = 0
var failCount = 0
for format in [NPK, NIP, NEXTER]:
for i in 0..<33:
try:
let manifest = genPackageManifest(format)
let hash = calculateManifestHash(manifest)
# Verify with correct hash
if verifyManifestHash(manifest, hash):
passCount.inc()
else:
failCount.inc()
echo $format, " iteration ", i, ": Verification failed for correct hash"
# Verify with incorrect hash (should fail)
let wrongHash = "xxh3-wrong-hash"
if not verifyManifestHash(manifest, wrongHash):
passCount.inc()
else:
failCount.inc()
echo $format, " iteration ", i, ": Verification passed for wrong hash"
except CatchableError as e:
failCount.inc()
echo $format, " iteration ", i, ": ", e.msg
echo "\nHash Verification Test Results:"
echo " Passed: ", passCount, "/198" # 99 correct + 99 incorrect
echo " Failed: ", failCount, "/198"
check passCount == 198
test "Property 9: Minimal Manifest - Hash works with minimal required fields":
## Test that hash calculation works with only required fields
var passCount = 0
var failCount = 0
for format in [NPK, NIP, NEXTER]:
for i in 0..<33:
try:
# Create minimal manifest (only required fields)
let manifest = PackageManifest(
format: format,
name: "minimal-" & $i,
version: SemanticVersion(major: 1, minor: 0, patch: 0),
license: "MIT",
buildHash: "xxh3-build",
sourceHash: "xxh3-source",
artifactHash: "xxh3-artifact"
)
# Calculate hash twice
let hash1 = calculateManifestHash(manifest)
let hash2 = calculateManifestHash(manifest)
if hash1 == hash2 and hash1.startsWith("xxh3-"):
passCount.inc()
else:
failCount.inc()
echo $format, " iteration ", i, ": Minimal manifest hash failed"
except CatchableError as e:
failCount.inc()
echo $format, " iteration ", i, ": ", e.msg
echo "\nMinimal Manifest Test Results:"
echo " Passed: ", passCount, "/99"
echo " Failed: ", failCount, "/99"
check passCount == 99
when isMainModule:
# Run tests
randomize()
echo "Running Manifest Hash Determinism Property Tests..."
echo "Testing Property 9: Manifest Hash Determinism"
echo "Validates: Requirements 6.4, 7.5"
echo ""