nip/tests/test_cdcl_solver.nim

700 lines
20 KiB
Nim
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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