nip/tests/test_nip_manifest_roundtrip...

477 lines
16 KiB
Nim

## Property-Based Test: NIP Manifest Roundtrip
##
## **Feature:** 01-nip-unified-storage-and-formats
## **Property 3:** Manifest Roundtrip
## **Validates:** Requirements 6.4
##
## **Property Statement:**
## For any NIP manifest, parsing and regenerating SHALL produce semantically equivalent KDL
##
## **Test Strategy:**
## 1. Generate random NIP manifests with valid data
## 2. Convert manifest to KDL string
## 3. Parse KDL string back to manifest
## 4. Verify semantic equivalence (all fields match)
## 5. Verify determinism (same manifest = same KDL)
import std/[unittest, times, options, random, strutils]
import nip/nip_manifest
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)
)
proc genAppInfo(): AppInfo =
## Generate random application metadata
AppInfo(
description: "Test application " & $rand(1000),
homepage: some("https://example.com/" & $rand(1000)),
license: ["MIT", "GPL-3.0", "Apache-2.0", "BSD-3-Clause"][rand(3)],
author: some("Test Author " & $rand(100)),
maintainer: some("Test Maintainer " & $rand(100)),
tags: @["test", "example", "app" & $rand(10)],
category: some(["Graphics", "Network", "Development", "Utility"][rand(3)])
)
proc genProvenanceInfo(): ProvenanceInfo =
## Generate random provenance information
ProvenanceInfo(
source: "https://github.com/test/repo" & $rand(1000),
sourceHash: "xxh3-" & $rand(high(int)),
upstream: some("https://upstream.example.com/" & $rand(1000)),
buildTimestamp: now(),
builder: some("builder-" & $rand(100))
)
proc genBuildConfiguration(): BuildConfiguration =
## Generate random build configuration
BuildConfiguration(
configureFlags: @["--enable-feature" & $rand(10), "--with-lib" & $rand(5)],
compilerFlags: @["-O2", "-march=native", "-flto"],
compilerVersion: "gcc-" & $rand(10..13) & ".0.0",
targetArchitecture: ["x86_64", "aarch64", "riscv64"][rand(2)],
libc: ["musl", "glibc"][rand(1)],
allocator: ["jemalloc", "tcmalloc", "default"][rand(2)],
buildSystem: ["cmake", "meson", "autotools"][rand(2)]
)
proc genChunkReference(): ChunkReference =
## Generate random CAS chunk reference
ChunkReference(
hash: "xxh3-" & $rand(high(int)),
size: rand(1024..1048576).int64,
chunkType: [Binary, Library, Runtime, Config, Data][rand(4)],
path: "bin/app" & $rand(10)
)
proc genDesktopFileSpec(): DesktopFileSpec =
## Generate random desktop file specification
DesktopFileSpec(
name: "Test App " & $rand(100),
genericName: some("Generic App " & $rand(100)),
comment: some("A test application"),
exec: "/usr/bin/testapp" & $rand(10),
icon: "testapp" & $rand(10),
terminal: rand(1) == 0,
categories: @["Graphics", "Utility"],
keywords: @["test", "example", "app"]
)
proc genIconSpec(): IconSpec =
## Generate random icon specification
IconSpec(
size: [16, 32, 48, 64, 128, 256][rand(5)],
path: "icons/icon" & $rand(10) & ".png",
format: ["png", "svg"][rand(1)]
)
proc genDesktopMetadata(): DesktopMetadata =
## Generate random desktop metadata
DesktopMetadata(
desktopFile: genDesktopFileSpec(),
icons: @[genIconSpec(), genIconSpec()],
mimeTypes: @["text/plain", "application/json"],
appId: "org.example.testapp" & $rand(100)
)
proc genFilesystemAccess(): FilesystemAccess =
## Generate random filesystem access permission
FilesystemAccess(
path: ["/home", "/tmp", "/var/cache"][rand(2)],
mode: [ReadOnly, ReadWrite, Create][rand(2)]
)
proc genDBusAccess(): DBusAccess =
## Generate random D-Bus access permissions
DBusAccess(
session: @["org.freedesktop.Notifications", "org.kde.StatusNotifierWatcher"],
system: @["org.freedesktop.NetworkManager"],
own: @["org.example.TestApp" & $rand(100)]
)
proc genMount(): Mount =
## Generate random mount specification
Mount(
source: "/host/path" & $rand(10),
target: "/app/path" & $rand(10),
mountType: [Bind, Tmpfs, Devtmpfs][rand(2)],
readOnly: rand(1) == 0
)
proc genNamespaceConfig(): NamespaceConfig =
## Generate random namespace configuration
NamespaceConfig(
namespaceType: ["user", "strict", "none"][rand(2)],
permissions: Permissions(
network: rand(1) == 0,
gpu: rand(1) == 0,
audio: rand(1) == 0,
camera: rand(1) == 0,
microphone: rand(1) == 0,
filesystem: @[genFilesystemAccess(), genFilesystemAccess()],
dbus: genDBusAccess()
),
mounts: @[genMount(), genMount()]
)
proc genSignatureInfo(): SignatureInfo =
## Generate random signature information
SignatureInfo(
algorithm: "ed25519",
keyId: "key-" & $rand(1000),
signature: "sig-" & $rand(high(int))
)
proc genNIPManifest(): NIPManifest =
## Generate random NIP manifest
NIPManifest(
name: "testapp" & $rand(1000),
version: genSemanticVersion(),
buildDate: now(),
metadata: genAppInfo(),
provenance: genProvenanceInfo(),
buildConfig: genBuildConfiguration(),
casChunks: @[genChunkReference(), genChunkReference(), genChunkReference()],
desktop: genDesktopMetadata(),
namespace: genNamespaceConfig(),
buildHash: "xxh3-" & $rand(high(int)),
signature: genSignatureInfo()
)
# ============================================================================
# Semantic Equivalence Checks
# ============================================================================
proc checkTimestampsClose(a, b: DateTime, toleranceSeconds: int = 2): bool =
## Check if two timestamps are within tolerance (for parsing precision)
let diff = abs((a - b).inSeconds)
return diff <= toleranceSeconds
proc checkAppInfoEqual(a, b: AppInfo): bool =
## Check if two AppInfo objects are semantically equivalent
result = a.description == b.description and
a.homepage == b.homepage and
a.license == b.license and
a.author == b.author and
a.maintainer == b.maintainer and
a.tags == b.tags and
a.category == b.category
proc checkProvenanceEqual(a, b: ProvenanceInfo): bool =
## Check if two ProvenanceInfo objects are semantically equivalent
result = a.source == b.source and
a.sourceHash == b.sourceHash and
a.upstream == b.upstream and
a.builder == b.builder and
checkTimestampsClose(a.buildTimestamp, b.buildTimestamp)
proc checkBuildConfigEqual(a, b: BuildConfiguration): bool =
## Check if two BuildConfiguration objects are semantically equivalent
result = a.configureFlags == b.configureFlags and
a.compilerFlags == b.compilerFlags and
a.compilerVersion == b.compilerVersion and
a.targetArchitecture == b.targetArchitecture and
a.libc == b.libc and
a.allocator == b.allocator and
a.buildSystem == b.buildSystem
proc checkChunkRefEqual(a, b: ChunkReference): bool =
## Check if two ChunkReference objects are semantically equivalent
result = a.hash == b.hash and
a.size == b.size and
a.chunkType == b.chunkType and
a.path == b.path
proc checkDesktopFileEqual(a, b: DesktopFileSpec): bool =
## Check if two DesktopFileSpec objects are semantically equivalent
result = a.name == b.name and
a.genericName == b.genericName and
a.comment == b.comment and
a.exec == b.exec and
a.icon == b.icon and
a.terminal == b.terminal and
a.categories == b.categories and
a.keywords == b.keywords
proc checkIconEqual(a, b: IconSpec): bool =
## Check if two IconSpec objects are semantically equivalent
result = a.size == b.size and
a.path == b.path and
a.format == b.format
proc checkDesktopMetadataEqual(a, b: DesktopMetadata, verbose: bool = false): bool =
## Check if two DesktopMetadata objects are semantically equivalent
if not checkDesktopFileEqual(a.desktopFile, b.desktopFile):
if verbose: echo " Desktop file mismatch"
return false
if a.icons.len != b.icons.len:
if verbose: echo " Icons length mismatch: ", a.icons.len, " vs ", b.icons.len
return false
for i in 0..<a.icons.len:
if not checkIconEqual(a.icons[i], b.icons[i]):
if verbose: echo " Icon ", i, " mismatch"
return false
if a.mimeTypes != b.mimeTypes:
if verbose: echo " MIME types mismatch: ", a.mimeTypes, " vs ", b.mimeTypes
return false
if a.appId != b.appId:
if verbose: echo " App ID mismatch: ", a.appId, " vs ", b.appId
return false
return true
proc checkFilesystemAccessEqual(a, b: FilesystemAccess): bool =
## Check if two FilesystemAccess objects are semantically equivalent
result = a.path == b.path and a.mode == b.mode
proc checkDBusAccessEqual(a, b: DBusAccess): bool =
## Check if two DBusAccess objects are semantically equivalent
result = a.session == b.session and
a.system == b.system and
a.own == b.own
proc checkMountEqual(a, b: Mount): bool =
## Check if two Mount objects are semantically equivalent
result = a.source == b.source and
a.target == b.target and
a.mountType == b.mountType and
a.readOnly == b.readOnly
proc checkNamespaceConfigEqual(a, b: NamespaceConfig): bool =
## Check if two NamespaceConfig objects are semantically equivalent
if a.namespaceType != b.namespaceType:
return false
if a.permissions.network != b.permissions.network or
a.permissions.gpu != b.permissions.gpu or
a.permissions.audio != b.permissions.audio or
a.permissions.camera != b.permissions.camera or
a.permissions.microphone != b.permissions.microphone:
return false
if a.permissions.filesystem.len != b.permissions.filesystem.len:
return false
for i in 0..<a.permissions.filesystem.len:
if not checkFilesystemAccessEqual(a.permissions.filesystem[i], b.permissions.filesystem[i]):
return false
if not checkDBusAccessEqual(a.permissions.dbus, b.permissions.dbus):
return false
if a.mounts.len != b.mounts.len:
return false
for i in 0..<a.mounts.len:
if not checkMountEqual(a.mounts[i], b.mounts[i]):
return false
result = true
proc checkSignatureEqual(a, b: SignatureInfo): bool =
## Check if two SignatureInfo objects are semantically equivalent
result = a.algorithm == b.algorithm and
a.keyId == b.keyId and
a.signature == b.signature
proc checkManifestsEqual(a, b: NIPManifest, verbose: bool = false): bool =
## Check if two NIP manifests are semantically equivalent
if a.name != b.name:
if verbose: echo "Name mismatch: ", a.name, " vs ", b.name
return false
if a.version != b.version:
if verbose: echo "Version mismatch: ", a.version, " vs ", b.version
return false
if not checkTimestampsClose(a.buildDate, b.buildDate):
if verbose: echo "BuildDate mismatch: ", a.buildDate, " vs ", b.buildDate
return false
if not checkAppInfoEqual(a.metadata, b.metadata):
if verbose: echo "Metadata mismatch"
return false
if not checkProvenanceEqual(a.provenance, b.provenance):
if verbose: echo "Provenance mismatch"
return false
if not checkBuildConfigEqual(a.buildConfig, b.buildConfig):
if verbose: echo "BuildConfig mismatch"
return false
if not checkDesktopMetadataEqual(a.desktop, b.desktop, verbose):
if verbose: echo "Desktop metadata mismatch"
return false
if not checkNamespaceConfigEqual(a.namespace, b.namespace):
if verbose: echo "Namespace config mismatch"
return false
if a.buildHash != b.buildHash:
if verbose: echo "BuildHash mismatch: ", a.buildHash, " vs ", b.buildHash
return false
if not checkSignatureEqual(a.signature, b.signature):
if verbose: echo "Signature mismatch"
return false
# Check CAS chunks
if a.casChunks.len != b.casChunks.len:
if verbose: echo "CAS chunks length mismatch: ", a.casChunks.len, " vs ", b.casChunks.len
return false
for i in 0..<a.casChunks.len:
if not checkChunkRefEqual(a.casChunks[i], b.casChunks[i]):
if verbose: echo "CAS chunk ", i, " mismatch"
return false
return true
# ============================================================================
# Property-Based Tests
# ============================================================================
suite "NIP Manifest Roundtrip Property Tests":
test "Property 3: Manifest Roundtrip - Generate then parse produces equivalent manifest":
## **Feature:** 01-nip-unified-storage-and-formats, Property 3: Manifest Roundtrip
## **Validates:** Requirements 6.4
##
## For any NIP manifest, generating KDL and parsing it back should produce
## a semantically equivalent manifest.
# Run property test with 100 random manifests
var passCount = 0
var failCount = 0
var errors: seq[string] = @[]
for i in 0..<100:
try:
# Generate random manifest
let original = genNIPManifest()
# Generate KDL
let kdl = generateNIPManifest(original)
# Parse KDL back to manifest
let parsed = parseNIPManifest(kdl)
# Check semantic equivalence
let verbose = (failCount == 0) # Show details for first failure only
if checkManifestsEqual(original, parsed, verbose):
passCount.inc()
else:
failCount.inc()
errors.add("Iteration " & $i & ": Manifests not equivalent after roundtrip")
except CatchableError as e:
failCount.inc()
errors.add("Iteration " & $i & ": " & e.msg)
# Report results
echo "\nProperty Test Results:"
echo " Passed: ", passCount, "/100"
echo " Failed: ", failCount, "/100"
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 == 100
test "Property 3: Manifest Roundtrip - Determinism (same manifest = same KDL)":
## **Feature:** 01-nip-unified-storage-and-formats, Property 3: Manifest Roundtrip
## **Validates:** Requirements 6.4
##
## For any NIP manifest, generating KDL twice should produce identical output.
var passCount = 0
var failCount = 0
for i in 0..<100:
try:
# Generate random manifest
let manifest = genNIPManifest()
# Generate KDL twice
let kdl1 = generateNIPManifest(manifest)
let kdl2 = generateNIPManifest(manifest)
# Check they're identical
if kdl1 == kdl2:
passCount.inc()
else:
failCount.inc()
echo "Iteration ", i, ": KDL output not deterministic"
except CatchableError as e:
failCount.inc()
echo "Iteration ", i, ": ", e.msg
echo "\nDeterminism Test Results:"
echo " Passed: ", passCount, "/100"
echo " Failed: ", failCount, "/100"
check passCount == 100
test "Property 3: Manifest Roundtrip - Validation catches invalid manifests":
## Verify that validation catches common errors
# Test empty name
var manifest = genNIPManifest()
manifest.name = ""
let issues1 = validateNIPManifest(manifest)
check issues1.len > 0
check "name cannot be empty" in issues1[0].toLowerAscii()
# Test invalid hash format
manifest = genNIPManifest()
manifest.buildHash = "invalid-hash"
let issues2 = validateNIPManifest(manifest)
check issues2.len > 0
check "xxh3" in issues2[0].toLowerAscii()
# Test invalid namespace type
manifest = genNIPManifest()
manifest.namespace.namespaceType = "invalid"
let issues3 = validateNIPManifest(manifest)
check issues3.len > 0
check "namespace type" in issues3[0].toLowerAscii()
# Test empty app_id
manifest = genNIPManifest()
manifest.desktop.appId = ""
let issues4 = validateNIPManifest(manifest)
check issues4.len > 0
check "app_id" in issues4[0].toLowerAscii()
when isMainModule:
# Run tests
randomize()
echo "Running NIP Manifest Roundtrip Property Tests..."
echo "Testing Property 3: Manifest Roundtrip"
echo "Validates: Requirements 6.4"
echo ""