use crate::builder::selector::GroupedScalarSelector;
use crate::heuristic::r#move::Move;
use crate::heuristic::selector::move_selector::{MoveCursor, MoveSelector};
fn scalar_assignment_selector(
value_candidate_limit: Option<usize>,
max_moves_per_step: Option<usize>,
require_hard_improvement: bool,
) -> GroupedScalarSelector<CoveragePlan> {
scalar_assignment_selector_with_model(
assignment_model(),
value_candidate_limit,
max_moves_per_step,
require_hard_improvement,
)
}
fn scalar_assignment_selector_with_model(
model: RuntimeModel<CoveragePlan, usize, DefaultMeter, DefaultMeter>,
value_candidate_limit: Option<usize>,
max_moves_per_step: Option<usize>,
require_hard_improvement: bool,
) -> GroupedScalarSelector<CoveragePlan> {
GroupedScalarSelector::new(
model.scalar_groups()[0].clone(),
value_candidate_limit,
max_moves_per_step,
require_hard_improvement,
)
}
fn repair_move_results(
plan: &CoveragePlan,
selector: &GroupedScalarSelector<CoveragePlan>,
) -> Vec<CoveragePlan> {
let director = CoverageDirector {
working_solution: plan.clone(),
descriptor: coverage_plan_descriptor(),
};
let mut cursor = selector.open_cursor(&director);
let mut results = Vec::new();
while let Some(id) = cursor.next_candidate() {
let mov = cursor.take_candidate(id);
let mut trial = CoverageDirector {
working_solution: plan.clone(),
descriptor: coverage_plan_descriptor(),
};
assert!(mov.is_doable(&trial));
mov.do_move(&mut trial);
trial.calculate_score();
results.push(trial.working_solution);
}
results
}
#[test]
fn scalar_assignment_selector_emits_only_hard_improving_required_moves() {
let plan = coverage_plan(
2,
vec![
coverage_slot(true, 0, None, &[0]),
coverage_slot(true, 0, Some(0), &[0, 1]),
],
);
let selector = scalar_assignment_selector(None, Some(8), true);
let director = CoverageDirector {
working_solution: plan.clone(),
descriptor: coverage_plan_descriptor(),
};
let mut cursor = selector.open_cursor(&director);
let mut emitted = 0;
while let Some(id) = cursor.next_candidate() {
let mov = cursor.take_candidate(id);
let mut trial = CoverageDirector {
working_solution: plan.clone(),
descriptor: coverage_plan_descriptor(),
};
let current = trial.calculate_score();
assert!(mov.requires_hard_improvement());
assert!(mov.is_doable(&trial));
mov.do_move(&mut trial);
let next = trial.calculate_score();
assert!(next.hard() >= current.hard());
emitted += 1;
}
assert!(emitted > 0);
}
#[test]
fn scalar_assignment_repair_falls_back_to_moving_preferred_keeper() {
let plan = coverage_plan(
2,
vec![
coverage_slot(true, 0, Some(0), &[0, 1]),
coverage_slot(true, 0, Some(0), &[0]),
],
);
let selector = scalar_assignment_selector(None, Some(8), true);
let results = repair_move_results(&plan, &selector);
assert!(results.iter().any(|result| {
result.slots[0].assigned == Some(1)
&& result.slots[1].assigned == Some(0)
&& result.score == Some(HardSoftScore::of(0, 0))
}));
}
#[test]
fn scalar_assignment_repair_orders_capacity_conflict_groups_by_coverage_order() {
let plan = coverage_plan(
2,
vec![
coverage_slot(true, 0, Some(1), &[1]),
coverage_slot(false, 0, Some(1), &[1]),
coverage_slot(true, 0, Some(0), &[0]),
coverage_slot(false, 0, Some(0), &[0]),
],
);
let selector = scalar_assignment_selector(None, Some(1), false);
let results = repair_move_results(&plan, &selector);
assert_eq!(results.len(), 1);
assert_eq!(results[0].slots[1].assigned, None);
assert_eq!(results[0].slots[3].assigned, Some(0));
}
#[test]
fn scalar_assignment_repair_honors_value_candidate_limit_when_relocating_blocker() {
let plan = coverage_plan(
2,
vec![
coverage_slot(true, 0, None, &[0]),
coverage_slot(true, 0, Some(0), &[0, 1]),
],
);
let limited_selector = scalar_assignment_selector(Some(1), Some(8), false);
let unlimited_selector = scalar_assignment_selector(None, Some(8), false);
assert!(repair_move_results(&plan, &limited_selector).is_empty());
let unlimited_results = repair_move_results(&plan, &unlimited_selector);
assert!(unlimited_results.iter().any(|result| {
result.slots[0].assigned == Some(0) && result.slots[1].assigned == Some(1)
}));
}
#[test]
fn scalar_assignment_repair_uses_group_cap_when_selector_cap_is_omitted() {
let model = assignment_model_with_limits(ScalarGroupLimits {
max_moves_per_step: Some(1),
max_augmenting_depth: Some(3),
..ScalarGroupLimits::new()
});
let plan = coverage_plan(
3,
vec![
coverage_slot(true, 0, None, &[0, 1, 2]),
coverage_slot(true, 0, None, &[0, 1, 2]),
coverage_slot(true, 0, None, &[0, 1, 2]),
],
);
let selector = scalar_assignment_selector_with_model(model, None, None, false);
assert_eq!(repair_move_results(&plan, &selector).len(), 1);
}
#[test]
fn scalar_assignment_selector_cap_overrides_group_cap() {
let model = assignment_model_with_limits(ScalarGroupLimits {
max_moves_per_step: Some(1),
max_augmenting_depth: Some(3),
..ScalarGroupLimits::new()
});
let plan = coverage_plan(
3,
vec![
coverage_slot(true, 0, None, &[0, 1, 2]),
coverage_slot(true, 0, None, &[0, 1, 2]),
coverage_slot(true, 0, None, &[0, 1, 2]),
],
);
let selector = scalar_assignment_selector_with_model(model, None, Some(2), false);
assert_eq!(repair_move_results(&plan, &selector).len(), 2);
}
#[test]
fn scalar_assignment_rematch_emits_bounded_sequence_swap() {
let model = assignment_model_with_limits(ScalarGroupLimits {
max_rematch_size: Some(2),
..ScalarGroupLimits::new()
});
let plan = coverage_plan(
2,
vec![
coverage_slot(true, 0, Some(0), &[0, 1]),
coverage_slot(true, 0, Some(1), &[0, 1]),
],
);
let selector = scalar_assignment_selector_with_model(model, None, Some(1), false);
let results = repair_move_results(&plan, &selector);
assert_eq!(results.len(), 1);
assert_eq!(results[0].slots[0].assigned, Some(1));
assert_eq!(results[0].slots[1].assigned, Some(0));
}
#[test]
fn scalar_assignment_rematch_orders_sequence_groups_deterministically() {
let model = assignment_model_with_limits(ScalarGroupLimits {
max_rematch_size: Some(2),
..ScalarGroupLimits::new()
});
let crate::builder::ScalarGroupBindingKind::Assignment(assignment) =
model.scalar_groups()[0].kind
else {
panic!("test model should contain an assignment-backed scalar group");
};
let plan = coverage_plan(
2,
vec![
coverage_slot(true, 1, Some(0), &[0, 1]),
coverage_slot(true, 1, Some(1), &[0, 1]),
coverage_slot(true, 0, Some(0), &[0, 1]),
coverage_slot(true, 0, Some(1), &[0, 1]),
],
);
let options = crate::phase::construction::grouped_scalar::ScalarAssignmentMoveOptions::for_selector(
model.scalar_groups()[0].limits,
None,
1,
);
let moves = crate::phase::construction::grouped_scalar::rematch_assignment_moves(
&assignment,
&plan,
options,
);
assert_eq!(moves.len(), 1);
let mut trial = CoverageDirector {
working_solution: plan,
descriptor: coverage_plan_descriptor(),
};
moves[0].do_move(&mut trial);
assert_eq!(trial.working_solution.slots[0].assigned, Some(0));
assert_eq!(trial.working_solution.slots[1].assigned, Some(1));
assert_eq!(trial.working_solution.slots[2].assigned, Some(1));
assert_eq!(trial.working_solution.slots[3].assigned, Some(0));
}
#[test]
fn scalar_assignment_reassignment_emits_bounded_direct_moves() {
let plan = coverage_plan(
3,
vec![coverage_slot(true, 0, Some(0), &[0, 1, 2])],
);
let selector = scalar_assignment_selector(None, Some(1), false);
let results = repair_move_results(&plan, &selector);
assert_eq!(results.len(), 1);
assert_eq!(results[0].slots[0].assigned, Some(1));
}