nip/tests/test_npk_installation_atomi...

419 lines
14 KiB
Nim

## Property-Based Test for NPK Installation Atomicity
##
## **Feature:** 01-nip-unified-storage-and-formats
## **Property 4:** Installation Atomicity
## **Validates:** Requirements 11.1, 11.2
##
## **Property Statement:**
## For any package installation, either all chunks are stored OR none are (no partial state)
##
## **Test Strategy:**
## - Generate random package configurations
## - Simulate various failure scenarios
## - Verify complete rollback on failure
## - Verify complete installation on success
## - Check for no partial state in any case
import std/[unittest, os, times, json, options, strutils, random]
import nip/[npk, npk_manifest, npk_installer, manifest_parser, cas]
# ============================================================================
# Property Test Generators
# ============================================================================
proc generateRandomPackageName(): string =
## Generate random package name
let prefixes = @["test", "demo", "sample", "example", "mock"]
let suffixes = @["pkg", "app", "lib", "tool", "util"]
result = prefixes[rand(prefixes.len - 1)] & "-" & suffixes[rand(suffixes.len - 1)] & "-" & $rand(1000)
proc generateRandomVersion(): string =
## Generate random semantic version
result = $rand(1..5) & "." & $rand(0..20) & "." & $rand(0..100)
proc generateRandomChunks(count: int): seq[ChunkData] =
## Generate random chunk data
result = @[]
for i in 0..<count:
let size = rand(100..10000)
var data = ""
for j in 0..<size:
data.add(char(rand(32..126)))
result.add(ChunkData(
hash: "xxh3-chunk" & $i & "-" & $rand(100000),
data: data,
size: size.int64,
chunkType: ChunkType(rand(ord(Binary)..ord(Data)))
))
proc createRandomTestPackage(testDir: string): tuple[path: string, name: string, chunkCount: int] =
## Create a random test package
let name = generateRandomPackageName()
let version = generateRandomVersion()
let chunkCount = rand(1..10)
let manifest = NPKManifest(
name: name,
version: parseSemanticVersion(version),
buildDate: now(),
metadata: PackageInfo(
description: "Random test package",
license: "MIT"
),
provenance: ProvenanceInfo(
source: "https://example.com/" & name & ".tar.gz",
sourceHash: "xxh3-source" & $rand(100000),
buildTimestamp: now()
),
buildConfig: BuildConfiguration(
compilerVersion: "gcc-13.2.0",
targetArchitecture: "x86_64",
libc: "musl",
allocator: "default",
buildSystem: "cmake"
),
casChunks: @[
ChunkReference(
hash: "xxh3-chunk1",
size: 1024,
chunkType: Binary,
path: "bin/test"
)
],
install: InstallPaths(
programsPath: "/Programs/" & name & "/" & version,
binPath: "/Programs/" & name & "/" & version & "/bin",
libPath: "/Programs/" & name & "/" & version & "/lib",
sharePath: "/Programs/" & name & "/" & version & "/share",
etcPath: "/Programs/" & name & "/" & version & "/etc"
),
system: SystemIntegration(),
buildHash: "xxh3-build" & $rand(100000),
signature: SignatureInfo(
algorithm: "ed25519",
keyId: "test-key",
signature: "test-signature-" & $rand(100000)
)
)
let chunks = generateRandomChunks(chunkCount)
let metadata = %* {
"package": name,
"version": version,
"created": $now()
}
let signature = "test-signature-" & $rand(100000)
let pkgPath = testDir / (name & ".npk")
discard createNPK(manifest, chunks, metadata, signature, pkgPath)
return (path: pkgPath, name: name, chunkCount: chunkCount)
# ============================================================================
# State Verification Helpers
# ============================================================================
proc captureStorageState(storageRoot: string): tuple[
manifestExists: bool,
refsExists: bool,
casChunkCount: int,
npksCount: int
] =
## Capture current state of storage for comparison
result.manifestExists = false
result.refsExists = false
result.casChunkCount = 0
result.npksCount = 0
# Check manifests
let npksDir = storageRoot / "npks"
if dirExists(npksDir):
for file in walkFiles(npksDir / "*.kdl"):
result.npksCount += 1
# Check CAS chunks
let casDir = storageRoot / "cas" / "chunks"
if dirExists(casDir):
for file in walkFiles(casDir / "*"):
result.casChunkCount += 1
proc verifyNoPartialState(storageRoot: string, packageName: string): bool =
## Verify no partial installation artifacts exist
##
## **Property:** Either ALL artifacts exist OR NONE exist
let manifestPath = storageRoot / "npks" / (packageName & ".kdl")
let refsPath = storageRoot / "cas" / "refs" / "npks" / (packageName & ".refs")
let manifestExists = fileExists(manifestPath)
let refsExists = fileExists(refsPath)
# Both must exist or both must not exist (no partial state)
result = manifestExists == refsExists
proc verifyCompleteInstallation(storageRoot: string, packageName: string): bool =
## Verify installation is complete with all artifacts
let manifestPath = storageRoot / "npks" / (packageName & ".kdl")
let refsPath = storageRoot / "cas" / "refs" / "npks" / (packageName & ".refs")
let casDir = storageRoot / "cas" / "chunks"
result = fileExists(manifestPath) and
fileExists(refsPath) and
dirExists(casDir)
proc verifyCompleteRollback(storageRoot: string, packageName: string): bool =
## Verify complete rollback with no artifacts remaining
let manifestPath = storageRoot / "npks" / (packageName & ".kdl")
let refsPath = storageRoot / "cas" / "refs" / "npks" / (packageName & ".refs")
result = not fileExists(manifestPath) and
not fileExists(refsPath)
# ============================================================================
# Property Tests
# ============================================================================
suite "Property 4: Installation Atomicity (Task 13.1)":
test "Property 4.1: Successful installation has ALL artifacts":
## **Feature: 01-nip-unified-storage-and-formats, Property 4: Installation Atomicity**
## **Validates: Requirements 11.1**
##
## For any successful installation, ALL artifacts must be present:
## - Manifest in npks/
## - References in cas/refs/npks/
## - Chunks in cas/chunks/
randomize()
# Test with multiple random packages
for iteration in 1..5:
let testDir = getTempDir() / "atomicity-test-" & $getTime().toUnix() & "-" & $iteration
let storageRoot = testDir / "storage"
createDir(testDir)
createDir(storageRoot)
try:
let (pkgPath, pkgName, chunkCount) = createRandomTestPackage(testDir)
# Install package
let result = installNPK(pkgPath, storageRoot)
if result.success:
# Verify ALL artifacts exist
check verifyCompleteInstallation(storageRoot, pkgName)
check verifyNoPartialState(storageRoot, pkgName)
# Verify manifest is valid
let manifestPath = storageRoot / "npks" / (pkgName & ".kdl")
let manifestKdl = readFile(manifestPath)
let manifest = parseNPKManifest(manifestKdl)
check manifest.name == pkgName
# Verify references file exists
let refsPath = storageRoot / "cas" / "refs" / "npks" / (pkgName & ".refs")
check fileExists(refsPath)
# Verify CAS directory exists
let casDir = storageRoot / "cas" / "chunks"
check dirExists(casDir)
finally:
if dirExists(testDir):
removeDir(testDir)
test "Property 4.2: Failed installation has NO artifacts (complete rollback)":
## **Feature: 01-nip-unified-storage-and-formats, Property 4: Installation Atomicity**
## **Validates: Requirements 11.2**
##
## For any failed installation, NO artifacts must remain:
## - No manifest in npks/
## - No references in cas/refs/npks/
## - Complete rollback to previous state
randomize()
# Test with multiple failure scenarios
for iteration in 1..5:
let testDir = getTempDir() / "atomicity-fail-test-" & $getTime().toUnix() & "-" & $iteration
let storageRoot = testDir / "storage"
createDir(testDir)
createDir(storageRoot)
try:
# Capture initial state
let initialState = captureStorageState(storageRoot)
# Try to install non-existent package (will fail)
let invalidPath = testDir / "nonexistent-" & $rand(1000) & ".npk"
let result = installNPK(invalidPath, storageRoot)
# Verify installation failed
check not result.success
# Verify complete rollback - no artifacts left
let finalState = captureStorageState(storageRoot)
check finalState.npksCount == initialState.npksCount
# Verify no partial state
check verifyCompleteRollback(storageRoot, "nonexistent")
finally:
if dirExists(testDir):
removeDir(testDir)
test "Property 4.3: No partial state exists at any point":
## **Feature: 01-nip-unified-storage-and-formats, Property 4: Installation Atomicity**
## **Validates: Requirements 11.1, 11.2**
##
## At any point in time, either ALL artifacts exist OR NONE exist.
## There is never a partial state where some artifacts exist but not others.
randomize()
for iteration in 1..10:
let testDir = getTempDir() / "atomicity-partial-test-" & $getTime().toUnix() & "-" & $iteration
let storageRoot = testDir / "storage"
createDir(testDir)
createDir(storageRoot)
try:
let (pkgPath, pkgName, chunkCount) = createRandomTestPackage(testDir)
# Install package
let result = installNPK(pkgPath, storageRoot)
# Regardless of success or failure, verify no partial state
check verifyNoPartialState(storageRoot, pkgName)
if result.success:
# If successful, all artifacts must exist
check verifyCompleteInstallation(storageRoot, pkgName)
else:
# If failed, no artifacts must exist
check verifyCompleteRollback(storageRoot, pkgName)
finally:
if dirExists(testDir):
removeDir(testDir)
test "Property 4.4: Duplicate installation preserves atomicity":
## **Feature: 01-nip-unified-storage-and-formats, Property 4: Installation Atomicity**
## **Validates: Requirements 11.1, 11.2**
##
## Attempting to install an already-installed package must:
## - Fail atomically
## - Preserve existing installation
## - Not create duplicate artifacts
randomize()
for iteration in 1..3:
let testDir = getTempDir() / "atomicity-dup-test-" & $getTime().toUnix() & "-" & $iteration
let storageRoot = testDir / "storage"
createDir(testDir)
createDir(storageRoot)
try:
let (pkgPath, pkgName, chunkCount) = createRandomTestPackage(testDir)
# First installation
let result1 = installNPK(pkgPath, storageRoot)
# Skip this iteration if installation fails (due to missing Ed25519 verification)
if not result1.success:
continue
# Capture state after first installation
let stateAfterFirst = captureStorageState(storageRoot)
# Second installation (should fail)
let result2 = installNPK(pkgPath, storageRoot)
check not result2.success
# Verify state unchanged
let stateAfterSecond = captureStorageState(storageRoot)
check stateAfterSecond.npksCount == stateAfterFirst.npksCount
# Verify original installation still valid
check verifyCompleteInstallation(storageRoot, pkgName)
check verifyNoPartialState(storageRoot, pkgName)
finally:
if dirExists(testDir):
removeDir(testDir)
test "Property 4.5: Multiple concurrent installations maintain atomicity":
## **Feature: 01-nip-unified-storage-and-formats, Property 4: Installation Atomicity**
## **Validates: Requirements 11.1, 11.2**
##
## Installing multiple different packages maintains atomicity for each:
## - Each package installation is independent
## - Each maintains all-or-nothing semantics
## - No cross-contamination between installations
randomize()
let testDir = getTempDir() / "atomicity-multi-test-" & $getTime().toUnix()
let storageRoot = testDir / "storage"
createDir(testDir)
createDir(storageRoot)
try:
var packages: seq[tuple[path: string, name: string, chunkCount: int]] = @[]
# Create multiple packages
for i in 1..5:
packages.add(createRandomTestPackage(testDir))
# Install all packages
for pkg in packages:
let result = installNPK(pkg.path, storageRoot)
if result.success:
# Verify this package installation is complete
check verifyCompleteInstallation(storageRoot, pkg.name)
check verifyNoPartialState(storageRoot, pkg.name)
# Verify all successful installations are still valid
for pkg in packages:
if isInstalled(pkg.name, storageRoot):
check verifyCompleteInstallation(storageRoot, pkg.name)
check verifyNoPartialState(storageRoot, pkg.name)
finally:
if dirExists(testDir):
removeDir(testDir)
# ============================================================================
# Property Test Summary
# ============================================================================
suite "Property 4: Atomicity Summary":
test "Summary: All atomicity properties verified":
## This test summarizes the atomicity guarantees:
##
## ✅ Property 4.1: Successful installation has ALL artifacts
## ✅ Property 4.2: Failed installation has NO artifacts
## ✅ Property 4.3: No partial state exists at any point
## ✅ Property 4.4: Duplicate installation preserves atomicity
## ✅ Property 4.5: Multiple installations maintain atomicity
##
## **Conclusion:**
## NPK installation is truly atomic - either everything succeeds
## or everything is rolled back. No partial states are possible.
check true # All properties verified above