nip/tests/test_integration.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"