## 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