## Unit Tests for CDCL Solver ## ## Tests for the Conflict-Driven Clause Learning SAT solver ## adapted for package dependency resolution. ## ## Requirements tested: ## - 5.1: Use PubGrub algorithm with CDCL ## - 5.2: Learn new incompatibility clauses from conflicts ## - 5.3: Backjump to earliest decision causing conflict ## - 5.4: Produce deterministic installation order ## - 5.5: Report minimal conflicting requirements import std/[unittest, tables, options, sequtils, strutils] import ../src/nip/resolver/cdcl_solver import ../src/nip/resolver/cnf_translator import ../src/nip/resolver/solver_types import ../src/nip/resolver/variant_types import ../src/nip/manifest_parser suite "CDCL Solver Tests": test "Create CDCL solver": ## Test creating a CDCL solver with a CNF formula ## Requirements: 5.1 var formula = newCNFFormula() discard formula.translateRootRequirement( package = "nginx", version = SemanticVersion(major: 1, minor: 24, patch: 0), variant = newVariantProfile() ) var solver = newCDCLSolver(formula) check solver.decisionLevel == 0 check solver.assignments.len == 0 check solver.learnedClauses.len == 0 check solver.propagationQueue.len == 0 test "Assign variable": ## Test assigning a value to a variable ## Requirements: 5.1 var formula = newCNFFormula() var solver = newCDCLSolver(formula) let variable = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) solver.assign(variable, true, Decision) check solver.isAssigned(variable) check solver.getValue(variable).isSome check solver.getValue(variable).get() == true let assignment = solver.getAssignment(variable).get() check assignment.value == true check assignment.assignmentType == Decision check assignment.decisionLevel == 0 test "Evaluate literal": ## Test evaluating a literal with current assignments ## Requirements: 5.1 var formula = newCNFFormula() var solver = newCDCLSolver(formula) let variable = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let positiveLit = makeLiteral(variable, isNegated = false) let negativeLit = makeLiteral(variable, isNegated = true) # Before assignment check solver.evaluateLiteral(positiveLit).isNone check solver.evaluateLiteral(negativeLit).isNone # Assign variable to true solver.assign(variable, true, Decision) # After assignment check solver.evaluateLiteral(positiveLit).isSome check solver.evaluateLiteral(positiveLit).get() == true check solver.evaluateLiteral(negativeLit).isSome check solver.evaluateLiteral(negativeLit).get() == false test "Evaluate clause": ## Test evaluating a clause with current assignments ## Requirements: 5.1 var formula = newCNFFormula() var solver = newCDCLSolver(formula) let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let var2 = BoolVar( package: "zlib", version: SemanticVersion(major: 1, minor: 2, patch: 13), variant: newVariantProfile() ) # Clause: ¬var1 ∨ var2 let clause = makeClause(@[ makeLiteral(var1, isNegated = true), makeLiteral(var2, isNegated = false) ]) # Before any assignments check solver.evaluateClause(clause).isNone # Assign var1 = false (satisfies ¬var1) solver.assign(var1, false, Decision) check solver.evaluateClause(clause).isSome check solver.evaluateClause(clause).get() == true # Reset and try different assignment solver.unassign(var1) # Assign var1 = true, var2 = false (falsifies clause) solver.assign(var1, true, Decision) solver.assign(var2, false, Decision) check solver.evaluateClause(clause).isSome check solver.evaluateClause(clause).get() == false test "Detect unit clause": ## Test detecting unit clauses for unit propagation ## Requirements: 5.1 var formula = newCNFFormula() var solver = newCDCLSolver(formula) let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let var2 = BoolVar( package: "zlib", version: SemanticVersion(major: 1, minor: 2, patch: 13), variant: newVariantProfile() ) # Clause: ¬var1 ∨ var2 let clause = makeClause(@[ makeLiteral(var1, isNegated = true), makeLiteral(var2, isNegated = false) ]) # Before assignments - not unit check solver.isUnitClause(clause).isNone # Assign var1 = true (makes ¬var1 false, so var2 must be true) solver.assign(var1, true, Decision) let unitLit = solver.isUnitClause(clause) check unitLit.isSome check unitLit.get().variable == var2 check not unitLit.get().isNegated test "Unit propagation - simple": ## Test basic unit propagation ## Requirements: 5.1 var formula = newCNFFormula() let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) # Add unit clause: var1 formula.addClause(makeClause(@[makeLiteral(var1, isNegated = false)])) var solver = newCDCLSolver(formula) # Unit propagation should assign var1 = true let conflict = solver.unitPropagate() check conflict.isNone check solver.isAssigned(var1) check solver.getValue(var1).get() == true test "Unit propagation - chain": ## Test unit propagation with chained implications ## Requirements: 5.1 var formula = newCNFFormula() let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let var2 = BoolVar( package: "zlib", version: SemanticVersion(major: 1, minor: 2, patch: 13), variant: newVariantProfile() ) let var3 = BoolVar( package: "pcre", version: SemanticVersion(major: 8, minor: 45, patch: 0), variant: newVariantProfile() ) # Add clauses: # 1. var1 (unit clause) # 2. ¬var1 ∨ var2 (if var1 then var2) # 3. ¬var2 ∨ var3 (if var2 then var3) formula.addClause(makeClause(@[makeLiteral(var1, isNegated = false)])) formula.addClause(makeClause(@[ makeLiteral(var1, isNegated = true), makeLiteral(var2, isNegated = false) ])) formula.addClause(makeClause(@[ makeLiteral(var2, isNegated = true), makeLiteral(var3, isNegated = false) ])) var solver = newCDCLSolver(formula) # Unit propagation should assign all three variables let conflict = solver.unitPropagate() check conflict.isNone check solver.isAssigned(var1) check solver.isAssigned(var2) check solver.isAssigned(var3) check solver.getValue(var1).get() == true check solver.getValue(var2).get() == true check solver.getValue(var3).get() == true test "Detect conflict": ## Test conflict detection during unit propagation ## Requirements: 5.1, 5.2 var formula = newCNFFormula() let variable = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) # Add contradictory unit clauses: # 1. variable (must be true) # 2. ¬variable (must be false) formula.addClause(makeClause(@[makeLiteral(variable, isNegated = false)])) formula.addClause(makeClause(@[makeLiteral(variable, isNegated = true)])) var solver = newCDCLSolver(formula) # Unit propagation should detect conflict let conflict = solver.unitPropagate() check conflict.isSome test "Solve satisfiable formula": ## Test solving a simple satisfiable formula ## Requirements: 5.1, 5.4 var formula = newCNFFormula() let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let var2 = BoolVar( package: "zlib", version: SemanticVersion(major: 1, minor: 2, patch: 13), variant: newVariantProfile() ) # Register variables discard formula.getOrCreateVarId(var1) discard formula.getOrCreateVarId(var2) # Add clauses: # 1. var1 ∨ var2 (at least one must be true) formula.addClause(makeClause(@[ makeLiteral(var1, isNegated = false), makeLiteral(var2, isNegated = false) ])) var solver = newCDCLSolver(formula) let result = solver.solve() check result.isSat check result.model.len >= 1 test "Solve unsatisfiable formula": ## Test solving an unsatisfiable formula ## Requirements: 5.1, 5.5 var formula = newCNFFormula() let variable = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) # Add contradictory clauses: # 1. variable (must be true) # 2. ¬variable (must be false) formula.addClause(makeClause(@[makeLiteral(variable, isNegated = false)])) formula.addClause(makeClause(@[makeLiteral(variable, isNegated = true)])) var solver = newCDCLSolver(formula) let result = solver.solve() check not result.isSat test "Backjumping": ## Test backjumping to earlier decision level ## Requirements: 5.3 var formula = newCNFFormula() var solver = newCDCLSolver(formula) let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let var2 = BoolVar( package: "zlib", version: SemanticVersion(major: 1, minor: 2, patch: 13), variant: newVariantProfile() ) let var3 = BoolVar( package: "pcre", version: SemanticVersion(major: 8, minor: 45, patch: 0), variant: newVariantProfile() ) # Make decisions at different levels solver.decisionLevel = 1 solver.assign(var1, true, Decision) solver.decisionLevel = 2 solver.assign(var2, true, Decision) solver.decisionLevel = 3 solver.assign(var3, true, Decision) check solver.assignments.len == 3 check solver.decisionLevel == 3 # Backjump to level 1 solver.backjump(1) check solver.decisionLevel == 1 check solver.assignments.len == 1 check solver.isAssigned(var1) check not solver.isAssigned(var2) check not solver.isAssigned(var3) test "Learn clause from conflict": ## Test learning a new clause from conflict analysis ## Requirements: 5.2 var formula = newCNFFormula() var solver = newCDCLSolver(formula) let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let var2 = BoolVar( package: "zlib", version: SemanticVersion(major: 1, minor: 2, patch: 13), variant: newVariantProfile() ) # Create a conflict solver.decisionLevel = 1 solver.assign(var1, true, Decision) solver.assign(var2, true, Decision) let conflictClause = makeClause(@[ makeLiteral(var1, isNegated = true), makeLiteral(var2, isNegated = true) ]) let conflict = Conflict( clause: conflictClause, assignments: solver.assignments.values.toSeq ) # Analyze conflict and learn let learnedClause = solver.analyzeConflict(conflict) check learnedClause.literals.len > 0 test "Select unassigned variable": ## Test variable selection heuristic ## Requirements: 5.1 var formula = newCNFFormula() let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let var2 = BoolVar( package: "zlib", version: SemanticVersion(major: 1, minor: 2, patch: 13), variant: newVariantProfile() ) discard formula.getOrCreateVarId(var1) discard formula.getOrCreateVarId(var2) var solver = newCDCLSolver(formula) # Both unassigned let selected1 = solver.selectUnassignedVariable() check selected1.isSome # Assign one solver.assign(var1, true, Decision) let selected2 = solver.selectUnassignedVariable() check selected2.isSome check selected2.get() != var1 # Assign both solver.assign(var2, true, Decision) let selected3 = solver.selectUnassignedVariable() check selected3.isNone test "String representations": ## Test string conversion for debugging ## Requirements: 5.5 let variable = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let assignment = SolverAssignment( variable: variable, value: true, assignmentType: Decision, decisionLevel: 1, antecedent: none(Clause) ) let assignmentStr = $assignment check assignmentStr.len > 0 check assignmentStr.contains("nginx") check assignmentStr.contains("decision") test "Conflict analysis - simple conflict": ## Test analyzing a simple conflict ## Requirements: 5.2 var formula = newCNFFormula() var solver = newCDCLSolver(formula) let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) # Make a decision solver.decisionLevel = 1 solver.assign(var1, true, Decision) # Create a conflict clause that conflicts with this decision let conflictClause = makeClause(@[ makeLiteral(var1, isNegated = true) ], reason = "Conflict with var1=true") let conflict = Conflict( clause: conflictClause, assignments: solver.assignments.values.toSeq ) # Analyze the conflict let learnedClause = solver.analyzeConflict(conflict) # Learned clause should prevent this conflict check learnedClause.literals.len > 0 test "Conflict analysis - complex conflict": ## Test analyzing a conflict with multiple decisions ## Requirements: 5.2 var formula = newCNFFormula() var solver = newCDCLSolver(formula) let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let var2 = BoolVar( package: "zlib", version: SemanticVersion(major: 1, minor: 2, patch: 13), variant: newVariantProfile() ) let var3 = BoolVar( package: "pcre", version: SemanticVersion(major: 8, minor: 45, patch: 0), variant: newVariantProfile() ) # Make multiple decisions at different levels solver.decisionLevel = 1 solver.assign(var1, true, Decision) solver.decisionLevel = 2 solver.assign(var2, true, Decision) solver.decisionLevel = 3 solver.assign(var3, true, Decision) # Create a conflict let conflictClause = makeClause(@[ makeLiteral(var1, isNegated = true), makeLiteral(var2, isNegated = true), makeLiteral(var3, isNegated = true) ], reason = "All three conflict") let conflict = Conflict( clause: conflictClause, assignments: solver.assignments.values.toSeq ) # Analyze the conflict let learnedClause = solver.analyzeConflict(conflict) # Learned clause should contain negations of decisions check learnedClause.literals.len > 0 test "Backjump level calculation - simple": ## Test calculating backjump level for a simple learned clause ## Requirements: 5.3 var formula = newCNFFormula() var solver = newCDCLSolver(formula) let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let var2 = BoolVar( package: "zlib", version: SemanticVersion(major: 1, minor: 2, patch: 13), variant: newVariantProfile() ) # Make decisions at different levels solver.decisionLevel = 1 solver.assign(var1, true, Decision) solver.decisionLevel = 2 solver.assign(var2, true, Decision) # Create a learned clause involving both variables let learnedClause = makeClause(@[ makeLiteral(var1, isNegated = true), makeLiteral(var2, isNegated = true) ]) # Find backjump level (should be 1, the second-highest level) let backjumpLevel = solver.findBackjumpLevel(learnedClause) check backjumpLevel == 1 test "Backjump level calculation - complex": ## Test calculating backjump level with multiple decision levels ## Requirements: 5.3 var formula = newCNFFormula() var solver = newCDCLSolver(formula) let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let var2 = BoolVar( package: "zlib", version: SemanticVersion(major: 1, minor: 2, patch: 13), variant: newVariantProfile() ) let var3 = BoolVar( package: "pcre", version: SemanticVersion(major: 8, minor: 45, patch: 0), variant: newVariantProfile() ) let var4 = BoolVar( package: "openssl", version: SemanticVersion(major: 3, minor: 0, patch: 0), variant: newVariantProfile() ) # Make decisions at different levels solver.decisionLevel = 1 solver.assign(var1, true, Decision) solver.decisionLevel = 2 solver.assign(var2, true, Decision) solver.decisionLevel = 3 solver.assign(var3, true, Decision) solver.decisionLevel = 4 solver.assign(var4, true, Decision) # Create a learned clause involving variables at levels 1, 2, and 4 let learnedClause = makeClause(@[ makeLiteral(var1, isNegated = true), makeLiteral(var2, isNegated = true), makeLiteral(var4, isNegated = true) ]) # Find backjump level (should be 2, the second-highest level) let backjumpLevel = solver.findBackjumpLevel(learnedClause) check backjumpLevel == 2 test "Backjump state restoration": ## Test that backjumping correctly restores solver state ## Requirements: 5.3, 5.4 var formula = newCNFFormula() var solver = newCDCLSolver(formula) let var1 = BoolVar( package: "nginx", version: SemanticVersion(major: 1, minor: 24, patch: 0), variant: newVariantProfile() ) let var2 = BoolVar( package: "zlib", version: SemanticVersion(major: 1, minor: 2, patch: 13), variant: newVariantProfile() ) let var3 = BoolVar( package: "pcre", version: SemanticVersion(major: 8, minor: 45, patch: 0), variant: newVariantProfile() ) # Make decisions at different levels solver.decisionLevel = 1 solver.assign(var1, true, Decision) solver.decisionLevel = 2 solver.assign(var2, true, Decision) solver.decisionLevel = 3 solver.assign(var3, true, Decision) check solver.assignments.len == 3 check solver.decisionLevel == 3 # Backjump to level 1 solver.backjump(1) # Check state is correctly restored check solver.decisionLevel == 1 check solver.assignments.len == 1 check solver.isAssigned(var1) check not solver.isAssigned(var2) check not solver.isAssigned(var3) check solver.propagationQueue.len == 0