sim-expr-tree-calc 0.1.0

Bounded incremental calculation of ordinary SIM expressions and values.
Documentation
use std::{
    collections::VecDeque,
    sync::{
        Arc, Mutex,
        atomic::{AtomicUsize, Ordering},
    },
};

use sim_kernel::Expr;

use super::*;
use crate::{BackendRefreshSample, MountRefreshSource};

struct TestRefreshSource {
    samples: Mutex<VecDeque<Result<BackendRefreshSample, String>>>,
    calls: AtomicUsize,
    watch: bool,
}

impl TestRefreshSource {
    fn new(samples: impl IntoIterator<Item = BackendRefreshSample>) -> Self {
        Self {
            samples: Mutex::new(samples.into_iter().map(Ok).collect()),
            calls: AtomicUsize::new(0),
            watch: false,
        }
    }

    fn watch_managed() -> Self {
        Self {
            samples: Mutex::new(VecDeque::new()),
            calls: AtomicUsize::new(0),
            watch: true,
        }
    }

    fn calls(&self) -> usize {
        self.calls.load(Ordering::Acquire)
    }
}

impl MountRefreshSource for TestRefreshSource {
    fn has_watch_contract(&self) -> bool {
        self.watch
    }

    fn sample(&self) -> Result<BackendRefreshSample, String> {
        self.calls.fetch_add(1, Ordering::AcqRel);
        self.samples
            .lock()
            .unwrap()
            .pop_front()
            .unwrap_or_else(|| Err("no test sample".to_owned()))
    }
}

fn external_sample(epoch: u64, listing: u64, stamp: u64) -> BackendRefreshSample {
    BackendRefreshSample::new(MountEpoch::new(epoch))
        .with_listing("/external", listing)
        .with_stamp("/external/value", stamp)
}

#[test]
fn refresh_is_explicit_and_invalidates_stale_external_observations() {
    let runtime = TestRuntime::default();
    *runtime.source.lock().unwrap() = "old".to_owned();
    let source = Arc::new(TestRefreshSource::new([
        external_sample(1, 1, 1),
        external_sample(1, 1, 2),
    ]));
    let mut calc = runtime_calc(runtime.clone());
    calc.mount(
        path("/external"),
        MountResource::Dir,
        BackendKind::Filesystem,
        MountEpoch::new(1),
    );
    calc.attach_refresh_source(&path("/external"), source.clone())
        .unwrap();
    calc.set_cell(path("/external/value"), probe_expr());
    calc.set_cell(path("/result"), explicit_ref("/external/value"));

    let initial = calc.refresh().unwrap();
    assert_eq!(initial.sampled_mounts, vec!["/external"]);
    assert!(initial.invalidated_observations > 0);
    assert_eq!(
        value_expr(calc.verify_cell(&path("/result")).unwrap()),
        Expr::String("old".to_owned())
    );
    assert_eq!(runtime.count("probe"), 1);

    *runtime.source.lock().unwrap() = "new".to_owned();
    assert_eq!(
        value_expr(calc.current_cell(&path("/result")).unwrap()),
        Expr::String("old".to_owned())
    );
    calc.current_cell(&path("/result"))
        .unwrap()
        .object()
        .display(&mut strict_context())
        .unwrap();
    assert_eq!(
        value_expr(calc.verify_cell(&path("/result")).unwrap()),
        Expr::String("old".to_owned()),
        "verification must not poll an external backend implicitly"
    );
    let _ = calc.explain(&path("/result"));
    assert_eq!(
        source.calls(),
        1,
        "ordinary reads and display/explanation must not sample backends"
    );

    let refreshed = calc.refresh().unwrap();
    assert_eq!(refreshed.changed_stamps, vec!["/external/value"]);
    assert!(refreshed.invalidated_observations >= 3);
    assert_eq!(
        value_expr(calc.verify_cell(&path("/result")).unwrap()),
        Expr::String("new".to_owned())
    );
    assert_eq!(runtime.count("probe"), 2);
}

#[test]
fn refresh_compares_epochs_listings_and_stamps_without_polling_watch_mounts() {
    let polled = Arc::new(TestRefreshSource::new([
        external_sample(2, 3, 4),
        external_sample(3, 5, 6),
    ]));
    let watched = Arc::new(TestRefreshSource::watch_managed());
    let mut calc = ExprTreeCalc::new();
    calc.mount(
        path("/external"),
        MountResource::Dir,
        BackendKind::Filesystem,
        MountEpoch::new(2),
    );
    calc.mount(
        path("/watched"),
        MountResource::Dir,
        BackendKind::Database,
        MountEpoch::new(9),
    );
    calc.attach_refresh_source(&path("/external"), polled.clone())
        .unwrap();
    calc.attach_refresh_source(&path("/watched"), watched.clone())
        .unwrap();

    let first = calc.refresh().unwrap();
    assert_eq!(first.sampled_mounts, vec!["/external"]);
    assert_eq!(first.watch_managed_mounts, vec!["/watched"]);
    assert_eq!(watched.calls(), 0);
    let second = calc.refresh().unwrap();
    assert_eq!(second.changed_epochs, vec!["/external"]);
    assert_eq!(second.changed_listings, vec!["/external"]);
    assert_eq!(second.changed_stamps, vec!["/external/value"]);
    assert_eq!(watched.calls(), 0);
}