374 lines
12 KiB
Nim
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 ""
|