use std::time::Duration;
use super::*;
use std::cmp::Ordering;
#[test]
fn solver_snapshot_preserves_exact_counts_and_durations() {
let mut stats = SolverStats::default();
stats.start();
stats.record_step();
stats.record_generated_batch(3, Duration::from_millis(4));
stats.record_evaluated_move(Duration::from_millis(5));
stats.record_move_accepted();
stats.record_score_calculation();
let snapshot = stats.snapshot();
assert_eq!(snapshot.step_count, 1);
assert_eq!(snapshot.moves_generated, 3);
assert_eq!(snapshot.moves_evaluated, 1);
assert_eq!(snapshot.moves_accepted, 1);
assert_eq!(snapshot.score_calculations, 1);
assert_eq!(snapshot.generation_time, Duration::from_millis(4));
assert_eq!(snapshot.evaluation_time, Duration::from_millis(5));
}
#[test]
fn phase_stats_track_generation_and_evaluation_separately() {
let mut stats = PhaseStats::new(2, "LocalSearch");
stats.record_step();
stats.record_generated_batch(7, Duration::from_millis(8));
stats.record_evaluated_move(Duration::from_millis(9));
stats.record_move_accepted();
stats.record_score_calculation();
assert_eq!(stats.phase_index, 2);
assert_eq!(stats.phase_type, "LocalSearch");
assert_eq!(stats.step_count, 1);
assert_eq!(stats.moves_generated, 7);
assert_eq!(stats.moves_evaluated, 1);
assert_eq!(stats.moves_accepted, 1);
assert_eq!(stats.score_calculations, 1);
assert_eq!(stats.generation_time(), Duration::from_millis(8));
assert_eq!(stats.evaluation_time(), Duration::from_millis(9));
}
#[test]
fn solver_snapshot_includes_selector_level_telemetry() {
let mut stats = SolverStats::default();
stats.start();
stats.record_selector_generated(2, 3, Duration::from_millis(4));
stats.record_selector_evaluated(2, Duration::from_millis(5));
stats.record_selector_accepted(2);
stats.record_selector_applied(2);
let snapshot = stats.snapshot();
assert_eq!(snapshot.moves_generated, 3);
assert_eq!(snapshot.moves_evaluated, 1);
assert_eq!(snapshot.moves_accepted, 1);
assert_eq!(snapshot.moves_applied, 1);
assert_eq!(snapshot.selector_telemetry.len(), 1);
assert_eq!(snapshot.selector_telemetry[0].selector_index, 2);
assert_eq!(snapshot.selector_telemetry[0].selector_label, "selector-2");
assert_eq!(snapshot.selector_telemetry[0].moves_generated, 3);
assert_eq!(snapshot.selector_telemetry[0].moves_evaluated, 1);
assert_eq!(snapshot.selector_telemetry[0].moves_accepted, 1);
assert_eq!(snapshot.selector_telemetry[0].moves_applied, 1);
}
#[test]
fn solver_snapshot_includes_move_level_telemetry() {
let mut stats = SolverStats::default();
stats.record_move_kind_generated("scalar_assignment_value_run_gap_swap");
stats.record_move_kind_evaluated("scalar_assignment_value_run_gap_swap", Ordering::Greater);
stats.record_move_kind_accepted("scalar_assignment_value_run_gap_swap");
stats.record_move_kind_applied("scalar_assignment_value_run_gap_swap", 25.0);
stats.record_move_kind_forager_ignored("scalar_assignment_value_run_gap_swap", 2);
let snapshot = stats.snapshot();
assert_eq!(snapshot.move_telemetry.len(), 1);
assert_eq!(
snapshot.move_telemetry[0].move_label,
"scalar_assignment_value_run_gap_swap"
);
assert_eq!(snapshot.move_telemetry[0].moves_generated, 1);
assert_eq!(snapshot.move_telemetry[0].moves_evaluated, 1);
assert_eq!(snapshot.move_telemetry[0].moves_accepted, 1);
assert_eq!(snapshot.move_telemetry[0].moves_applied, 1);
assert_eq!(snapshot.move_telemetry[0].moves_score_improving, 1);
assert_eq!(snapshot.move_telemetry[0].moves_forager_ignored, 2);
assert_eq!(snapshot.move_telemetry[0].applied_score_improvement, 25.0);
}
#[test]
fn solver_snapshot_includes_bounded_applied_move_trace() {
let mut stats = SolverStats::default();
stats.record_applied_move_trace(AppliedMoveTelemetry {
step_index: 7,
move_label: "scalar_assignment_value_window_swap",
selected_candidate_index: 42,
moves_generated_this_step: 43,
moves_evaluated_this_step: 43,
moves_accepted_this_step: 1,
moves_forager_ignored_this_step: 0,
score_before: -100.0,
score_after: -80.0,
score_delta: 20.0,
hard_feasible_before: true,
hard_feasible_after: true,
});
let snapshot = stats.snapshot();
assert_eq!(snapshot.applied_move_trace.len(), 1);
assert_eq!(snapshot.applied_move_trace[0].step_index, 7);
assert_eq!(
snapshot.applied_move_trace[0].move_label,
"scalar_assignment_value_window_swap"
);
assert_eq!(snapshot.applied_move_trace[0].selected_candidate_index, 42);
assert_eq!(snapshot.applied_move_trace[0].score_delta, 20.0);
assert!(snapshot.applied_move_trace[0].hard_feasible_after);
}
#[test]
fn compact_solver_snapshot_omits_applied_move_trace() {
let mut stats = SolverStats::default();
stats.record_applied_move_trace(AppliedMoveTelemetry {
step_index: 1,
move_label: "scalar_assignment_value_window_swap",
selected_candidate_index: 0,
hard_feasible_before: true,
hard_feasible_after: true,
..AppliedMoveTelemetry::default()
});
assert_eq!(stats.snapshot().applied_move_trace.len(), 1);
assert!(stats
.snapshot_without_applied_move_trace()
.applied_move_trace
.is_empty());
}
#[test]
fn solver_snapshot_prefers_observed_selector_label() {
let mut stats = SolverStats::default();
stats.record_selector_generated(2, 1, Duration::from_millis(1));
stats.record_selector_generated_with_label(2, "conflict_repair", 1, Duration::from_millis(2));
let snapshot = stats.snapshot();
assert_eq!(
snapshot.selector_telemetry[0].selector_label,
"conflict_repair"
);
assert_eq!(snapshot.selector_telemetry[0].moves_generated, 2);
}
#[test]
fn solver_scalar_assignment_remaining_aggregates_by_group() {
let mut stats = SolverStats::default();
stats.record_scalar_assignment_required_remaining("group_a", 3);
stats.record_scalar_assignment_required_remaining("group_b", 0);
assert_eq!(stats.scalar_assignment_required_remaining, 3);
assert_eq!(stats.snapshot().scalar_assignment_required_remaining, 3);
stats.record_scalar_assignment_required_remaining("group_a", 0);
assert_eq!(stats.scalar_assignment_required_remaining, 0);
assert_eq!(stats.snapshot().scalar_assignment_required_remaining, 0);
}
#[test]
fn unattributed_applied_moves_do_not_create_selector_zero_telemetry() {
let mut stats = SolverStats::default();
stats.record_generated_move(Duration::from_millis(1));
stats.record_evaluated_move(Duration::from_millis(2));
stats.record_move_accepted();
stats.record_move_applied();
let snapshot = stats.snapshot();
assert_eq!(snapshot.moves_generated, 1);
assert_eq!(snapshot.moves_evaluated, 1);
assert_eq!(snapshot.moves_accepted, 1);
assert_eq!(snapshot.moves_applied, 1);
assert!(snapshot.selector_telemetry.is_empty());
}
#[test]
fn throughput_helpers_use_stage_specific_durations() {
let mut solver_stats = SolverStats::default();
solver_stats.start();
solver_stats.record_generated_batch(5, Duration::from_millis(7));
solver_stats.record_evaluated_move(Duration::from_millis(11));
let mut phase_stats = PhaseStats::new(1, "LocalSearch");
phase_stats.record_generated_batch(3, Duration::from_millis(13));
phase_stats.record_evaluated_move(Duration::from_millis(17));
assert_eq!(
solver_stats.generated_throughput(),
Throughput {
count: 5,
elapsed: Duration::from_millis(7),
}
);
assert_eq!(
solver_stats.evaluated_throughput(),
Throughput {
count: 1,
elapsed: Duration::from_millis(11),
}
);
assert_eq!(
phase_stats.generated_throughput(),
Throughput {
count: 3,
elapsed: Duration::from_millis(13),
}
);
assert_eq!(
phase_stats.evaluated_throughput(),
Throughput {
count: 1,
elapsed: Duration::from_millis(17),
}
);
}
#[test]
fn whole_units_per_second_uses_integer_rate_math() {
assert_eq!(whole_units_per_second(3, Duration::from_millis(2_000)), 1);
assert_eq!(whole_units_per_second(9, Duration::from_secs(2)), 4);
assert_eq!(whole_units_per_second(5, Duration::ZERO), 0);
}
#[test]
fn format_duration_uses_exact_integer_units() {
assert_eq!(format_duration(Duration::from_millis(750)), "750ms");
assert_eq!(format_duration(Duration::from_millis(2_500)), "2s 500ms");
assert_eq!(format_duration(Duration::from_secs(125)), "2m 5s");
assert_eq!(format_duration(Duration::from_micros(42)), "42us");
}