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));
assert!(snapshot.phase.is_none());
}
#[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));
let snapshot = stats.snapshot();
assert_eq!(snapshot.phase_index, 2);
assert_eq!(snapshot.phase_type, "LocalSearch");
assert_eq!(snapshot.step_count, 1);
assert_eq!(snapshot.moves_generated, 7);
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(8));
assert_eq!(snapshot.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");
}
fn provisional_candidate_trace() -> CandidateTraceTelemetry {
CandidateTraceTelemetry::new(
CandidateTraceHeader::new(
"[candidate_trace]\nmax_entries = 4\n".to_string(),
CandidateTraceExecutionPolicy::known(
"test.execution_policy",
std::iter::empty::<(String, String)>(),
),
CandidateTracePhasePlan::opaque("test.runtime.default.pending"),
None,
),
4,
)
}
#[test]
fn candidate_trace_plan_stays_provisional_across_pause_and_resume_until_finalization() {
let mut stats = SolverStats::default();
stats.start();
stats.enable_candidate_trace(provisional_candidate_trace());
let before_pause = stats
.snapshot()
.candidate_trace
.expect("enabled candidate trace");
assert!(!before_pause.has_complete_execution_provenance());
assert!(!before_pause.header.resolved_phase_plan_complete);
let provisional_digest = before_pause.header.resolved_phase_plan_digest;
stats.pause();
let paused = stats
.snapshot()
.candidate_trace
.expect("paused candidate trace");
stats.resume();
let resumed = stats
.snapshot()
.candidate_trace
.expect("resumed candidate trace");
assert_eq!(
paused.header.resolved_phase_plan,
before_pause.header.resolved_phase_plan
);
assert_eq!(paused.header.resolved_phase_plan_digest, provisional_digest);
assert_eq!(
resumed.header.resolved_phase_plan,
before_pause.header.resolved_phase_plan
);
assert_eq!(
resumed.header.resolved_phase_plan_digest,
provisional_digest
);
assert!(!resumed.has_complete_execution_provenance());
let terminal_plan = CandidateTracePhasePlan::known(
"test.runtime.default",
[("terminal", "completed")],
vec![CandidateTracePhasePlan::known(
"test.runtime.list_clarke_wright",
std::iter::empty::<(String, String)>(),
Vec::new(),
)],
);
let expected_digest = CandidateTraceDigest::of_bytes(&terminal_plan.canonical_bytes());
stats.finalize_candidate_trace_resolved_phase_plan(terminal_plan.clone());
let terminal = stats
.snapshot()
.candidate_trace
.expect("terminal candidate trace");
assert_eq!(terminal.header.resolved_phase_plan, terminal_plan);
assert_eq!(terminal.header.resolved_phase_plan_digest, expected_digest);
assert!(terminal.header.resolved_phase_plan_complete);
assert!(terminal.has_complete_execution_provenance());
assert_eq!(
terminal.provenance_status().qualification,
CandidateTraceQualificationStatus::NotRequested
);
assert!(!paused.header.resolved_phase_plan_complete);
assert_eq!(paused.header.resolved_phase_plan_digest, provisional_digest);
}
#[test]
#[should_panic(expected = "candidate trace resolved phase plan may be finalized only once")]
fn candidate_trace_plan_finalization_is_one_way() {
let mut trace = provisional_candidate_trace();
let plan = CandidateTracePhasePlan::known(
"test.runtime.final",
std::iter::empty::<(String, String)>(),
Vec::new(),
);
trace.finalize_resolved_phase_plan(plan.clone());
trace.finalize_resolved_phase_plan(plan);
}