Skip to main content

solverforge_solver/phase/construction/
phase.rs

1// Construction heuristic phase implementation.
2
3use std::any::Any;
4use std::fmt::Debug;
5use std::marker::PhantomData;
6
7use solverforge_core::domain::PlanningSolution;
8use solverforge_core::score::Score;
9use solverforge_scoring::{Director, RecordingDirector};
10use tracing::info;
11
12use crate::heuristic::r#move::Move;
13use crate::phase::construction::{
14    BestFitForager, ConstructionForager, EntityPlacer, FirstFeasibleForager, FirstFitForager,
15};
16use crate::phase::control::{
17    settle_construction_interrupt, should_interrupt_evaluation, StepInterrupt,
18};
19use crate::phase::Phase;
20use crate::scope::ProgressCallback;
21use crate::scope::{PhaseScope, SolverScope, StepScope};
22
23/// Construction heuristic phase that builds an initial solution.
24///
25/// This phase iterates over uninitialized entities and assigns values
26/// to their planning variables using a greedy approach.
27///
28/// # Type Parameters
29/// * `S` - The planning solution type
30/// * `M` - The move type
31/// * `P` - The entity placer type
32/// * `Fo` - The forager type
33pub struct ConstructionHeuristicPhase<S, M, P, Fo>
34where
35    S: PlanningSolution,
36    M: Move<S>,
37    P: EntityPlacer<S, M>,
38    Fo: ConstructionForager<S, M>,
39{
40    placer: P,
41    forager: Fo,
42    _phantom: PhantomData<fn() -> (S, M)>,
43}
44
45impl<S, M, P, Fo> ConstructionHeuristicPhase<S, M, P, Fo>
46where
47    S: PlanningSolution,
48    M: Move<S>,
49    P: EntityPlacer<S, M>,
50    Fo: ConstructionForager<S, M>,
51{
52    pub fn new(placer: P, forager: Fo) -> Self {
53        Self {
54            placer,
55            forager,
56            _phantom: PhantomData,
57        }
58    }
59}
60
61impl<S, M, P, Fo> Debug for ConstructionHeuristicPhase<S, M, P, Fo>
62where
63    S: PlanningSolution,
64    M: Move<S>,
65    P: EntityPlacer<S, M> + Debug,
66    Fo: ConstructionForager<S, M> + Debug,
67{
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.debug_struct("ConstructionHeuristicPhase")
70            .field("placer", &self.placer)
71            .field("forager", &self.forager)
72            .finish()
73    }
74}
75
76impl<S, D, BestCb, M, P, Fo> Phase<S, D, BestCb> for ConstructionHeuristicPhase<S, M, P, Fo>
77where
78    S: PlanningSolution,
79    D: Director<S>,
80    BestCb: ProgressCallback<S>,
81    M: Move<S> + 'static,
82    P: EntityPlacer<S, M>,
83    Fo: ConstructionForager<S, M> + 'static,
84{
85    fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
86        let mut phase_scope = PhaseScope::new(solver_scope, 0);
87        let phase_index = phase_scope.phase_index();
88
89        info!(
90            event = "phase_start",
91            phase = "Construction Heuristic",
92            phase_index = phase_index,
93        );
94
95        // Get all placements (entities that need values assigned)
96        let mut placements = self
97            .placer
98            .get_placements(phase_scope.score_director())
99            .into_iter();
100        let mut pending_placement = None;
101
102        loop {
103            // Construction must complete — only stop for external flag or time limit,
104            // never for step/move count limits (those are for local search).
105            if phase_scope
106                .solver_scope_mut()
107                .should_terminate_construction()
108            {
109                break;
110            }
111
112            let mut placement = match pending_placement.take().or_else(|| placements.next()) {
113                Some(placement) => placement,
114                None => break,
115            };
116
117            // Record move evaluations at call-site (Option C: maintains trait purity)
118            // BestFitForager evaluates ALL moves in the placement
119            let moves_in_placement = placement.moves.len() as u64;
120
121            let mut step_scope = StepScope::new(&mut phase_scope);
122
123            // Use forager to pick the best move index for this placement
124            let selected_idx = match select_move_index(&self.forager, &placement, &mut step_scope) {
125                ConstructionSelection::Selected(selected_idx) => selected_idx,
126                ConstructionSelection::Interrupted => {
127                    match settle_construction_interrupt(&mut step_scope) {
128                        StepInterrupt::Restart => {
129                            pending_placement = Some(placement);
130                            continue;
131                        }
132                        StepInterrupt::TerminatePhase => break,
133                    }
134                }
135            };
136
137            // Record all moves as evaluated, with one accepted if selection succeeded
138            for i in 0..moves_in_placement {
139                let accepted = selected_idx == Some(i as usize);
140                step_scope
141                    .phase_scope_mut()
142                    .solver_scope_mut()
143                    .stats_mut()
144                    .record_move(accepted);
145            }
146
147            if let Some(idx) = selected_idx {
148                // Take ownership of the move
149                let m = placement.take_move(idx);
150
151                // Execute the move
152                m.do_move(step_scope.score_director_mut());
153
154                // Calculate and record the step score
155                let step_score = step_scope.calculate_score();
156                step_scope.set_step_score(step_score);
157            }
158
159            step_scope.complete();
160        }
161
162        // Update best solution at end of phase
163        phase_scope.update_best_solution();
164
165        let best_score = phase_scope
166            .solver_scope()
167            .best_score()
168            .map(|s| format!("{}", s))
169            .unwrap_or_else(|| "none".to_string());
170
171        let duration = phase_scope.elapsed();
172        let steps = phase_scope.step_count();
173        let speed = if duration.as_secs_f64() > 0.0 {
174            (steps as f64 / duration.as_secs_f64()) as u64
175        } else {
176            0
177        };
178        let stats = phase_scope.solver_scope().stats();
179
180        info!(
181            event = "phase_end",
182            phase = "Construction Heuristic",
183            phase_index = phase_index,
184            duration_ms = duration.as_millis() as u64,
185            steps = steps,
186            moves_evaluated = stats.moves_evaluated,
187            moves_accepted = stats.moves_accepted,
188            score_calculations = stats.score_calculations,
189            speed = speed,
190            score = best_score,
191        );
192    }
193
194    fn phase_type_name(&self) -> &'static str {
195        "ConstructionHeuristic"
196    }
197}
198
199enum ConstructionSelection {
200    Selected(Option<usize>),
201    Interrupted,
202}
203
204fn select_move_index<S, D, BestCb, M, Fo>(
205    forager: &Fo,
206    placement: &crate::phase::construction::Placement<S, M>,
207    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
208) -> ConstructionSelection
209where
210    S: PlanningSolution,
211    S::Score: Score,
212    D: Director<S>,
213    BestCb: ProgressCallback<S>,
214    M: Move<S> + 'static,
215    Fo: ConstructionForager<S, M> + 'static,
216{
217    let erased = forager as &dyn Any;
218
219    if erased.is::<FirstFitForager<S, M>>() {
220        return select_first_fit_index(placement, step_scope);
221    }
222    if erased.is::<BestFitForager<S, M>>() {
223        return select_best_fit_index(placement, step_scope);
224    }
225    if erased.is::<FirstFeasibleForager<S, M>>() {
226        return select_first_feasible_index(placement, step_scope);
227    }
228
229    ConstructionSelection::Selected(
230        forager.pick_move_index(placement, step_scope.score_director_mut()),
231    )
232}
233
234fn select_first_fit_index<S, D, BestCb, M>(
235    placement: &crate::phase::construction::Placement<S, M>,
236    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
237) -> ConstructionSelection
238where
239    S: PlanningSolution,
240    D: Director<S>,
241    BestCb: ProgressCallback<S>,
242    M: Move<S> + 'static,
243{
244    for (idx, m) in placement.moves.iter().enumerate() {
245        if should_interrupt_evaluation(step_scope, idx) {
246            return ConstructionSelection::Interrupted;
247        }
248        if m.is_doable(step_scope.score_director()) {
249            return ConstructionSelection::Selected(Some(idx));
250        }
251    }
252    ConstructionSelection::Selected(None)
253}
254
255fn select_best_fit_index<S, D, BestCb, M>(
256    placement: &crate::phase::construction::Placement<S, M>,
257    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
258) -> ConstructionSelection
259where
260    S: PlanningSolution,
261    S::Score: Score,
262    D: Director<S>,
263    BestCb: ProgressCallback<S>,
264    M: Move<S> + 'static,
265{
266    let mut best_idx: Option<usize> = None;
267    let mut best_score: Option<S::Score> = None;
268
269    for (idx, m) in placement.moves.iter().enumerate() {
270        if should_interrupt_evaluation(step_scope, idx) {
271            return ConstructionSelection::Interrupted;
272        }
273        if !m.is_doable(step_scope.score_director()) {
274            continue;
275        }
276
277        let score = {
278            let mut recording = RecordingDirector::new(step_scope.score_director_mut());
279            m.do_move(&mut recording);
280            let score = recording.calculate_score();
281            recording.undo_changes();
282            score
283        };
284
285        let is_better = match &best_score {
286            None => true,
287            Some(best) => score > *best,
288        };
289        if is_better {
290            best_idx = Some(idx);
291            best_score = Some(score);
292        }
293    }
294
295    ConstructionSelection::Selected(best_idx)
296}
297
298fn select_first_feasible_index<S, D, BestCb, M>(
299    placement: &crate::phase::construction::Placement<S, M>,
300    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
301) -> ConstructionSelection
302where
303    S: PlanningSolution,
304    S::Score: Score,
305    D: Director<S>,
306    BestCb: ProgressCallback<S>,
307    M: Move<S> + 'static,
308{
309    let mut fallback_idx: Option<usize> = None;
310    let mut fallback_score: Option<S::Score> = None;
311
312    for (idx, m) in placement.moves.iter().enumerate() {
313        if should_interrupt_evaluation(step_scope, idx) {
314            return ConstructionSelection::Interrupted;
315        }
316        if !m.is_doable(step_scope.score_director()) {
317            continue;
318        }
319
320        let score = {
321            let mut recording = RecordingDirector::new(step_scope.score_director_mut());
322            m.do_move(&mut recording);
323            let score = recording.calculate_score();
324            recording.undo_changes();
325            score
326        };
327
328        if score.is_feasible() {
329            return ConstructionSelection::Selected(Some(idx));
330        }
331
332        let is_better = match &fallback_score {
333            None => true,
334            Some(best) => score > *best,
335        };
336        if is_better {
337            fallback_idx = Some(idx);
338            fallback_score = Some(score);
339        }
340    }
341
342    ConstructionSelection::Selected(fallback_idx)
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use std::any::TypeId;
349    use std::sync::atomic::{AtomicUsize, Ordering};
350    use std::sync::{Arc, Condvar, Mutex};
351
352    use solverforge_core::domain::SolutionDescriptor;
353    use solverforge_core::score::SoftScore;
354    use solverforge_scoring::Director;
355
356    use crate::heuristic::selector::EntityReference;
357    use crate::heuristic::selector::{FromSolutionEntitySelector, StaticValueSelector};
358    use crate::manager::{
359        Solvable, SolverEvent, SolverLifecycleState, SolverManager, SolverRuntime,
360        SolverTerminalReason,
361    };
362    use crate::phase::construction::{
363        BestFitForager, FirstFitForager, Placement, QueuedEntityPlacer,
364    };
365    use crate::test_utils::{
366        create_simple_nqueens_director, get_queen_row, set_queen_row, NQueensSolution,
367    };
368
369    fn create_placer(
370        values: Vec<i64>,
371    ) -> QueuedEntityPlacer<
372        NQueensSolution,
373        i64,
374        FromSolutionEntitySelector,
375        StaticValueSelector<NQueensSolution, i64>,
376    > {
377        let es = FromSolutionEntitySelector::new(0);
378        let vs = StaticValueSelector::new(values);
379        QueuedEntityPlacer::new(es, vs, get_queen_row, set_queen_row, 0, "row")
380    }
381
382    #[derive(Clone, Debug)]
383    struct BlockingPoint {
384        state: Arc<(Mutex<BlockingPointState>, Condvar)>,
385    }
386
387    #[derive(Debug)]
388    struct BlockingPointState {
389        blocked: bool,
390        released: bool,
391    }
392
393    impl BlockingPoint {
394        fn new() -> Self {
395            Self {
396                state: Arc::new((
397                    Mutex::new(BlockingPointState {
398                        blocked: false,
399                        released: false,
400                    }),
401                    Condvar::new(),
402                )),
403            }
404        }
405
406        fn block(&self) {
407            let (lock, condvar) = &*self.state;
408            let mut state = lock.lock().unwrap();
409            state.blocked = true;
410            condvar.notify_all();
411            while !state.released {
412                state = condvar.wait(state).unwrap();
413            }
414        }
415
416        fn wait_until_blocked(&self) {
417            let (lock, condvar) = &*self.state;
418            let mut state = lock.lock().unwrap();
419            while !state.blocked {
420                state = condvar.wait(state).unwrap();
421            }
422        }
423
424        fn release(&self) {
425            let (lock, condvar) = &*self.state;
426            let mut state = lock.lock().unwrap();
427            state.released = true;
428            condvar.notify_all();
429        }
430    }
431
432    #[derive(Clone, Debug)]
433    struct BlockingEvaluationGate {
434        block_at: usize,
435        seen: Arc<AtomicUsize>,
436        blocker: BlockingPoint,
437    }
438
439    impl BlockingEvaluationGate {
440        fn new(block_at: usize) -> Self {
441            Self {
442                block_at,
443                seen: Arc::new(AtomicUsize::new(0)),
444                blocker: BlockingPoint::new(),
445            }
446        }
447
448        fn on_evaluation(&self) {
449            let seen = self.seen.fetch_add(1, Ordering::SeqCst) + 1;
450            if seen == self.block_at {
451                self.blocker.block();
452            }
453        }
454
455        fn wait_until_blocked(&self) {
456            self.blocker.wait_until_blocked();
457        }
458
459        fn release(&self) {
460            self.blocker.release();
461        }
462    }
463
464    #[derive(Clone, Debug)]
465    struct ConstructionPauseEntity {
466        value: Option<i64>,
467    }
468
469    #[derive(Clone, Debug)]
470    struct ConstructionPauseSolution {
471        entities: Vec<ConstructionPauseEntity>,
472        score: Option<SoftScore>,
473        eval_gate: Option<BlockingEvaluationGate>,
474    }
475
476    impl ConstructionPauseSolution {
477        fn new(eval_gate: Option<BlockingEvaluationGate>) -> Self {
478            Self {
479                entities: vec![ConstructionPauseEntity { value: None }],
480                score: None,
481                eval_gate,
482            }
483        }
484    }
485
486    impl PlanningSolution for ConstructionPauseSolution {
487        type Score = SoftScore;
488
489        fn score(&self) -> Option<Self::Score> {
490            self.score
491        }
492
493        fn set_score(&mut self, score: Option<Self::Score>) {
494            self.score = score;
495        }
496    }
497
498    #[derive(Clone, Debug)]
499    struct ConstructionPauseDirector {
500        working_solution: ConstructionPauseSolution,
501        descriptor: SolutionDescriptor,
502    }
503
504    impl ConstructionPauseDirector {
505        fn new(solution: ConstructionPauseSolution) -> Self {
506            Self {
507                working_solution: solution,
508                descriptor: SolutionDescriptor::new(
509                    "ConstructionPauseSolution",
510                    TypeId::of::<ConstructionPauseSolution>(),
511                ),
512            }
513        }
514    }
515
516    impl Director<ConstructionPauseSolution> for ConstructionPauseDirector {
517        fn working_solution(&self) -> &ConstructionPauseSolution {
518            &self.working_solution
519        }
520
521        fn working_solution_mut(&mut self) -> &mut ConstructionPauseSolution {
522            &mut self.working_solution
523        }
524
525        fn calculate_score(&mut self) -> SoftScore {
526            let score = SoftScore::of(
527                self.working_solution
528                    .entities
529                    .iter()
530                    .filter_map(|entity| entity.value)
531                    .sum(),
532            );
533            self.working_solution.set_score(Some(score));
534            score
535        }
536
537        fn solution_descriptor(&self) -> &SolutionDescriptor {
538            &self.descriptor
539        }
540
541        fn clone_working_solution(&self) -> ConstructionPauseSolution {
542            self.working_solution.clone()
543        }
544
545        fn before_variable_changed(&mut self, _descriptor_index: usize, _entity_index: usize) {}
546
547        fn after_variable_changed(&mut self, _descriptor_index: usize, _entity_index: usize) {}
548
549        fn entity_count(&self, descriptor_index: usize) -> Option<usize> {
550            (descriptor_index == 0).then_some(self.working_solution.entities.len())
551        }
552
553        fn total_entity_count(&self) -> Option<usize> {
554            Some(self.working_solution.entities.len())
555        }
556    }
557
558    #[derive(Clone, Debug)]
559    struct ConstructionPauseMove {
560        entity_index: usize,
561        entity_indices: [usize; 1],
562        value: i64,
563        doable: bool,
564        eval_gate: Option<BlockingEvaluationGate>,
565    }
566
567    impl ConstructionPauseMove {
568        fn new(
569            entity_index: usize,
570            value: i64,
571            doable: bool,
572            eval_gate: Option<BlockingEvaluationGate>,
573        ) -> Self {
574            Self {
575                entity_index,
576                entity_indices: [entity_index],
577                value,
578                doable,
579                eval_gate,
580            }
581        }
582    }
583
584    impl Move<ConstructionPauseSolution> for ConstructionPauseMove {
585        fn is_doable<D: Director<ConstructionPauseSolution>>(&self, _score_director: &D) -> bool {
586            if let Some(gate) = &self.eval_gate {
587                gate.on_evaluation();
588            }
589            self.doable
590        }
591
592        fn do_move<D: Director<ConstructionPauseSolution>>(&self, score_director: &mut D) {
593            score_director.working_solution_mut().entities[self.entity_index].value =
594                Some(self.value);
595        }
596
597        fn descriptor_index(&self) -> usize {
598            0
599        }
600
601        fn entity_indices(&self) -> &[usize] {
602            &self.entity_indices
603        }
604
605        fn variable_name(&self) -> &str {
606            "value"
607        }
608    }
609
610    #[derive(Clone, Debug)]
611    struct ConstructionPausePlacer {
612        eval_gate: Option<BlockingEvaluationGate>,
613    }
614
615    impl ConstructionPausePlacer {
616        fn new(eval_gate: Option<BlockingEvaluationGate>) -> Self {
617            Self { eval_gate }
618        }
619    }
620
621    impl EntityPlacer<ConstructionPauseSolution, ConstructionPauseMove> for ConstructionPausePlacer {
622        fn get_placements<D: Director<ConstructionPauseSolution>>(
623            &self,
624            score_director: &D,
625        ) -> Vec<Placement<ConstructionPauseSolution, ConstructionPauseMove>> {
626            score_director
627                .working_solution()
628                .entities
629                .iter()
630                .enumerate()
631                .filter_map(|(entity_index, entity)| {
632                    if entity.value.is_some() {
633                        return None;
634                    }
635
636                    let moves = (0..65)
637                        .map(|value| {
638                            ConstructionPauseMove::new(
639                                entity_index,
640                                value as i64,
641                                value == 64,
642                                (value == 0).then(|| self.eval_gate.clone()).flatten(),
643                            )
644                        })
645                        .collect();
646
647                    Some(Placement::new(EntityReference::new(0, entity_index), moves))
648                })
649                .collect()
650        }
651    }
652
653    impl Solvable for ConstructionPauseSolution {
654        fn solve(self, runtime: SolverRuntime<Self>) {
655            let eval_gate = self.eval_gate.clone();
656            let mut solver_scope = SolverScope::new_with_callback(
657                ConstructionPauseDirector::new(self),
658                (),
659                None,
660                Some(runtime),
661            );
662
663            solver_scope.start_solving();
664
665            let mut phase = ConstructionHeuristicPhase::new(
666                ConstructionPausePlacer::new(eval_gate),
667                FirstFitForager::new(),
668            );
669            phase.solve(&mut solver_scope);
670
671            let mut current_score = solver_scope.current_score().copied();
672            let best_score = if let Some(best_score) = solver_scope.best_score().copied() {
673                best_score
674            } else {
675                let score = solver_scope.calculate_score();
676                current_score.get_or_insert(score);
677                score
678            };
679
680            let telemetry = solver_scope.stats().snapshot();
681            let solution = solver_scope.score_director().clone_working_solution();
682
683            if runtime.is_cancel_requested() {
684                runtime.emit_cancelled(current_score, Some(best_score), telemetry);
685            } else {
686                runtime.emit_completed(
687                    solution,
688                    current_score,
689                    best_score,
690                    telemetry,
691                    SolverTerminalReason::Completed,
692                );
693            }
694        }
695    }
696
697    #[test]
698    fn test_construction_first_fit() {
699        let director = create_simple_nqueens_director(4);
700        let mut solver_scope = SolverScope::new(director);
701        solver_scope.start_solving();
702
703        let values: Vec<i64> = (0..4).collect();
704        let placer = create_placer(values);
705        let forager = FirstFitForager::new();
706        let mut phase = ConstructionHeuristicPhase::new(placer, forager);
707
708        phase.solve(&mut solver_scope);
709
710        let solution = solver_scope.working_solution();
711        assert_eq!(solution.queens.len(), 4);
712        for queen in &solution.queens {
713            assert!(queen.row.is_some(), "Queen should have a row assigned");
714        }
715
716        assert!(solver_scope.best_solution().is_some());
717        // Verify stats were recorded
718        assert!(solver_scope.stats().moves_evaluated > 0);
719    }
720
721    #[test]
722    fn test_construction_best_fit() {
723        let director = create_simple_nqueens_director(4);
724        let mut solver_scope = SolverScope::new(director);
725        solver_scope.start_solving();
726
727        let values: Vec<i64> = (0..4).collect();
728        let placer = create_placer(values);
729        let forager = BestFitForager::new();
730        let mut phase = ConstructionHeuristicPhase::new(placer, forager);
731
732        phase.solve(&mut solver_scope);
733
734        let solution = solver_scope.working_solution();
735        for queen in &solution.queens {
736            assert!(queen.row.is_some(), "Queen should have a row assigned");
737        }
738
739        assert!(solver_scope.best_solution().is_some());
740        assert!(solver_scope.best_score().is_some());
741
742        // BestFitForager evaluates all moves: 4 entities * 4 values = 16 moves
743        assert_eq!(solver_scope.stats().moves_evaluated, 16);
744    }
745
746    #[test]
747    fn test_construction_empty_solution() {
748        let director = create_simple_nqueens_director(0);
749        let mut solver_scope = SolverScope::new(director);
750        solver_scope.start_solving();
751
752        let values: Vec<i64> = vec![];
753        let placer = create_placer(values);
754        let forager = FirstFitForager::new();
755        let mut phase = ConstructionHeuristicPhase::new(placer, forager);
756
757        phase.solve(&mut solver_scope);
758
759        // No moves should be evaluated for empty solution
760        assert_eq!(solver_scope.stats().moves_evaluated, 0);
761    }
762
763    #[test]
764    fn test_construction_resume_retries_interrupted_placement() {
765        static MANAGER: SolverManager<ConstructionPauseSolution> = SolverManager::new();
766
767        let (uninterrupted_job_id, mut uninterrupted_receiver) = MANAGER
768            .solve(ConstructionPauseSolution::new(None))
769            .expect("uninterrupted job should start");
770
771        let uninterrupted_value = match uninterrupted_receiver
772            .blocking_recv()
773            .expect("uninterrupted completed event")
774        {
775            SolverEvent::Completed { metadata, solution } => {
776                assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Completed);
777                assert_eq!(solution.entities[0].value, Some(64));
778                assert_eq!(solution.score(), Some(SoftScore::of(64)));
779                solution.entities[0].value
780            }
781            other => panic!("unexpected event: {other:?}"),
782        };
783
784        MANAGER
785            .delete(uninterrupted_job_id)
786            .expect("delete uninterrupted job");
787
788        let gate = BlockingEvaluationGate::new(1);
789        let (job_id, mut receiver) = MANAGER
790            .solve(ConstructionPauseSolution::new(Some(gate.clone())))
791            .expect("paused job should start");
792
793        gate.wait_until_blocked();
794        MANAGER.pause(job_id).expect("pause should be accepted");
795
796        match receiver.blocking_recv().expect("pause requested event") {
797            SolverEvent::PauseRequested { metadata } => {
798                assert_eq!(
799                    metadata.lifecycle_state,
800                    SolverLifecycleState::PauseRequested
801                );
802            }
803            other => panic!("unexpected event: {other:?}"),
804        }
805
806        gate.release();
807
808        let paused_snapshot_revision = match receiver.blocking_recv().expect("paused event") {
809            SolverEvent::Paused { metadata } => {
810                assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Paused);
811                metadata
812                    .snapshot_revision
813                    .expect("paused snapshot revision")
814            }
815            other => panic!("unexpected event: {other:?}"),
816        };
817
818        let paused_snapshot = MANAGER
819            .get_snapshot(job_id, Some(paused_snapshot_revision))
820            .expect("paused snapshot");
821        assert_eq!(paused_snapshot.solution.entities[0].value, None);
822
823        MANAGER.resume(job_id).expect("resume should be accepted");
824
825        match receiver.blocking_recv().expect("resumed event") {
826            SolverEvent::Resumed { metadata } => {
827                assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Solving);
828            }
829            other => panic!("unexpected event: {other:?}"),
830        }
831
832        match receiver.blocking_recv().expect("completed event") {
833            SolverEvent::Completed { metadata, solution } => {
834                assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Completed);
835                assert_eq!(solution.entities[0].value, uninterrupted_value);
836                assert_eq!(solution.score(), Some(SoftScore::of(64)));
837            }
838            other => panic!("unexpected event: {other:?}"),
839        }
840
841        MANAGER.delete(job_id).expect("delete resumed job");
842    }
843}