lix 0.17.1

Embeddable version control for apps and AI agents.
Documentation
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};

use super::mode::SimulationMode;

#[derive(Clone)]
pub(super) struct SimulationAssertions {
    shared: SharedExpectSameRun,
}

impl SimulationAssertions {
    pub(super) fn shared(run: SharedExpectSameRun) -> Self {
        Self { shared: run }
    }

    pub(super) fn start_mode(&self, _mode: SimulationMode) {
        self.shared.start_mode();
    }

    pub(super) fn finish_mode(&self, _mode: SimulationMode) {
        self.shared.finish_mode();
    }
}

#[derive(Clone)]
pub(crate) struct SharedExpectSameRun {
    case_id: String,
    mode: SimulationMode,
    call_index: Arc<Mutex<usize>>,
    case: Arc<SharedExpectSameCase>,
}

pub(super) struct SharedExpectSameCase {
    state: Mutex<SharedExpectSameState>,
    condvar: Condvar,
}

#[derive(Default)]
struct SharedExpectSameState {
    base_finished: bool,
    base_failed: bool,
    expected: Vec<(String, String)>,
}

pub(crate) struct SharedExpectSameRunGuard {
    run: SharedExpectSameRun,
    finished: bool,
}

impl SharedExpectSameRun {
    pub(crate) fn with_case(
        case_id: &str,
        mode: SimulationMode,
        case: Arc<SharedExpectSameCase>,
    ) -> Self {
        Self {
            case_id: case_id.to_string(),
            mode,
            call_index: Arc::new(Mutex::new(0)),
            case,
        }
    }

    #[expect(clippy::unused_self)]
    fn start_mode(&self) {}

    fn next_index(&self) -> usize {
        let mut guard = self
            .call_index
            .lock()
            .expect("engine shared expectation call index lock poisoned");
        let index = *guard;
        *guard += 1;
        index
    }

    fn call_count(&self) -> usize {
        *self
            .call_index
            .lock()
            .expect("engine shared expectation call index lock poisoned")
    }

    fn assert_same(&self, label: &str, actual: String) {
        let index = self.next_index();
        match self.mode {
            SimulationMode::Base => {
                let mut state = self
                    .case
                    .state
                    .lock()
                    .expect("engine shared expectation lock poisoned");
                state.expected.push((label.to_string(), actual));
                self.case.condvar.notify_all();
            }
            SimulationMode::TrackedStateRebuild => {
                let expected = self.wait_for_expected(index, label);
                assert_eq!(
                    expected.0,
                    label,
                    "simulation_test assertion order changed for case `{}` mode `{}` at call #{}",
                    self.case_id,
                    self.mode.name(),
                    index
                );
                assert_eq!(
                    expected.1,
                    actual,
                    "simulation_test assert_same `{label}` differed for case `{}` mode `{}`",
                    self.case_id,
                    self.mode.name()
                );
            }
        }
    }

    fn wait_for_expected(&self, index: usize, label: &str) -> (String, String) {
        let deadline = Instant::now() + Duration::from_mins(2);
        let mut state = self
            .case
            .state
            .lock()
            .expect("engine shared expectation lock poisoned");
        loop {
            assert!(
                !state.base_failed,
                "simulation_test case `{}` base failed before `{}` could compare call #{}",
                self.case_id, label, index
            );
            if let Some(expected) = state.expected.get(index) {
                return expected.clone();
            }
            assert!(
                !state.base_finished,
                "simulation_test case `{}` mode `{}` called assert_same one extra time at call #{} ({label})",
                self.case_id,
                self.mode.name(),
                index
            );

            let remaining = deadline.saturating_duration_since(Instant::now());
            assert!(
                !remaining.is_zero(),
                "simulation_test timed out waiting for base assert_same call #{} in case `{}`",
                index,
                self.case_id
            );
            let (next_state, timeout) = self
                .case
                .condvar
                .wait_timeout(state, remaining)
                .expect("engine shared expectation condvar wait poisoned");
            state = next_state;
            assert!(
                !timeout.timed_out(),
                "simulation_test timed out waiting for base assert_same call #{} in case `{}`",
                index,
                self.case_id
            );
        }
    }

    fn finish_mode(&self) {
        match self.mode {
            SimulationMode::Base => self.finish_base(std::thread::panicking()),
            SimulationMode::TrackedStateRebuild => self.finish_compare(),
        }
    }

    fn finish_base(&self, failed: bool) {
        let mut state = self
            .case
            .state
            .lock()
            .expect("engine shared expectation lock poisoned");
        state.base_finished = true;
        state.base_failed = failed;
        self.case.condvar.notify_all();
    }

    fn finish_compare(&self) {
        let deadline = Instant::now() + Duration::from_mins(2);
        let mut state = self
            .case
            .state
            .lock()
            .expect("engine shared expectation lock poisoned");
        while !state.base_finished && !state.base_failed {
            let remaining = deadline.saturating_duration_since(Instant::now());
            assert!(
                !remaining.is_zero(),
                "simulation_test timed out waiting for base completion in case `{}`",
                self.case_id
            );
            let (next_state, timeout) = self
                .case
                .condvar
                .wait_timeout(state, remaining)
                .expect("engine shared expectation condvar wait poisoned");
            state = next_state;
            assert!(
                !timeout.timed_out(),
                "simulation_test timed out waiting for base completion in case `{}`",
                self.case_id
            );
        }
        assert!(
            !state.base_failed,
            "simulation_test case `{}` base failed before mode `{}` completed",
            self.case_id,
            self.mode.name()
        );
        assert_eq!(
            self.call_count(),
            state.expected.len(),
            "simulation_test mode `{}` for case `{}` did not execute all assert_same checks",
            self.mode.name(),
            self.case_id
        );
    }
}

impl SharedExpectSameCase {
    pub(crate) fn new() -> Arc<Self> {
        Arc::new(Self {
            state: Mutex::new(SharedExpectSameState::default()),
            condvar: Condvar::new(),
        })
    }
}

impl SharedExpectSameRunGuard {
    pub(crate) fn new(run: SharedExpectSameRun) -> Self {
        Self {
            run,
            finished: false,
        }
    }
}

impl Drop for SharedExpectSameRunGuard {
    fn drop(&mut self) {
        if self.finished || self.run.mode != SimulationMode::Base {
            return;
        }
        self.run.finish_base(std::thread::panicking());
        self.finished = true;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn shared_expect_same_compares_against_base_run() {
        let case_id = "expect_same_unit_shared";
        let shared_case = SharedExpectSameCase::new();
        let base =
            SharedExpectSameRun::with_case(case_id, SimulationMode::Base, shared_case.clone());
        base.assert_same("value", "1".to_string());
        base.finish_mode();

        let rebuild = SharedExpectSameRun::with_case(
            case_id,
            SimulationMode::TrackedStateRebuild,
            shared_case,
        );
        rebuild.assert_same("value", "1".to_string());
        rebuild.finish_mode();
    }
}