1use 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
23pub 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 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 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 let moves_in_placement = placement.moves.len() as u64;
120
121 let mut step_scope = StepScope::new(&mut phase_scope);
122
123 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 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 let m = placement.take_move(idx);
150
151 m.do_move(step_scope.score_director_mut());
153
154 let step_score = step_scope.calculate_score();
156 step_scope.set_step_score(step_score);
157 }
158
159 step_scope.complete();
160 }
161
162 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 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 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 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}