use csp_solver::constraint::{Constraint, ImplicationConstraint, Revision};
use csp_solver::domain::{BitsetDomain, Domain};
use csp_solver::variable::Variable;
use csp_solver::{Csp, Pruning, SolveConfig};
type Impl = ImplicationConstraint<u32>;
#[test]
fn check_vacuous_when_antecedent_not_trigger() {
let c = Impl::new(0, 1, 5, vec![2]);
let assignment: Vec<Option<u32>> = vec![Some(9), Some(7)];
assert!(<Impl as Constraint<BitsetDomain>>::check(&c, &assignment));
}
#[test]
fn check_holds_when_consequent_in_allowed() {
let c = Impl::new(0, 1, 5, vec![2, 3]);
let assignment: Vec<Option<u32>> = vec![Some(5), Some(3)];
assert!(<Impl as Constraint<BitsetDomain>>::check(&c, &assignment));
}
#[test]
fn check_fails_when_triggered_and_consequent_disallowed() {
let c = Impl::new(0, 1, 5, vec![2, 3]);
let assignment: Vec<Option<u32>> = vec![Some(5), Some(7)];
assert!(!<Impl as Constraint<BitsetDomain>>::check(&c, &assignment));
}
#[test]
fn check_vacuous_when_consequent_unbound() {
let c = Impl::new(0, 1, 5, vec![2]);
let assignment: Vec<Option<u32>> = vec![Some(5), None];
assert!(<Impl as Constraint<BitsetDomain>>::check(&c, &assignment));
}
#[test]
fn revise_prunes_consequent_to_allowed_when_triggered() {
let mut vars: Vec<Variable<BitsetDomain>> = vec![
Variable::new(BitsetDomain::new([5])),
Variable::new(BitsetDomain::range(6)),
];
let c = Impl::new(0, 1, 5, vec![2, 3]);
let rev = <Impl as Constraint<BitsetDomain>>::revise(&c, &mut vars, 0);
assert_eq!(rev, Revision::Changed, "triggered revise must prune");
for v in 0u32..6 {
let want = v == 2 || v == 3;
assert_eq!(
vars[1].domain.contains(&v),
want,
"consequent value {v}: expected present={want}"
);
}
assert_eq!(vars[0].domain.singleton_value(), Some(5));
}
#[test]
fn revise_noop_when_antecedent_not_singleton_trigger() {
let mut vars: Vec<Variable<BitsetDomain>> = vec![
Variable::new(BitsetDomain::range(6)),
Variable::new(BitsetDomain::range(6)),
];
let c = Impl::new(0, 1, 5, vec![2, 3]);
let rev = <Impl as Constraint<BitsetDomain>>::revise(&c, &mut vars, 0);
assert_eq!(
rev,
Revision::Unchanged,
"untriggered revise must be a no-op"
);
for v in 0u32..6 {
assert!(vars[1].domain.contains(&v), "consequent lost {v} on no-op");
}
}
#[test]
fn revise_noop_when_singleton_but_not_trigger() {
let mut vars: Vec<Variable<BitsetDomain>> = vec![
Variable::new(BitsetDomain::new([1])),
Variable::new(BitsetDomain::range(6)),
];
let c = Impl::new(0, 1, 5, vec![2, 3]);
let rev = <Impl as Constraint<BitsetDomain>>::revise(&c, &mut vars, 0);
assert_eq!(rev, Revision::Unchanged);
assert_eq!(vars[1].domain.iter().count(), 6, "consequent untouched");
}
#[test]
fn revise_reports_unsatisfiable_on_wipeout() {
let mut vars: Vec<Variable<BitsetDomain>> = vec![
Variable::new(BitsetDomain::new([5])),
Variable::new(BitsetDomain::new([0, 1])),
];
let c = Impl::new(0, 1, 5, vec![7]);
let rev = <Impl as Constraint<BitsetDomain>>::revise(&c, &mut vars, 0);
assert_eq!(
rev,
Revision::Unsatisfiable,
"wiping the consequent must surface Unsatisfiable"
);
assert!(vars[1].domain.is_empty());
}
#[test]
fn solve_end_to_end_pairwise_engine_equality() {
let mut csp: Csp<BitsetDomain> = Csp::new();
let a = csp.add_variable(BitsetDomain::range(3));
let b = csp.add_variable(BitsetDomain::range(3));
let c = csp.add_variable(BitsetDomain::range(3));
let vars = [a, b, c];
for &i in &vars {
for &j in &vars {
if i == j {
continue;
}
for e in 0u32..3 {
csp.add_constraint(Impl::new(i, j, e, vec![e]));
}
}
}
csp.finalize();
let config = SolveConfig {
pruning: Pruning::ForwardChecking,
max_solutions: 1,
..Default::default()
};
let solutions = csp.solve(&config);
assert!(
!solutions.is_empty(),
"pairwise engine-equality chain must be satisfiable"
);
let sol = &solutions[0];
let (va, vb, vc) = (sol[a as usize], sol[b as usize], sol[c as usize]);
assert_eq!(va, vb, "engine vars must agree (a==b), got {va} vs {vb}");
assert_eq!(vb, vc, "engine vars must agree (b==c), got {vb} vs {vc}");
}
#[test]
fn solve_end_to_end_unsat_when_forced_out_of_domain() {
let mut csp: Csp<BitsetDomain> = Csp::new();
let x = csp.add_variable(BitsetDomain::new([1])); let y = csp.add_variable(BitsetDomain::range(3)); csp.add_constraint(Impl::new(x, y, 1, vec![9]));
csp.finalize();
let config = SolveConfig {
pruning: Pruning::ForwardChecking,
max_solutions: 1,
..Default::default()
};
let solutions = csp.solve(&config);
assert!(
solutions.is_empty(),
"an implication forcing the consequent out of its domain must be UNSAT"
);
}