use std::time::{Duration, Instant};
use crate::sat::{Bounded, CaDiCal, SearchStats, Status, Terminator, WallClockTerminator};
fn pigeonhole(solver: &mut CaDiCal, pigeons: i32, holes: i32) -> i64 {
let var = |i: i32, h: i32| i * holes + h + 1;
solver.reserve(pigeons * holes);
let mut clauses = 0;
for i in 0..pigeons {
for h in 0..holes {
solver.add(var(i, h));
}
solver.add(0);
clauses += 1;
}
for h in 0..holes {
for i in 0..pigeons {
for j in (i + 1)..pigeons {
solver.add(-var(i, h));
solver.add(-var(j, h));
solver.add(0);
clauses += 1;
}
}
}
clauses
}
fn search_a_little(solver: &mut CaDiCal, conflicts: i32) {
assert!(
solver.limit(c"conflicts", conflicts),
"the solver does not recognise a \"conflicts\" limit",
);
solver.solve();
}
#[test]
fn a_solver_answers_a_formula_and_then_answers_it_again_with_the_answer_ruled_out() {
let mut solver = CaDiCal::new().expect("a solver");
for lit in [1, 2, 0, -1, 2, 0] {
solver.add(lit);
}
assert_eq!(solver.solve(), Status::Satisfiable);
assert!(solver.val(2) > 0, "b must hold in every model");
for lit in [-2, 0] {
solver.add(lit);
}
assert_eq!(solver.solve(), Status::Unsatisfiable);
}
#[test]
fn a_solver_that_has_not_searched_holds_only_the_clauses_it_was_given() {
let mut solver = CaDiCal::new().expect("a solver");
let added = pigeonhole(&mut solver, 4, 3);
assert_eq!(
solver.irredundant(),
added,
"the irredundant count is not the number of clauses added",
);
assert_eq!(
solver.redundant(),
0,
"a solver that has not searched cannot have learnt anything",
);
}
#[test]
fn a_search_that_hits_conflicts_learns_redundant_clauses() {
let mut solver = CaDiCal::new().expect("a solver");
let added = pigeonhole(&mut solver, 6, 5);
search_a_little(&mut solver, 50);
assert!(
solver.redundant() > 0,
"a search that hit conflicts learnt no clause",
);
assert!(
solver.irredundant() <= added,
"searching added irredundant clauses the caller never gave",
);
}
#[test]
fn the_search_counters_report_the_work_between_two_snapshots() {
let mut solver = CaDiCal::new().expect("a solver");
pigeonhole(&mut solver, 6, 5);
let before = solver.search_stats();
search_a_little(&mut solver, 50);
let did = solver.search_stats().since(before);
assert!(did.conflicts > 0, "a bounded search reported no conflicts");
assert!(did.decisions > 0, "a search reported no decisions");
assert!(
did.propagations > 0,
"a search with conflicts reported no propagations",
);
}
#[test]
fn differencing_two_snapshots_the_wrong_way_round_reports_no_work() {
let mut solver = CaDiCal::new().expect("a solver");
pigeonhole(&mut solver, 6, 5);
let before = solver.search_stats();
search_a_little(&mut solver, 50);
let after = solver.search_stats();
assert_eq!(
before.since(after),
SearchStats::default(),
"an earlier snapshot reported work done since a later one",
);
}
#[test]
fn every_counter_slot_reaches_its_own_field() {
assert_eq!(
SearchStats::SLOTS,
6,
"the accessor is asked for a different number of slots than are read",
);
let stats = SearchStats::from_slots([1, 2, 3, 4, 5, 6]);
assert_eq!(stats.conflicts, 1);
assert_eq!(stats.decisions, 2);
assert_eq!(stats.propagations, 3);
assert_eq!(stats.restarts, 4);
assert_eq!(stats.learned_clauses, 5);
assert_eq!(stats.searched, 6);
}
#[test]
fn only_the_variables_a_search_touches_gain_activity() {
let mut solver = CaDiCal::new().expect("a solver");
let (pigeons, holes) = (8, 7);
pigeonhole(&mut solver, pigeons, holes);
let untouched = pigeons * holes + 1;
solver.reserve(untouched);
assert_eq!(
solver.score_of(1),
solver.score_of(untouched),
"a solver that has not searched already separates its variables",
);
search_a_little(&mut solver, 3000);
let busiest = (1..=pigeons * holes)
.map(|v| solver.score_of(v))
.fold(f64::NEG_INFINITY, f64::max);
assert!(
busiest > solver.score_of(untouched),
"no variable the search branched on outscores one it never saw",
);
}
#[test]
fn the_two_literals_over_a_variable_score_the_same() {
let mut solver = CaDiCal::new().expect("a solver");
pigeonhole(&mut solver, 8, 7);
search_a_little(&mut solver, 3000);
for v in 1..=8 * 7 {
assert_eq!(
solver.score_of(v),
solver.score_of(-v),
"variable {v} scores differently through its two literals",
);
}
}
#[test]
fn a_spent_wall_clock_budget_is_expired_immediately() {
let mut wall = WallClockTerminator::new(Duration::ZERO);
assert!(wall.terminated(), "a zero budget had time left in it");
}
#[test]
fn a_wall_clock_deadline_can_be_moved_through_its_handle() {
let mut wall = WallClockTerminator::new(Duration::ZERO);
let bounded_clone = wall.clone();
let handle = wall.deadline_handle();
assert!(wall.terminated(), "a zero budget had time left in it");
handle.set(Instant::now() + Duration::from_secs(60));
assert!(!wall.terminated(), "the extended deadline was not shared");
handle.set(Instant::now());
assert!(wall.terminated(), "the shortened deadline was not shared");
let mut solver = CaDiCal::new().expect("a solver");
pigeonhole(&mut solver, 5, 4);
assert_eq!(
Bounded::new(&mut solver, bounded_clone).solve(),
Status::Unknown,
"the bounded clone did not observe the shortened deadline",
);
}
#[test]
fn the_guard_disconnects_its_terminator_even_when_the_scope_unwinds() {
struct NeverStop;
impl Terminator for NeverStop {
fn terminated(&mut self) -> bool {
false
}
}
let mut solver = CaDiCal::new().expect("a solver");
for lit in [1, 2, 0] {
solver.add(lit);
}
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _bounded = Bounded::new(&mut solver, NeverStop);
panic!("intentional panic inside the bounded region");
}));
assert!(unwound.is_err(), "the panic did not reach the caller");
assert_eq!(solver.solve(), Status::Satisfiable);
}