use super::{
dispatch::{
cancel_in_flight_solve, is_current, queue_or_claim_solve_slot, solve_cancellably,
solving_banner,
},
replan::{stash_current_design, take_matching_design},
runtime::{PendingDispatch, RUNTIME},
scheduling::{
auto_solve_off_note, last_measured_solve_duration, last_solve, record_solve_duration,
reset_for_new_design, should_schedule_auto_solve,
},
};
use indicatrix::geometry::meet_solver::SolveError;
use indicatrix_cut_core::{Design, DesignSolveError};
use indicatrix_editor::solve_policy::should_solve_synchronously;
use std::{
collections::BTreeSet,
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
thread,
time::{Duration, Instant},
};
#[test]
fn zero_budget_disables_auto_solve_regardless_of_measurement() {
assert!(!should_schedule_auto_solve(None, Duration::ZERO));
assert!(!should_schedule_auto_solve(
Some(Duration::from_millis(1)),
Duration::ZERO
));
}
#[test]
fn no_measurement_yet_is_scheduled_optimistically() {
assert!(should_schedule_auto_solve(None, Duration::from_millis(300)));
}
#[test]
fn a_measurement_under_budget_is_scheduled() {
assert!(should_schedule_auto_solve(
Some(Duration::from_millis(120)),
Duration::from_millis(300)
));
}
#[test]
fn a_measurement_at_or_over_budget_is_not_scheduled() {
assert!(!should_schedule_auto_solve(
Some(Duration::from_millis(300)),
Duration::from_millis(300)
));
assert!(!should_schedule_auto_solve(
Some(Duration::from_secs(6)),
Duration::from_millis(300)
));
}
#[test]
fn a_fresh_design_with_few_planes_solves_synchronously() {
assert!(should_solve_synchronously(4, None));
}
#[test]
fn a_fresh_design_with_many_planes_does_not_solve_synchronously() {
assert!(!should_solve_synchronously(210, None));
}
#[test]
fn a_real_fast_measurement_wins_over_a_high_plane_count() {
assert!(should_solve_synchronously(
210,
Some(Duration::from_millis(50))
));
}
#[test]
fn a_real_slow_measurement_loses_even_with_few_planes() {
assert!(!should_solve_synchronously(4, Some(Duration::from_secs(2))));
}
#[test]
fn last_measured_solve_duration_reflects_record_solve_duration() {
reset_for_new_design(0);
assert_eq!(last_measured_solve_duration(), None);
record_solve_duration(Duration::from_millis(77));
assert_eq!(
last_measured_solve_duration(),
Some(Duration::from_millis(77))
);
reset_for_new_design(0);
assert_eq!(last_measured_solve_duration(), None);
}
#[test]
fn last_solve_is_the_production_counterpart_of_last_measured_solve_duration() {
reset_for_new_design(0);
assert_eq!(last_solve(), None);
record_solve_duration(Duration::from_millis(123));
assert_eq!(last_solve(), Some(Duration::from_millis(123)));
assert_eq!(last_solve(), last_measured_solve_duration());
}
#[test]
fn a_freshly_recorded_sequence_number_is_current() {
let seq = RUNTIME.with(|cell| {
let mut rt = cell.borrow_mut();
rt.current_seq += 1;
rt.current_seq
});
assert!(is_current(seq));
}
#[test]
fn an_older_sequence_number_is_superseded_by_a_newer_dispatch() {
let old_seq = RUNTIME.with(|cell| {
let mut rt = cell.borrow_mut();
rt.current_seq += 1;
rt.current_seq
});
RUNTIME.with(|cell| cell.borrow_mut().current_seq += 1);
assert!(!is_current(old_seq));
}
#[test]
fn cancel_in_flight_solve_supersedes_whatever_sequence_was_current() {
let seq = RUNTIME.with(|cell| {
let mut rt = cell.borrow_mut();
rt.current_seq += 1;
rt.current_seq
});
assert!(is_current(seq));
cancel_in_flight_solve();
assert!(
!is_current(seq),
"cancelling must supersede whatever solve was in flight, so its \
eventual completion is dropped as stale"
);
}
#[test]
fn cancel_in_flight_solve_drops_a_queued_pending_dispatch() {
RUNTIME.with(|cell| {
cell.borrow_mut().pending_dispatch = Some(PendingDispatch {
design: fixture_design(),
started_generation: 0,
generation: Arc::new(AtomicU64::new(0)),
multi_selected: BTreeSet::new(),
});
});
cancel_in_flight_solve();
RUNTIME.with(|cell| {
assert!(
cell.borrow().pending_dispatch.is_none(),
"a cancel must drop whatever dispatch was queued behind the \
cancelled solve, not replay it once the in-flight worker \
eventually frees the slot"
);
});
}
#[test]
fn queue_or_claim_solve_slot_captures_started_generation_at_queue_time_not_live() {
RUNTIME.with(|cell| {
let mut rt = cell.borrow_mut();
rt.solve_in_flight = true;
rt.pending_dispatch = None;
});
let generation = Arc::new(AtomicU64::new(5));
let cancel = Arc::new(AtomicBool::new(false));
let design = fixture_design();
let multi_selected = BTreeSet::new();
let queued = queue_or_claim_solve_slot(&design, &generation, 5, &multi_selected, &cancel);
assert!(
queued,
"a solve already in flight must queue, not claim the slot"
);
generation.store(99, Ordering::Relaxed);
RUNTIME.with(|cell| {
let pending_generation = cell
.borrow()
.pending_dispatch
.as_ref()
.map(|p| p.started_generation);
assert_eq!(
pending_generation,
Some(5),
"the queued dispatch must keep the generation it was snapshotted \
at, not read the live counter after a later edit moved it on"
);
});
RUNTIME.with(|cell| {
let mut rt = cell.borrow_mut();
rt.solve_in_flight = false;
rt.pending_dispatch = None;
rt.current_cancel = None;
});
}
#[test]
fn cancel_in_flight_solve_drops_the_pending_debounce() {
RUNTIME.with(|cell| cell.borrow_mut().debounce = Some(slint::Timer::default()));
cancel_in_flight_solve();
RUNTIME.with(|cell| {
assert!(
cell.borrow().debounce.is_none(),
"a cancel must also drop whatever debounced auto-solve was pending"
);
});
}
#[test]
fn reset_for_new_design_drops_the_idle_replan_timer() {
RUNTIME.with(|cell| cell.borrow_mut().idle_replan = Some(slint::Timer::default()));
reset_for_new_design(0);
RUNTIME.with(|cell| {
assert!(
cell.borrow().idle_replan.is_none(),
"a wholesale New/Load must drop a partial-frame idle-replan timer \
armed for whatever design it is replacing -- otherwise that timer \
can fire later and resubmit the OLD design's stashed masts on top \
of the one just loaded"
);
});
}
#[test]
fn cancel_in_flight_solve_drops_the_idle_replan_timer() {
RUNTIME.with(|cell| cell.borrow_mut().idle_replan = Some(slint::Timer::default()));
cancel_in_flight_solve();
RUNTIME.with(|cell| {
assert!(
cell.borrow().idle_replan.is_none(),
"cancelling a solve must also drop whatever idle-replan timer was pending"
);
});
}
#[test]
fn cancel_in_flight_solve_leaves_the_measurement_and_design_stash_untouched() {
reset_for_new_design(0);
record_solve_duration(Duration::from_millis(250));
stash_current_design(3, Arc::new(fixture_design()), BTreeSet::new());
cancel_in_flight_solve();
assert_eq!(
last_measured_solve_duration(),
Some(Duration::from_millis(250)),
"cancelling an in-flight solve must not discard this design's own \
last REAL measurement -- unlike reset_for_new_design, no different \
design has just replaced it"
);
assert!(
take_matching_design(3).is_some(),
"cancelling an in-flight solve must not clear an unrelated stash \
from a concurrent edit's own submit_preview_replan"
);
}
#[test]
fn record_solve_duration_is_visible_to_the_next_schedule_decision() {
record_solve_duration(Duration::from_millis(42));
let last = RUNTIME.with(|cell| cell.borrow().last_solve);
assert_eq!(last, Some(Duration::from_millis(42)));
reset_for_new_design(0);
let last = RUNTIME.with(|cell| cell.borrow().last_solve);
assert_eq!(last, None);
}
#[test]
fn solving_banner_pluralizes_the_tier_count() {
assert!(solving_banner(1, Duration::ZERO).contains("1 tier)"));
assert!(solving_banner(2, Duration::ZERO).contains("2 tiers)"));
}
#[test]
fn auto_solve_off_note_reports_the_measured_seconds() {
let text = auto_solve_off_note(Duration::from_millis(5900));
assert!(text.contains("5.9s"));
}
fn fixture_design() -> Design {
Design::fresh(
indicatrix_cut_core::PreformSpec::cylinder(96, 1.5, 1.0, 1.5),
96,
8,
1.54,
)
}
#[test]
fn take_matching_design_returns_none_when_nothing_stashed() {
reset_for_new_design(0);
assert!(take_matching_design(1).is_none());
}
#[test]
fn take_matching_design_returns_the_stash_for_its_own_generation() {
stash_current_design(5, Arc::new(fixture_design()), BTreeSet::from([2]));
let (design, multi_selected) = take_matching_design(5).expect("stashed at generation 5");
assert!(
design.tiers.is_empty(),
"a fresh design starts with no tiers"
);
assert_eq!(multi_selected, BTreeSet::from([2]));
}
#[test]
fn take_matching_design_only_matches_once_per_stash() {
stash_current_design(11, Arc::new(fixture_design()), BTreeSet::new());
assert!(
take_matching_design(11).is_some(),
"the first call must match"
);
assert!(
take_matching_design(11).is_none(),
"a second call for the same generation must not match again"
);
}
#[test]
fn a_second_frame_of_the_same_generation_finds_the_stash_taken() {
stash_current_design(9, Arc::new(fixture_design()), BTreeSet::new());
stash_current_design(9, Arc::new(fixture_design()), BTreeSet::from([1]));
let (_, multi_selected) = take_matching_design(9).expect("the first frame lands");
assert_eq!(
multi_selected,
BTreeSet::from([1]),
"the later stash overwrote the earlier one"
);
assert!(
take_matching_design(9).is_none(),
"the second frame of the generation claims nothing, so it pushes no rows"
);
}
#[test]
fn take_matching_design_rejects_a_superseded_generation() {
stash_current_design(5, Arc::new(fixture_design()), BTreeSet::new());
assert!(
take_matching_design(6).is_none(),
"a newer generation must not match an older stash -- an edit landed \
after the frame asking for generation 6 was submitted"
);
}
#[test]
fn stash_current_design_shares_the_arc_rather_than_deep_cloning() {
let snapshot = Arc::new(fixture_design());
assert_eq!(Arc::strong_count(&snapshot), 1);
stash_current_design(7, Arc::clone(&snapshot), BTreeSet::new());
assert_eq!(
Arc::strong_count(&snapshot),
2,
"the stash must hold a SHARED clone of the same allocation, not a deep copy"
);
let (taken, _) = take_matching_design(7).expect("stashed at generation 7");
assert!(
Arc::ptr_eq(&snapshot, &taken),
"take_matching_design must hand back the SAME allocation stash_current_design \
was given, never a fresh deep clone"
);
}
const CRACKOTTO_STEP_ASC: &str = include_str!(
"../../../../../../crates/indicatrix-cut-core/src/optimize_cost_probe_crackotto_step.asc"
);
fn crackotto_step_with_real_meet_structure() -> Design {
use indicatrix::geometry::meet_solver::{Block, classify_blocks};
let schedule =
indicatrix_formats::asc::parse_asc(CRACKOTTO_STEP_ASC).expect("fixture must parse");
let mut design = Design::from_asc_schedule(
indicatrix_cut_core::PreformSpec::block(2.0, 1.0, 2.0),
&schedule,
);
let inputs = design.meet_tier_inputs();
let blocks = classify_blocks(&inputs);
let mut anchor_kept = [false; 3];
let slot = |b: Block| match b {
Block::Crown => 0,
Block::Pavilion => 1,
Block::Girdle => 2,
};
for (i, tier) in design.tiers.iter_mut().enumerate() {
let s = slot(blocks[i]);
if !anchor_kept[s] {
anchor_kept[s] = true;
continue;
}
if let Some(original) = tier.imported_meet.clone() {
tier.constraint = original;
}
}
design
}
#[test]
fn solve_cancellably_stops_within_100ms_on_the_largest_real_fixture() {
let design = crackotto_step_with_real_meet_structure();
let cancel = Arc::new(AtomicBool::new(false));
let cancel_for_thread = Arc::clone(&cancel);
let worker = thread::spawn(move || solve_cancellably(&design, &cancel_for_thread));
thread::sleep(Duration::from_millis(50));
let cancel_requested_at = Instant::now();
cancel.store(true, Ordering::Relaxed);
let result = worker.join().expect("worker thread must not panic");
let cancel_latency = cancel_requested_at.elapsed();
assert!(
cancel_latency < Duration::from_millis(100),
"cancel took {cancel_latency:?}, want < 100ms (full uncancelled solve is ~5.9s)"
);
assert!(
matches!(result, Err(DesignSolveError::Solve(SolveError::Cancelled))),
"expected a cancelled solve"
);
}
#[test]
fn take_matching_design_drops_the_pending_debounce_on_a_match() {
stash_current_design(9, Arc::new(fixture_design()), BTreeSet::new());
RUNTIME.with(|cell| cell.borrow_mut().debounce = Some(slint::Timer::default()));
assert!(take_matching_design(9).is_some());
RUNTIME.with(|cell| {
assert!(
cell.borrow().debounce.is_none(),
"a matching frame must cancel whatever debounced auto-solve was pending"
);
});
}