solverforge-solver 0.18.0

Solver engine for SolverForge
Documentation
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Barrier, Mutex};

use solverforge_config::{CandidateTraceConfig, SolverConfig};

use super::Solver;
use crate::manager::SolverTerminalReason;
use crate::phase::{Phase, PhaseSequence};
use crate::scope::{ProgressCallback, SolverScope};
use crate::stats::CandidateTracePhasePlan;
use crate::termination::StepCountTermination;
use crate::test_utils::{create_minimal_director, TestDirector, TestSolution};

#[derive(Clone, Debug)]
struct LifecycleRecorder {
    label: &'static str,
    events: Arc<Mutex<Vec<String>>>,
}

impl LifecycleRecorder {
    fn record(&self, event: &'static str) {
        self.events
            .lock()
            .expect("lifecycle recorder must not be poisoned")
            .push(format!("{event}:{}", self.label));
    }
}

#[derive(Clone, Debug)]
struct RecordingPhase(LifecycleRecorder);

impl<ProgressCb> Phase<TestSolution, TestDirector, ProgressCb> for RecordingPhase
where
    ProgressCb: ProgressCallback<TestSolution>,
{
    fn solve(
        &mut self,
        _solver_scope: &mut SolverScope<'_, TestSolution, TestDirector, ProgressCb>,
    ) {
        self.0.record("solve");
    }

    fn phase_type_name(&self) -> &'static str {
        "RecordingPhase"
    }

    fn on_solver_terminal(
        &mut self,
        _solver_scope: &mut SolverScope<'_, TestSolution, TestDirector, ProgressCb>,
    ) {
        self.0.record("terminal");
    }
}

#[derive(Debug)]
struct OrdinaryPhase {
    solve_count: Arc<AtomicUsize>,
}

impl<ProgressCb> Phase<TestSolution, TestDirector, ProgressCb> for OrdinaryPhase
where
    ProgressCb: ProgressCallback<TestSolution>,
{
    fn solve(
        &mut self,
        solver_scope: &mut SolverScope<'_, TestSolution, TestDirector, ProgressCb>,
    ) {
        self.solve_count.fetch_add(1, Ordering::SeqCst);
        solver_scope.increment_step_count();
    }

    fn phase_type_name(&self) -> &'static str {
        "OrdinaryPhase"
    }
}

#[derive(Debug)]
struct TraceFinalizingPhase {
    terminal_count: Arc<AtomicUsize>,
    terminal_plan: CandidateTracePhasePlan,
}

#[derive(Debug)]
struct BlockingTerminalPhase {
    entered: Arc<Barrier>,
    release: Arc<Barrier>,
}

impl<ProgressCb> Phase<TestSolution, TestDirector, ProgressCb> for BlockingTerminalPhase
where
    ProgressCb: ProgressCallback<TestSolution>,
{
    fn solve(
        &mut self,
        _solver_scope: &mut SolverScope<'_, TestSolution, TestDirector, ProgressCb>,
    ) {
    }

    fn phase_type_name(&self) -> &'static str {
        "BlockingTerminalPhase"
    }

    fn on_solver_terminal(
        &mut self,
        _solver_scope: &mut SolverScope<'_, TestSolution, TestDirector, ProgressCb>,
    ) {
        self.entered.wait();
        self.release.wait();
    }
}

impl<ProgressCb> Phase<TestSolution, TestDirector, ProgressCb> for TraceFinalizingPhase
where
    ProgressCb: ProgressCallback<TestSolution>,
{
    fn solve(
        &mut self,
        _solver_scope: &mut SolverScope<'_, TestSolution, TestDirector, ProgressCb>,
    ) {
    }

    fn phase_type_name(&self) -> &'static str {
        "TraceFinalizingPhase"
    }

    fn on_solver_terminal(
        &mut self,
        solver_scope: &mut SolverScope<'_, TestSolution, TestDirector, ProgressCb>,
    ) {
        self.terminal_count.fetch_add(1, Ordering::SeqCst);
        solver_scope.finalize_candidate_trace_resolved_phase_plan(self.terminal_plan.clone());
    }
}

fn recording_phase(label: &'static str, events: &Arc<Mutex<Vec<String>>>) -> RecordingPhase {
    RecordingPhase(LifecycleRecorder {
        label,
        events: Arc::clone(events),
    })
}

fn recorded_events(events: &Arc<Mutex<Vec<String>>>) -> Vec<String> {
    events
        .lock()
        .expect("lifecycle recorder must not be poisoned")
        .clone()
}

#[test]
fn phase_default_terminal_hook_preserves_ordinary_solver_semantics() {
    let solve_count = Arc::new(AtomicUsize::new(0));
    let result = Solver::new((OrdinaryPhase {
        solve_count: Arc::clone(&solve_count),
    },))
    .solve(create_minimal_director());

    assert_eq!(solve_count.load(Ordering::SeqCst), 1);
    assert_eq!(result.step_count(), 1);
    assert_eq!(result.terminal_reason(), SolverTerminalReason::Completed);
}

#[test]
fn terminal_hook_propagates_once_through_phase_composites() {
    let events = Arc::new(Mutex::new(Vec::new()));
    let sequence = PhaseSequence::new(vec![
        recording_phase("sequence-a", &events),
        recording_phase("sequence-b", &events),
    ]);
    let nested_tuple = (
        ((), recording_phase("tuple-a", &events)),
        recording_phase("tuple-b", &events),
    );
    let result = Solver::new((sequence, nested_tuple)).solve(create_minimal_director());

    assert_eq!(result.terminal_reason(), SolverTerminalReason::Completed);
    assert_eq!(
        recorded_events(&events),
        [
            "solve:sequence-a",
            "solve:sequence-b",
            "solve:tuple-a",
            "solve:tuple-b",
            "terminal:sequence-a",
            "terminal:sequence-b",
            "terminal:tuple-a",
            "terminal:tuple-b",
        ]
    );
}

#[test]
fn terminal_hook_runs_once_when_cancellation_skips_every_phase() {
    let events = Arc::new(Mutex::new(Vec::new()));
    let cancel = AtomicBool::new(true);
    let result = Solver::new((
        recording_phase("first", &events),
        recording_phase("second", &events),
    ))
    .with_terminate(&cancel)
    .solve(create_minimal_director());

    assert_eq!(result.terminal_reason(), SolverTerminalReason::Cancelled);
    assert_eq!(
        recorded_events(&events),
        ["terminal:first", "terminal:second"]
    );
}

#[test]
fn terminal_hook_runs_once_when_config_termination_skips_every_phase() {
    let events = Arc::new(Mutex::new(Vec::new()));
    let result = Solver::new((
        recording_phase("first", &events),
        recording_phase("second", &events),
    ))
    .with_termination(StepCountTermination::new(0))
    .solve(create_minimal_director());

    assert_eq!(
        result.terminal_reason(),
        SolverTerminalReason::TerminatedByConfig
    );
    assert_eq!(
        recorded_events(&events),
        ["terminal:first", "terminal:second"]
    );
}

#[test]
fn terminal_hook_can_finalize_candidate_trace_before_final_stats_are_taken() {
    let terminal_plan = CandidateTracePhasePlan::known(
        "test.terminal_phase_plan",
        [("status", "completed")],
        Vec::new(),
    );
    let terminal_count = Arc::new(AtomicUsize::new(0));
    let config = SolverConfig {
        candidate_trace: Some(CandidateTraceConfig::new(
            std::num::NonZeroUsize::new(1).expect("one is non-zero"),
        )),
        ..SolverConfig::default()
    };

    let result = Solver::new((TraceFinalizingPhase {
        terminal_count: Arc::clone(&terminal_count),
        terminal_plan: terminal_plan.clone(),
    },))
    .with_config(config)
    .solve(create_minimal_director());

    assert_eq!(terminal_count.load(Ordering::SeqCst), 1);
    let trace = result
        .stats()
        .snapshot()
        .candidate_trace
        .expect("enabled candidate trace must be retained in final stats");
    assert_eq!(trace.header.resolved_phase_plan, terminal_plan);
    assert!(trace.header.resolved_phase_plan_complete);
}

#[test]
fn cancellation_accepted_during_terminal_hook_is_settled_before_result_extraction() {
    let entered = Arc::new(Barrier::new(2));
    let release = Arc::new(Barrier::new(2));
    let cancel = Arc::new(AtomicBool::new(false));
    let worker_cancel = Arc::clone(&cancel);
    let worker_entered = Arc::clone(&entered);
    let worker_release = Arc::clone(&release);
    let (sender, receiver) = std::sync::mpsc::channel();

    rayon::spawn(move || {
        let result = Solver::new((BlockingTerminalPhase {
            entered: worker_entered,
            release: worker_release,
        },))
        .with_terminate(worker_cancel.as_ref())
        .solve(create_minimal_director());
        sender
            .send(result.terminal_reason())
            .expect("test receiver remains alive");
    });

    entered.wait();
    cancel.store(true, Ordering::SeqCst);
    release.wait();

    assert_eq!(
        receiver
            .recv_timeout(std::time::Duration::from_secs(1))
            .expect("solver settles cancellation after the hook"),
        SolverTerminalReason::Cancelled
    );
}