411 lines
13 KiB
Nim
411 lines
13 KiB
Nim
## NimPak Integration Tests
|
|
##
|
|
## End-to-end integration tests for the NimPak package manager.
|
|
## Task 46: Integration testing.
|
|
##
|
|
## Tests cover:
|
|
## - Cross-format deduplication (NPK, NIP, NEXTER sharing CAS)
|
|
## - Atomic installations (no partial states)
|
|
## - Garbage collection safety (pinned objects preserved)
|
|
## - Migration workflows (format conversions)
|
|
## - Error recovery (graceful failure handling)
|
|
|
|
import std/[os, strutils, strformat, times, sequtils, sets]
|
|
import unittest
|
|
import ../src/nimpak/cas
|
|
import ../src/nimpak/migration
|
|
import ../src/nimpak/errors
|
|
import ../src/nip/types
|
|
|
|
suite "Integration - Cross-Format Deduplication":
|
|
|
|
var testDir: string
|
|
var casManager: CasManager
|
|
|
|
setup:
|
|
testDir = getTempDir() / "nip_integration_test_" & $epochTime().int
|
|
createDir(testDir)
|
|
casManager = initCasManager(testDir / "cas", testDir / "cas" / "system")
|
|
|
|
teardown:
|
|
removeDir(testDir)
|
|
|
|
test "Same content across NPK, NIP, NEXTER shares CAS object":
|
|
# Simulate the same library being used by all three formats
|
|
let sharedLibData = @[byte(1), byte(2), byte(3), byte(4), byte(5)]
|
|
|
|
# Store for NPK format
|
|
let npkResult = casManager.storeObject(sharedLibData)
|
|
check npkResult.isOk
|
|
let hash1 = npkResult.get().hash
|
|
discard casManager.addReference(hash1, NPK, "libfoo-npk")
|
|
|
|
# Store same data for NIP format - should deduplicate
|
|
let nipResult = casManager.storeObject(sharedLibData)
|
|
check nipResult.isOk
|
|
let hash2 = nipResult.get().hash
|
|
discard casManager.addReference(hash2, NIP, "myapp-nip")
|
|
|
|
# Store same data for NEXTER format - should deduplicate
|
|
let nexterResult = casManager.storeObject(sharedLibData)
|
|
check nexterResult.isOk
|
|
let hash3 = nexterResult.get().hash
|
|
discard casManager.addReference(hash3, NEXTER, "devenv-nexter")
|
|
|
|
# All three should have the same hash (deduplication)
|
|
check hash1 == hash2
|
|
check hash2 == hash3
|
|
|
|
# Object should exist only once in CAS
|
|
check casManager.objectExists(hash1)
|
|
|
|
# Reference count should reflect multiple stores
|
|
let refCount = casManager.getRefCount(hash1)
|
|
check refCount >= 3 # At least 3 stores
|
|
|
|
test "Deduplication across multiple packages of same format":
|
|
# Common data shared by multiple packages
|
|
let commonData = @[byte(10), byte(20), byte(30)]
|
|
|
|
var storedHashes: seq[string] = @[]
|
|
|
|
# Simulate 5 NPK packages sharing the same file
|
|
for i in 1..5:
|
|
let result = casManager.storeObject(commonData)
|
|
check result.isOk
|
|
storedHashes.add(result.get().hash)
|
|
discard casManager.addReference(result.get().hash, NPK, fmt"pkg-{i}")
|
|
|
|
# All hashes should be identical (deduplication)
|
|
check storedHashes.toHashSet.len == 1
|
|
|
|
# Object should exist
|
|
check casManager.objectExists(storedHashes[0])
|
|
|
|
# Reference count should be high
|
|
let refCount = casManager.getRefCount(storedHashes[0])
|
|
check refCount >= 5
|
|
|
|
test "Different content produces different hashes":
|
|
let data1 = @[byte(1), byte(2), byte(3)]
|
|
let data2 = @[byte(4), byte(5), byte(6)]
|
|
let data3 = @[byte(7), byte(8), byte(9)]
|
|
|
|
let hash1 = casManager.storeObject(data1).get().hash
|
|
let hash2 = casManager.storeObject(data2).get().hash
|
|
let hash3 = casManager.storeObject(data3).get().hash
|
|
|
|
check hash1 != hash2
|
|
check hash2 != hash3
|
|
check hash1 != hash3
|
|
|
|
suite "Integration - Atomic Installations":
|
|
|
|
var testDir: string
|
|
var casManager: CasManager
|
|
|
|
setup:
|
|
testDir = getTempDir() / "nip_atomic_test_" & $epochTime().int
|
|
createDir(testDir)
|
|
casManager = initCasManager(testDir / "cas", testDir / "cas" / "system")
|
|
|
|
teardown:
|
|
removeDir(testDir)
|
|
|
|
test "Package files are stored atomically":
|
|
# Simulate installing multiple files as a package
|
|
var packageHashes: seq[string] = @[]
|
|
|
|
for i in 1..5:
|
|
let data = @[byte(i), byte(i+1), byte(i+2)]
|
|
let result = casManager.storeObject(data)
|
|
check result.isOk
|
|
packageHashes.add(result.get().hash)
|
|
# Add reference as part of atomic transaction
|
|
discard casManager.addReference(result.get().hash, NPK, "test-pkg")
|
|
|
|
# Verify all objects exist
|
|
for hash in packageHashes:
|
|
check casManager.objectExists(hash)
|
|
|
|
test "Complete installation is fully committed":
|
|
var packageHashes: seq[string] = @[]
|
|
|
|
# Install a "package" with 3 files
|
|
for i in 1..3:
|
|
let data = @[byte(100 + i)]
|
|
let result = casManager.storeObject(data)
|
|
check result.isOk
|
|
packageHashes.add(result.get().hash)
|
|
discard casManager.addReference(result.get().hash, NIP, "complete-pkg")
|
|
|
|
# Verify all exist
|
|
for hash in packageHashes:
|
|
check casManager.objectExists(hash)
|
|
|
|
# Run GC - should not remove anything (still referenced)
|
|
discard casManager.garbageCollect()
|
|
|
|
# All still exist
|
|
for hash in packageHashes:
|
|
check casManager.objectExists(hash)
|
|
|
|
suite "Integration - Garbage Collection Safety":
|
|
|
|
var testDir: string
|
|
var casManager: CasManager
|
|
|
|
setup:
|
|
testDir = getTempDir() / "nip_gc_safety_test_" & $epochTime().int
|
|
createDir(testDir)
|
|
casManager = initCasManager(testDir / "cas", testDir / "cas" / "system")
|
|
|
|
teardown:
|
|
removeDir(testDir)
|
|
|
|
test "Pinned objects survive garbage collection":
|
|
let criticalData = @[byte(255), byte(254), byte(253)]
|
|
|
|
let result = casManager.storeObject(criticalData)
|
|
check result.isOk
|
|
let hash = result.get().hash
|
|
|
|
# Pin the object
|
|
discard casManager.pinObject(hash, "system-critical")
|
|
|
|
# Run GC
|
|
discard casManager.garbageCollect()
|
|
|
|
# Pinned object should still exist
|
|
check casManager.objectExists(hash)
|
|
|
|
test "Referenced objects survive GC":
|
|
let data = @[byte(42)]
|
|
|
|
let result = casManager.storeObject(data)
|
|
check result.isOk
|
|
let hash = result.get().hash
|
|
|
|
# Add reference
|
|
discard casManager.addReference(hash, NPK, "my-package")
|
|
|
|
# Run GC
|
|
discard casManager.garbageCollect()
|
|
|
|
# Object should still exist
|
|
check casManager.objectExists(hash)
|
|
|
|
test "Cross-format references all protect object":
|
|
let sharedData = @[byte(50), byte(51)]
|
|
|
|
let result = casManager.storeObject(sharedData)
|
|
check result.isOk
|
|
let hash = result.get().hash
|
|
|
|
# Add references from multiple formats
|
|
discard casManager.addReference(hash, NPK, "pkg1")
|
|
discard casManager.addReference(hash, NIP, "app1")
|
|
discard casManager.addReference(hash, NEXTER, "container1")
|
|
|
|
# Run GC
|
|
discard casManager.garbageCollect()
|
|
|
|
# Object should still exist (multiple references)
|
|
check casManager.objectExists(hash)
|
|
|
|
suite "Integration - Migration Workflows":
|
|
|
|
var testDir: string
|
|
var migrationManager: MigrationManager
|
|
|
|
setup:
|
|
testDir = getTempDir() / "nip_migration_test_" & $epochTime().int
|
|
createDir(testDir)
|
|
migrationManager = initMigrationManager(testDir / "cas", dryRun = false, verbose = false)
|
|
|
|
teardown:
|
|
removeDir(testDir)
|
|
|
|
test "Legacy NIP to new format migration preserves data":
|
|
# Create a mock legacy NIP structure
|
|
let legacyDir = testDir / "legacy-app"
|
|
createDir(legacyDir)
|
|
createDir(legacyDir / "bin")
|
|
createDir(legacyDir / "lib")
|
|
|
|
writeFile(legacyDir / "manifest.kdl", "name \"test-app\"\nversion \"1.0.0\"")
|
|
writeFile(legacyDir / "bin" / "app", "#!/bin/sh\necho hello")
|
|
writeFile(legacyDir / "lib" / "libfoo.so", "ELF binary data simulation")
|
|
|
|
# Migrate
|
|
let result = migrationManager.migrateLegacyNip(legacyDir)
|
|
|
|
check result.source == OldNip
|
|
check result.packageName == "legacy-app"
|
|
check result.success == true
|
|
check result.casHashes.len > 0
|
|
|
|
test "Format conversion NPK to NIP":
|
|
let result = migrationManager.convertNpkToNip("/mock/package.npk")
|
|
|
|
check result.success == true # Placeholder succeeds
|
|
check result.warnings.len > 0 # Has warnings about placeholder
|
|
|
|
test "Format conversion NIP to NEXTER":
|
|
let result = migrationManager.convertNipToNexter("/mock/app.nip")
|
|
|
|
check result.success == true # Placeholder succeeds
|
|
check result.warnings.len > 0
|
|
|
|
test "Migration verification catches missing objects":
|
|
let mockResult = MigrationResult(
|
|
success: true,
|
|
source: OldNip,
|
|
packageName: "missing-objects",
|
|
casHashes: @["xxh3-nonexistent123456"],
|
|
errors: @[]
|
|
)
|
|
|
|
# Verification should fail for missing objects
|
|
check not migrationManager.verifyMigration(mockResult)
|
|
|
|
test "Migration report generation":
|
|
# Create two mock results
|
|
let results = @[
|
|
MigrationResult(success: true, source: OldNip, packageName: "app1"),
|
|
MigrationResult(success: true, source: Flatpak, packageName: "app2"),
|
|
MigrationResult(success: false, source: AppImage, packageName: "app3", errors: @["Test error"])
|
|
]
|
|
|
|
let report = generateMigrationReport(results)
|
|
|
|
check report.len > 0
|
|
check report.contains("app1")
|
|
check report.contains("app2")
|
|
check report.contains("app3")
|
|
|
|
suite "Integration - Error Recovery":
|
|
|
|
var testDir: string
|
|
var casManager: CasManager
|
|
|
|
setup:
|
|
testDir = getTempDir() / "nip_error_test_" & $epochTime().int
|
|
createDir(testDir)
|
|
casManager = initCasManager(testDir / "cas", testDir / "cas" / "system")
|
|
|
|
teardown:
|
|
removeDir(testDir)
|
|
|
|
test "Retrieve nonexistent object returns error":
|
|
let result = casManager.retrieveObject("xxh3-doesnotexist000000")
|
|
|
|
check result.isErr
|
|
|
|
test "Store and retrieve cycle works correctly":
|
|
let originalData = @[byte(1), byte(2), byte(3), byte(4), byte(5)]
|
|
|
|
# Store
|
|
let storeResult = casManager.storeObject(originalData)
|
|
check storeResult.isOk
|
|
let hash = storeResult.get().hash
|
|
|
|
# Retrieve
|
|
let retrieveResult = casManager.retrieveObject(hash)
|
|
check retrieveResult.isOk
|
|
|
|
let retrievedData = retrieveResult.get()
|
|
check retrievedData == originalData
|
|
|
|
test "Error factory provides actionable suggestions":
|
|
let err = checksumMismatchError("pkg.nip", "xxh3-expected", "xxh3-actual")
|
|
|
|
check err.code == ChecksumMismatch
|
|
check err.suggestions.len > 0
|
|
check err.msg.len > 0
|
|
|
|
test "Recovery strategies are appropriate for error types":
|
|
# Network errors should suggest retry
|
|
let networkErr = networkError("timeout", "https://example.com")
|
|
check suggestRecovery(networkErr) == Retry
|
|
|
|
# Permission errors should suggest manual intervention
|
|
let permErr = permissionDeniedError("/root/file", "write")
|
|
check suggestRecovery(permErr) == Manual
|
|
|
|
# Checksum errors should abort
|
|
let checksumErr = checksumMismatchError("file", "a", "b")
|
|
check suggestRecovery(checksumErr) == Abort
|
|
|
|
suite "Integration - End-to-End Workflows":
|
|
|
|
var testDir: string
|
|
var casManager: CasManager
|
|
|
|
setup:
|
|
testDir = getTempDir() / "nip_e2e_test_" & $epochTime().int
|
|
createDir(testDir)
|
|
casManager = initCasManager(testDir / "cas", testDir / "cas" / "system")
|
|
|
|
teardown:
|
|
removeDir(testDir)
|
|
|
|
test "Multiple package versions coexist":
|
|
# Install v1.0
|
|
let v1Data = @[byte(1), byte(0)]
|
|
let v1Result = casManager.storeObject(v1Data)
|
|
check v1Result.isOk
|
|
let v1Hash = v1Result.get().hash
|
|
discard casManager.addReference(v1Hash, NPK, "myapp-1.0")
|
|
|
|
# Install v2.0 (different content)
|
|
let v2Data = @[byte(2), byte(0)]
|
|
let v2Result = casManager.storeObject(v2Data)
|
|
check v2Result.isOk
|
|
let v2Hash = v2Result.get().hash
|
|
discard casManager.addReference(v2Hash, NPK, "myapp-2.0")
|
|
|
|
# Both versions exist
|
|
check casManager.objectExists(v1Hash)
|
|
check casManager.objectExists(v2Hash)
|
|
check v1Hash != v2Hash
|
|
|
|
test "Concurrent format usage: NPK + NIP + NEXTER":
|
|
# System library (NPK)
|
|
let libData = @[byte(100)]
|
|
let libResult = casManager.storeObject(libData)
|
|
check libResult.isOk
|
|
discard casManager.addReference(libResult.get().hash, NPK, "libsystem")
|
|
|
|
# Application using library (NIP)
|
|
let appData = @[byte(200)]
|
|
let appResult = casManager.storeObject(appData)
|
|
check appResult.isOk
|
|
discard casManager.addReference(appResult.get().hash, NIP, "myapp")
|
|
|
|
# Dev container (NEXTER)
|
|
let containerData = @[byte(255)]
|
|
let containerResult = casManager.storeObject(containerData)
|
|
check containerResult.isOk
|
|
discard casManager.addReference(containerResult.get().hash, NEXTER, "devenv")
|
|
|
|
# All three exist
|
|
check casManager.objectExists(libResult.get().hash)
|
|
check casManager.objectExists(appResult.get().hash)
|
|
check casManager.objectExists(containerResult.get().hash)
|
|
|
|
test "Hash consistency across store cycles":
|
|
let data = @[byte(1), byte(2), byte(3)]
|
|
|
|
# Store same data multiple times
|
|
let hash1 = casManager.storeObject(data).get().hash
|
|
let hash2 = casManager.storeObject(data).get().hash
|
|
let hash3 = casManager.storeObject(data).get().hash
|
|
|
|
# All hashes are identical
|
|
check hash1 == hash2
|
|
check hash2 == hash3
|
|
|
|
when isMainModule:
|
|
echo "Integration Tests Complete"
|