nip/tests/test_topological_sort.nim

373 lines
17 KiB
Nim

## Unit Tests for Topological Sort
##
## Tests for the topological sorting implementation using Kahn's algorithm.
## Topological sort produces an ordering where dependencies come before dependents.
import std/[unittest, options, sequtils]
import ../src/nip/resolver/dependency_graph
import ../src/nip/resolver/variant_types
suite "Topological Sort Tests":
test "Simple chain produces correct order":
## Test topological sort on a simple chain: A -> B -> C
var graph = newDependencyGraph()
var profile = newVariantProfile()
profile.addFlag("optimization", "lto")
profile.calculateHash()
# Create terms
let termAId = createTermId("packageA", profile.hash)
let termBId = createTermId("packageB", profile.hash)
let termCId = createTermId("packageC", profile.hash)
let termA = PackageTerm(id: termAId, packageName: "packageA", variantProfile: profile, optional: false, source: "nix")
let termB = PackageTerm(id: termBId, packageName: "packageB", variantProfile: profile, optional: false, source: "nix")
let termC = PackageTerm(id: termCId, packageName: "packageC", variantProfile: profile, optional: false, source: "nix")
graph.addTerm(termA)
graph.addTerm(termB)
graph.addTerm(termC)
# Create edges: A -> B -> C
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termBId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termBId, toTerm: termCId, dependencyType: Required))
# Perform topological sort
let sorted = graph.topologicalSort()
# Check that all nodes are present
check sorted.len == 3
# Check that dependencies come before dependents
# For installation order: dependencies first (C, then B, then A)
let posA = sorted.find(termAId)
let posB = sorted.find(termBId)
let posC = sorted.find(termCId)
# Dependencies must come before dependents
check posC < posB # C before B (B depends on C)
check posB < posA # B before A (A depends on B)
test "Diamond produces valid order":
## Test topological sort on diamond: A -> B,C -> D
var graph = newDependencyGraph()
var profile = newVariantProfile()
profile.addFlag("optimization", "lto")
profile.calculateHash()
# Create terms
let termAId = createTermId("packageA", profile.hash)
let termBId = createTermId("packageB", profile.hash)
let termCId = createTermId("packageC", profile.hash)
let termDId = createTermId("packageD", profile.hash)
let termA = PackageTerm(id: termAId, packageName: "packageA", variantProfile: profile, optional: false, source: "nix")
let termB = PackageTerm(id: termBId, packageName: "packageB", variantProfile: profile, optional: false, source: "nix")
let termC = PackageTerm(id: termCId, packageName: "packageC", variantProfile: profile, optional: false, source: "nix")
let termD = PackageTerm(id: termDId, packageName: "packageD", variantProfile: profile, optional: false, source: "nix")
graph.addTerm(termA)
graph.addTerm(termB)
graph.addTerm(termC)
graph.addTerm(termD)
# Create edges: A -> B, A -> C, B -> D, C -> D
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termBId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termCId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termBId, toTerm: termDId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termCId, toTerm: termDId, dependencyType: Required))
# Perform topological sort
let sorted = graph.topologicalSort()
# Check that all nodes are present
check sorted.len == 4
# Check that dependencies come before dependents
# Installation order: D first (no deps), then B and C (depend on D), then A (depends on B and C)
let posA = sorted.find(termAId)
let posB = sorted.find(termBId)
let posC = sorted.find(termCId)
let posD = sorted.find(termDId)
# D must come before both B and C (B and C depend on D)
check posD < posB
check posD < posC
# Both B and C must come before A (A depends on B and C)
check posB < posA
check posC < posA
test "Cycle raises error":
## Test that topological sort raises error on cyclic graph
var graph = newDependencyGraph()
var profile = newVariantProfile()
profile.addFlag("optimization", "lto")
profile.calculateHash()
# Create terms
let termAId = createTermId("packageA", profile.hash)
let termBId = createTermId("packageB", profile.hash)
let termA = PackageTerm(id: termAId, packageName: "packageA", variantProfile: profile, optional: false, source: "nix")
let termB = PackageTerm(id: termBId, packageName: "packageB", variantProfile: profile, optional: false, source: "nix")
graph.addTerm(termA)
graph.addTerm(termB)
# Create cycle: A -> B -> A
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termBId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termBId, toTerm: termAId, dependencyType: Required))
# Topological sort should raise error
expect(ValueError):
discard graph.topologicalSort()
test "Empty graph returns empty list":
## Test topological sort on empty graph
let graph = newDependencyGraph()
let sorted = graph.topologicalSort()
check sorted.len == 0
test "Single node":
## Test topological sort on single node
var graph = newDependencyGraph()
var profile = newVariantProfile()
profile.addFlag("optimization", "lto")
profile.calculateHash()
let termId = createTermId("packageA", profile.hash)
let term = PackageTerm(id: termId, packageName: "packageA", variantProfile: profile, optional: false, source: "nix")
graph.addTerm(term)
let sorted = graph.topologicalSort()
check sorted.len == 1
check sorted[0] == termId
test "Multiple independent nodes":
## Test topological sort on graph with no edges
var graph = newDependencyGraph()
var profile = newVariantProfile()
profile.addFlag("optimization", "lto")
profile.calculateHash()
# Create three independent terms
let termAId = createTermId("packageA", profile.hash)
let termBId = createTermId("packageB", profile.hash)
let termCId = createTermId("packageC", profile.hash)
let termA = PackageTerm(id: termAId, packageName: "packageA", variantProfile: profile, optional: false, source: "nix")
let termB = PackageTerm(id: termBId, packageName: "packageB", variantProfile: profile, optional: false, source: "nix")
let termC = PackageTerm(id: termCId, packageName: "packageC", variantProfile: profile, optional: false, source: "nix")
graph.addTerm(termA)
graph.addTerm(termB)
graph.addTerm(termC)
# No edges - all independent
let sorted = graph.topologicalSort()
# All nodes should be present
check sorted.len == 3
check termAId in sorted
check termBId in sorted
check termCId in sorted
test "Complex graph with multiple paths":
## Test topological sort on complex graph with multiple dependency paths
var graph = newDependencyGraph()
var profile = newVariantProfile()
profile.addFlag("optimization", "lto")
profile.calculateHash()
# Create terms: A -> B -> D, A -> C -> D, B -> E, C -> E
let termAId = createTermId("packageA", profile.hash)
let termBId = createTermId("packageB", profile.hash)
let termCId = createTermId("packageC", profile.hash)
let termDId = createTermId("packageD", profile.hash)
let termEId = createTermId("packageE", profile.hash)
let termA = PackageTerm(id: termAId, packageName: "packageA", variantProfile: profile, optional: false, source: "nix")
let termB = PackageTerm(id: termBId, packageName: "packageB", variantProfile: profile, optional: false, source: "nix")
let termC = PackageTerm(id: termCId, packageName: "packageC", variantProfile: profile, optional: false, source: "nix")
let termD = PackageTerm(id: termDId, packageName: "packageD", variantProfile: profile, optional: false, source: "nix")
let termE = PackageTerm(id: termEId, packageName: "packageE", variantProfile: profile, optional: false, source: "nix")
graph.addTerm(termA)
graph.addTerm(termB)
graph.addTerm(termC)
graph.addTerm(termD)
graph.addTerm(termE)
# Create edges
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termBId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termCId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termBId, toTerm: termDId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termCId, toTerm: termDId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termBId, toTerm: termEId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termCId, toTerm: termEId, dependencyType: Required))
# Perform topological sort
let sorted = graph.topologicalSort()
# Check that all nodes are present
check sorted.len == 5
# Check dependency constraints
# Installation order: D and E first (no deps), then B and C (depend on D and E), then A (depends on B and C)
let posA = sorted.find(termAId)
let posB = sorted.find(termBId)
let posC = sorted.find(termCId)
let posD = sorted.find(termDId)
let posE = sorted.find(termEId)
# D and E must come before B and C (B and C depend on D and E)
check posD < posB
check posD < posC
check posE < posB
check posE < posC
# B and C must come before A (A depends on B and C)
check posB < posA
check posC < posA
suite "Topological Sort Property Tests":
test "Property: Dependencies always come before dependents":
## Property-based test: For any valid dependency graph,
## the topological sort must place all dependencies before their dependents.
##
## This property validates Requirements 9.1, 9.2, 9.3:
## - 9.1: Produce valid installation order
## - 9.2: Dependencies installed before dependents
## - 9.3: Handle complex dependency patterns
# Test with multiple graph structures
for iteration in 1..20:
var graph = newDependencyGraph()
var profile = newVariantProfile()
profile.addFlag("optimization", "lto")
profile.addFlag("iteration", $iteration)
profile.calculateHash()
# Create a random graph structure
# We'll test different patterns: chains, diamonds, trees
let pattern = iteration mod 4
case pattern:
of 0:
# Chain: A -> B -> C -> D
let termAId = createTermId("packageA", profile.hash)
let termBId = createTermId("packageB", profile.hash)
let termCId = createTermId("packageC", profile.hash)
let termDId = createTermId("packageD", profile.hash)
graph.addTerm(PackageTerm(id: termAId, packageName: "packageA", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termBId, packageName: "packageB", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termCId, packageName: "packageC", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termDId, packageName: "packageD", variantProfile: profile, optional: false, source: "nix"))
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termBId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termBId, toTerm: termCId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termCId, toTerm: termDId, dependencyType: Required))
of 1:
# Diamond: A -> B,C -> D
let termAId = createTermId("packageA", profile.hash)
let termBId = createTermId("packageB", profile.hash)
let termCId = createTermId("packageC", profile.hash)
let termDId = createTermId("packageD", profile.hash)
graph.addTerm(PackageTerm(id: termAId, packageName: "packageA", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termBId, packageName: "packageB", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termCId, packageName: "packageC", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termDId, packageName: "packageD", variantProfile: profile, optional: false, source: "nix"))
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termBId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termCId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termBId, toTerm: termDId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termCId, toTerm: termDId, dependencyType: Required))
of 2:
# Tree: A -> B,C; B -> D,E; C -> F,G
let termAId = createTermId("packageA", profile.hash)
let termBId = createTermId("packageB", profile.hash)
let termCId = createTermId("packageC", profile.hash)
let termDId = createTermId("packageD", profile.hash)
let termEId = createTermId("packageE", profile.hash)
let termFId = createTermId("packageF", profile.hash)
let termGId = createTermId("packageG", profile.hash)
graph.addTerm(PackageTerm(id: termAId, packageName: "packageA", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termBId, packageName: "packageB", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termCId, packageName: "packageC", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termDId, packageName: "packageD", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termEId, packageName: "packageE", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termFId, packageName: "packageF", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termGId, packageName: "packageG", variantProfile: profile, optional: false, source: "nix"))
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termBId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termCId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termBId, toTerm: termDId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termBId, toTerm: termEId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termCId, toTerm: termFId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termCId, toTerm: termGId, dependencyType: Required))
else:
# Complex: Multiple paths and shared dependencies
let termAId = createTermId("packageA", profile.hash)
let termBId = createTermId("packageB", profile.hash)
let termCId = createTermId("packageC", profile.hash)
let termDId = createTermId("packageD", profile.hash)
let termEId = createTermId("packageE", profile.hash)
graph.addTerm(PackageTerm(id: termAId, packageName: "packageA", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termBId, packageName: "packageB", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termCId, packageName: "packageC", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termDId, packageName: "packageD", variantProfile: profile, optional: false, source: "nix"))
graph.addTerm(PackageTerm(id: termEId, packageName: "packageE", variantProfile: profile, optional: false, source: "nix"))
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termBId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termAId, toTerm: termCId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termBId, toTerm: termDId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termCId, toTerm: termDId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termBId, toTerm: termEId, dependencyType: Required))
graph.addEdge(DependencyEdge(fromTerm: termCId, toTerm: termEId, dependencyType: Required))
# Perform topological sort
let sorted = graph.topologicalSort()
# Property: For each node in sorted order, all its dependencies must come before it
for i, termId in sorted:
let outgoingEdges = graph.getOutgoingEdges(termId)
for edge in outgoingEdges:
# edge.toTerm is a dependency of termId
# It must appear before termId in the sorted list
let dependencyPos = sorted.find(edge.toTerm)
# Check: dependency position < dependent position
if dependencyPos >= i:
checkpoint("Iteration: " & $iteration)
checkpoint("Pattern: " & $pattern)
checkpoint("Dependent: " & $termId)
checkpoint("Dependency: " & $edge.toTerm)
checkpoint("Dependent position: " & $i)
checkpoint("Dependency position: " & $dependencyPos)
checkpoint("Sorted order: " & $sorted)
fail()
check dependencyPos < i