503 lines
17 KiB
Nim
503 lines
17 KiB
Nim
## Unit Tests for NipCell Fallback
|
|
##
|
|
## This module tests the NipCell isolation fallback mechanism for the
|
|
## NIP dependency resolver.
|
|
##
|
|
## **Requirements Tested:**
|
|
## - 10.1: Detect unresolvable conflicts and suggest NipCell isolation
|
|
## - 10.2: Create separate NipCells for conflicting packages
|
|
## - 10.3: Maintain separate dependency graphs per cell
|
|
## - 10.4: Support cell switching
|
|
## - 10.5: Clean up cell-specific packages when removing cells
|
|
|
|
import std/[unittest, options, sets, tables, strutils, strformat, json]
|
|
import ../src/nip/resolver/nipcell_fallback
|
|
import ../src/nip/resolver/conflict_detection
|
|
import ../src/nip/resolver/solver_types
|
|
|
|
# =============================================================================
|
|
# Test Helpers
|
|
# =============================================================================
|
|
|
|
proc createVersionConflict(pkg: string): ConflictReport =
|
|
## Create a test version conflict
|
|
ConflictReport(
|
|
kind: VersionConflict,
|
|
packages: @[pkg],
|
|
details: fmt"Package '{pkg}' has conflicting version requirements",
|
|
suggestions: @["Try relaxing version constraints"],
|
|
conflictingTerms: @[],
|
|
cyclePath: none(seq[string])
|
|
)
|
|
|
|
proc createVariantConflict(pkg: string, domain: string = "init"): ConflictReport =
|
|
## Create a test variant conflict
|
|
ConflictReport(
|
|
kind: VariantConflict,
|
|
packages: @[pkg],
|
|
details: fmt"Package '{pkg}' has conflicting exclusive variant flags in domain '{domain}'",
|
|
suggestions: @["Consider using NipCell isolation"],
|
|
conflictingTerms: @[],
|
|
cyclePath: none(seq[string])
|
|
)
|
|
|
|
proc createCircularDependency(packages: seq[string]): ConflictReport =
|
|
## Create a test circular dependency conflict
|
|
let cycleStr = packages.join(" -> ")
|
|
ConflictReport(
|
|
kind: CircularDependency,
|
|
packages: packages,
|
|
details: "Circular dependency detected: " & cycleStr,
|
|
suggestions: @["Break the cycle"],
|
|
conflictingTerms: @[],
|
|
cyclePath: some(packages)
|
|
)
|
|
|
|
# =============================================================================
|
|
# Conflict Severity Analysis Tests
|
|
# =============================================================================
|
|
|
|
suite "Conflict Severity Analysis":
|
|
test "Version conflict has low severity":
|
|
let conflict = createVersionConflict("openssl")
|
|
let severity = analyzeConflictSeverity(conflict)
|
|
check severity == Low
|
|
|
|
test "Variant conflict with exclusive domain has high severity":
|
|
let conflict = createVariantConflict("systemd", "init")
|
|
let severity = analyzeConflictSeverity(conflict)
|
|
check severity == High
|
|
|
|
test "Circular dependency has critical severity":
|
|
let conflict = createCircularDependency(@["a", "b", "c", "a"])
|
|
let severity = analyzeConflictSeverity(conflict)
|
|
check severity == Critical
|
|
|
|
test "Low severity does not suggest isolation":
|
|
check shouldSuggestIsolation(Low) == false
|
|
|
|
test "Medium severity suggests isolation":
|
|
check shouldSuggestIsolation(Medium) == true
|
|
|
|
test "High severity suggests isolation":
|
|
check shouldSuggestIsolation(High) == true
|
|
|
|
test "Critical severity suggests isolation":
|
|
check shouldSuggestIsolation(Critical) == true
|
|
|
|
# =============================================================================
|
|
# Isolation Candidate Detection Tests
|
|
# =============================================================================
|
|
|
|
suite "Isolation Candidate Detection":
|
|
test "Detect candidates from variant conflict":
|
|
let conflicts = @[createVariantConflict("openssl", "crypto")]
|
|
let candidates = detectIsolationCandidates(conflicts)
|
|
|
|
check candidates.len >= 1
|
|
check candidates[0].packageName == "openssl"
|
|
check candidates[0].suggestedCellName == "openssl-cell"
|
|
|
|
test "No candidates for low severity conflicts":
|
|
let conflicts = @[createVersionConflict("zlib")]
|
|
let candidates = detectIsolationCandidates(conflicts)
|
|
|
|
# Version conflicts are low severity, should not suggest isolation
|
|
check candidates.len == 0
|
|
|
|
test "Detect candidates from circular dependency":
|
|
let conflicts = @[createCircularDependency(@["a", "b", "c", "a"])]
|
|
let candidates = detectIsolationCandidates(conflicts)
|
|
|
|
# Circular dependencies are critical, should suggest isolation
|
|
check candidates.len >= 1
|
|
|
|
test "Multiple conflicts generate multiple candidates":
|
|
let conflicts = @[
|
|
createVariantConflict("openssl", "crypto"),
|
|
createVariantConflict("nginx", "http")
|
|
]
|
|
let candidates = detectIsolationCandidates(conflicts)
|
|
|
|
check candidates.len >= 2
|
|
|
|
# =============================================================================
|
|
# Isolation Suggestion Generation Tests
|
|
# =============================================================================
|
|
|
|
suite "Isolation Suggestion Generation":
|
|
test "Generate suggestion with commands":
|
|
let conflict = createVariantConflict("openssl", "crypto")
|
|
let candidates = @[
|
|
IsolationCandidate(
|
|
packageName: "openssl",
|
|
conflictingWith: @["nginx"],
|
|
severity: High,
|
|
suggestedCellName: "openssl-cell",
|
|
reason: "Exclusive domain conflict"
|
|
)
|
|
]
|
|
|
|
let suggestion = generateIsolationSuggestion(conflict, candidates)
|
|
|
|
check suggestion.candidates.len == 1
|
|
check suggestion.suggestedCells.len == 1
|
|
check suggestion.commands.len >= 1
|
|
check suggestion.explanation.len > 0
|
|
|
|
test "Suggestion includes cell creation command":
|
|
let conflict = createVariantConflict("openssl", "crypto")
|
|
let candidates = @[
|
|
IsolationCandidate(
|
|
packageName: "openssl",
|
|
conflictingWith: @[],
|
|
severity: High,
|
|
suggestedCellName: "openssl-cell",
|
|
reason: "Conflict"
|
|
)
|
|
]
|
|
|
|
let suggestion = generateIsolationSuggestion(conflict, candidates)
|
|
|
|
var hasCreateCommand = false
|
|
for cmd in suggestion.commands:
|
|
if cmd.contains("cell create"):
|
|
hasCreateCommand = true
|
|
break
|
|
|
|
check hasCreateCommand
|
|
|
|
test "Format suggestion produces readable output":
|
|
let conflict = createVariantConflict("openssl", "crypto")
|
|
let candidates = @[
|
|
IsolationCandidate(
|
|
packageName: "openssl",
|
|
conflictingWith: @["nginx"],
|
|
severity: High,
|
|
suggestedCellName: "openssl-cell",
|
|
reason: "Conflict"
|
|
)
|
|
]
|
|
|
|
let suggestion = generateIsolationSuggestion(conflict, candidates)
|
|
let formatted = formatIsolationSuggestion(suggestion)
|
|
|
|
check formatted.contains("IsolationSuggested")
|
|
check formatted.contains("openssl")
|
|
check formatted.contains("Suggested commands")
|
|
|
|
# =============================================================================
|
|
# NipCell Graph Manager Tests
|
|
# =============================================================================
|
|
|
|
suite "NipCell Graph Manager - Cell Creation":
|
|
test "Create new cell":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
|
|
let result = manager.createCell("test-cell", "Test cell description")
|
|
|
|
check result.success == true
|
|
check result.cellName == "test-cell"
|
|
check result.cellId.len > 0
|
|
check result.error == ""
|
|
|
|
test "Cannot create duplicate cell":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
|
|
discard manager.createCell("test-cell")
|
|
let result = manager.createCell("test-cell")
|
|
|
|
check result.success == false
|
|
check result.error.contains("already exists")
|
|
|
|
test "List cells returns created cells":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
|
|
discard manager.createCell("cell-a")
|
|
discard manager.createCell("cell-b")
|
|
discard manager.createCell("cell-c")
|
|
|
|
let cells = manager.listCells()
|
|
|
|
check cells.len == 3
|
|
check "cell-a" in cells
|
|
check "cell-b" in cells
|
|
check "cell-c" in cells
|
|
|
|
# =============================================================================
|
|
# NipCell Graph Manager Tests - Cell Switching
|
|
# =============================================================================
|
|
|
|
suite "NipCell Graph Manager - Cell Switching":
|
|
test "Switch to existing cell":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("test-cell")
|
|
|
|
let result = manager.switchCell("test-cell")
|
|
|
|
check result.success == true
|
|
check result.newCell == "test-cell"
|
|
check result.error == ""
|
|
|
|
test "Cannot switch to non-existent cell":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
|
|
let result = manager.switchCell("non-existent")
|
|
|
|
check result.success == false
|
|
check result.error.contains("not found")
|
|
|
|
test "Get active cell after switch":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("test-cell")
|
|
discard manager.switchCell("test-cell")
|
|
|
|
let activeCell = manager.getActiveCell()
|
|
|
|
check activeCell.isSome
|
|
check activeCell.get() == "test-cell"
|
|
|
|
test "No active cell initially":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
|
|
let activeCell = manager.getActiveCell()
|
|
|
|
check activeCell.isNone
|
|
|
|
test "Switch tracks previous cell":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("cell-a")
|
|
discard manager.createCell("cell-b")
|
|
|
|
discard manager.switchCell("cell-a")
|
|
let result = manager.switchCell("cell-b")
|
|
|
|
check result.success == true
|
|
check result.previousCell.isSome
|
|
check result.previousCell.get() == "cell-a"
|
|
check result.newCell == "cell-b"
|
|
|
|
# =============================================================================
|
|
# NipCell Graph Manager Tests - Separate Graphs
|
|
# =============================================================================
|
|
|
|
suite "NipCell Graph Manager - Separate Graphs":
|
|
test "Each cell has its own graph":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("cell-a")
|
|
discard manager.createCell("cell-b")
|
|
|
|
let graphA = manager.getCellGraph("cell-a")
|
|
let graphB = manager.getCellGraph("cell-b")
|
|
|
|
check graphA.isSome
|
|
check graphB.isSome
|
|
check graphA.get().cellName == "cell-a"
|
|
check graphB.get().cellName == "cell-b"
|
|
check graphA.get().cellId != graphB.get().cellId
|
|
|
|
test "Get active cell graph":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("test-cell")
|
|
discard manager.switchCell("test-cell")
|
|
|
|
let graph = manager.getActiveCellGraph()
|
|
|
|
check graph.isSome
|
|
check graph.get().cellName == "test-cell"
|
|
|
|
test "No active graph when no cell active":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("test-cell")
|
|
|
|
let graph = manager.getActiveCellGraph()
|
|
|
|
check graph.isNone
|
|
|
|
# =============================================================================
|
|
# NipCell Graph Manager Tests - Package Management
|
|
# =============================================================================
|
|
|
|
suite "NipCell Graph Manager - Package Management":
|
|
test "Add package to cell":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("test-cell")
|
|
|
|
let result = manager.addPackageToCell("test-cell", "nginx")
|
|
|
|
check result == true
|
|
check manager.isPackageInCell("test-cell", "nginx")
|
|
|
|
test "Cannot add package to non-existent cell":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
|
|
let result = manager.addPackageToCell("non-existent", "nginx")
|
|
|
|
check result == false
|
|
|
|
test "Remove package from cell":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("test-cell")
|
|
discard manager.addPackageToCell("test-cell", "nginx")
|
|
|
|
let result = manager.removePackageFromCell("test-cell", "nginx")
|
|
|
|
check result == true
|
|
check not manager.isPackageInCell("test-cell", "nginx")
|
|
|
|
test "Get cell packages":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("test-cell")
|
|
discard manager.addPackageToCell("test-cell", "nginx")
|
|
discard manager.addPackageToCell("test-cell", "openssl")
|
|
discard manager.addPackageToCell("test-cell", "zlib")
|
|
|
|
let packages = manager.getCellPackages("test-cell")
|
|
|
|
check packages.len == 3
|
|
check "nginx" in packages
|
|
check "openssl" in packages
|
|
check "zlib" in packages
|
|
|
|
test "Packages are isolated between cells":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("cell-a")
|
|
discard manager.createCell("cell-b")
|
|
|
|
discard manager.addPackageToCell("cell-a", "nginx")
|
|
discard manager.addPackageToCell("cell-b", "apache")
|
|
|
|
check manager.isPackageInCell("cell-a", "nginx")
|
|
check not manager.isPackageInCell("cell-a", "apache")
|
|
check manager.isPackageInCell("cell-b", "apache")
|
|
check not manager.isPackageInCell("cell-b", "nginx")
|
|
|
|
# =============================================================================
|
|
# NipCell Graph Manager Tests - Cell Deletion
|
|
# =============================================================================
|
|
|
|
suite "NipCell Graph Manager - Cell Deletion":
|
|
test "Delete existing cell":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("test-cell")
|
|
|
|
let result = manager.deleteCell("test-cell")
|
|
|
|
check result == true
|
|
check manager.listCells().len == 0
|
|
|
|
test "Cannot delete non-existent cell":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
|
|
let result = manager.deleteCell("non-existent")
|
|
|
|
check result == false
|
|
|
|
test "Deleting active cell deactivates it":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("test-cell")
|
|
discard manager.switchCell("test-cell")
|
|
|
|
discard manager.deleteCell("test-cell")
|
|
|
|
check manager.getActiveCell().isNone
|
|
|
|
test "Packages are cleaned up when cell is deleted":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
discard manager.createCell("test-cell")
|
|
discard manager.addPackageToCell("test-cell", "nginx")
|
|
discard manager.addPackageToCell("test-cell", "openssl")
|
|
|
|
discard manager.deleteCell("test-cell")
|
|
|
|
# Cell no longer exists, so packages are gone
|
|
check manager.getCellPackages("test-cell").len == 0
|
|
|
|
# =============================================================================
|
|
# Conflict-Triggered Fallback Tests
|
|
# =============================================================================
|
|
|
|
suite "Conflict-Triggered Fallback":
|
|
test "No suggestion for empty conflicts":
|
|
let suggestion = checkForIsolationFallback(@[])
|
|
check suggestion.isNone
|
|
|
|
test "No suggestion for low severity conflicts":
|
|
let conflicts = @[createVersionConflict("zlib")]
|
|
let suggestion = checkForIsolationFallback(conflicts)
|
|
check suggestion.isNone
|
|
|
|
test "Suggestion for high severity conflicts":
|
|
let conflicts = @[createVariantConflict("openssl", "crypto")]
|
|
let suggestion = checkForIsolationFallback(conflicts)
|
|
|
|
check suggestion.isSome
|
|
check suggestion.get().candidates.len >= 1
|
|
|
|
test "Handle unresolvable conflict with auto-create":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
let conflict = createVariantConflict("openssl", "crypto")
|
|
|
|
let (suggestion, cellsCreated) = manager.handleUnresolvableConflict(
|
|
conflict, autoCreate = true
|
|
)
|
|
|
|
check suggestion.candidates.len >= 1
|
|
check cellsCreated.len >= 1
|
|
check manager.listCells().len >= 1
|
|
|
|
test "Handle unresolvable conflict without auto-create":
|
|
let manager = newNipCellGraphManager("/tmp/test-cells")
|
|
let conflict = createVariantConflict("openssl", "crypto")
|
|
|
|
let (suggestion, cellsCreated) = manager.handleUnresolvableConflict(
|
|
conflict, autoCreate = false
|
|
)
|
|
|
|
check suggestion.candidates.len >= 1
|
|
check cellsCreated.len == 0
|
|
check manager.listCells().len == 0
|
|
|
|
# =============================================================================
|
|
# Cell Serialization Tests
|
|
# =============================================================================
|
|
|
|
suite "Cell Serialization":
|
|
test "Serialize cell to JSON":
|
|
var cell = newNipCellGraph("test-cell", "test-id-123")
|
|
cell.packages.incl("nginx")
|
|
cell.packages.incl("openssl")
|
|
cell.metadata["description"] = "Test cell"
|
|
|
|
let json = cell.toJson()
|
|
|
|
check json["cellName"].getStr() == "test-cell"
|
|
check json["cellId"].getStr() == "test-id-123"
|
|
check json["packages"].len == 2
|
|
check json["metadata"]["description"].getStr() == "Test cell"
|
|
|
|
test "Deserialize cell from JSON":
|
|
let json = %*{
|
|
"cellName": "test-cell",
|
|
"cellId": "test-id-123",
|
|
"packages": ["nginx", "openssl"],
|
|
"created": "2025-01-01T00:00:00Z",
|
|
"lastModified": "2025-01-01T00:00:00Z",
|
|
"metadata": {"description": "Test cell"}
|
|
}
|
|
|
|
let cell = fromJson(json)
|
|
|
|
check cell.cellName == "test-cell"
|
|
check cell.cellId == "test-id-123"
|
|
check "nginx" in cell.packages
|
|
check "openssl" in cell.packages
|
|
check cell.metadata["description"] == "Test cell"
|
|
|
|
# =============================================================================
|
|
# Run Tests
|
|
# =============================================================================
|
|
|
|
when isMainModule:
|
|
echo "Running NipCell Fallback Tests..."
|