sim-lib-expr-tree-server 0.1.0

Authoritative bounded EvalSite server for SIM expression-tree sessions.
Documentation
use std::sync::{
    Arc,
    atomic::{AtomicBool, Ordering},
};

use sim_kernel::{Args, Callable, Cx, Expr, Object, ObjectCompat, Symbol, Value};
use sim_lib_intent::{Origin, intent};
use sim_lib_server::{EvalSite, ServerAddress, eval_reply_from_frame, server_frame_from_request};
use sim_value::build;

use crate::{
    ExpressionTreeServer, ExpressionTreeServerLib, expr_tree_server_site_symbol,
    install_expr_tree_server_lib,
};

use super::support::{
    error_code, full_cx, read_cx, realize_expr, request, runtime_call, snapshot_revision, target,
    web_commit,
};

#[test]
fn loadable_export_is_both_eval_site_and_eval_fabric() {
    let mut cx = full_cx();
    let manifest = sim_kernel::Lib::manifest(&ExpressionTreeServerLib::default());
    assert!(manifest.exports.iter().any(
        |export| matches!(export, sim_kernel::Export::Site { symbol, .. }
            if symbol == &expr_tree_server_site_symbol())
    ));
    install_expr_tree_server_lib(&mut cx).unwrap();
    let value = cx
        .registry()
        .site_by_symbol(&expr_tree_server_site_symbol())
        .unwrap();
    assert!(value.object().as_eval_fabric().is_some());
}

#[test]
fn two_clients_share_one_session_while_other_sessions_stay_isolated() {
    let server = ExpressionTreeServer::local();
    let mut creator = full_cx();
    let shared = server.create_session(&mut creator, "shared").unwrap();
    let isolated = server.create_session(&mut creator, "isolated").unwrap();

    let mut client_a = full_cx();
    let path = realize_expr(
        &server,
        &mut client_a,
        runtime_call(
            &shared,
            "new-cell",
            vec![
                Expr::String("/".to_owned()),
                Expr::String("answer".to_owned()),
                Expr::String("forty-two".to_owned()),
            ],
        ),
    );
    assert_eq!(path, Expr::String("/answer".to_owned()));

    let mut client_b = full_cx();
    assert_eq!(
        realize_expr(
            &server,
            &mut client_b,
            runtime_call(&shared, "ref", vec![Expr::String("/answer".to_owned())],),
        ),
        Expr::String("forty-two".to_owned())
    );
    let absent = realize_expr(
        &server,
        &mut client_b,
        runtime_call(&isolated, "ref", vec![Expr::String("/answer".to_owned())]),
    );
    assert_eq!(error_code(&absent).as_deref(), Some("operation-failed"));

    let mut reconnected = full_cx();
    assert_eq!(
        realize_expr(
            &server,
            &mut reconnected,
            runtime_call(&shared, "ref", vec![Expr::String("/answer".to_owned())],),
        ),
        Expr::String("forty-two".to_owned())
    );
}

#[test]
fn surface_intent_runs_through_existing_codec_and_optimistic_revision() {
    let server = ExpressionTreeServer::local();
    let mut cx = full_cx();
    let session = server.create_session(&mut cx, "surface").unwrap();
    let snapshot = server.snapshot(&session).unwrap();
    let revision = snapshot_revision(&snapshot);
    let create_dir = intent(
        "create",
        Origin::human(7),
        vec![
            ("class", build::sym("directory")),
            ("at", target(&session, revision, "/")),
            ("args", build::list(vec![build::text("work")])),
        ],
    );
    let updated = server
        .apply_intent(&mut cx, &session, revision, &create_dir)
        .unwrap();
    assert_eq!(snapshot_revision(&updated), revision + 1);

    let stale = server
        .apply_intent(&mut cx, &session, revision, &create_dir)
        .unwrap_err();
    assert_eq!(stale.code(), "stale-revision");
}

#[test]
fn caller_authority_is_diminished_and_errors_remain_structured() {
    let server = ExpressionTreeServer::local();
    let mut creator = full_cx();
    let session = server.create_session(&mut creator, "authority").unwrap();
    let expected = server.snapshot(&session).unwrap();
    let operation = runtime_call(
        &session,
        "new-dir",
        vec![
            Expr::String("/".to_owned()),
            Expr::String("denied".to_owned()),
        ],
    );
    let mut read_only = read_cx();
    let unchanged = expected.clone();
    let reply = web_commit(&server, &mut read_only, &session, operation, expected);
    assert_eq!(error_code(&reply).as_deref(), Some("authority-denied"));
    assert!(server.snapshot(&session).unwrap().canonical_eq(&unchanged));
}

#[test]
fn server_registry_lock_does_not_span_runtime_evaluation() {
    let server = Arc::new(ExpressionTreeServer::local());
    let observed_unlocked = Arc::new(AtomicBool::new(false));
    let mut cx = full_cx();
    let probe = cx
        .factory()
        .opaque(Arc::new(RegistryLockProbe {
            server: Arc::clone(&server),
            observed_unlocked: Arc::clone(&observed_unlocked),
        }))
        .unwrap();
    cx.env_mut()
        .define(Symbol::new("server-registry-lock-probe"), probe);
    let session = server.create_session(&mut cx, "lock-probe").unwrap();
    realize_expr(
        &server,
        &mut cx,
        runtime_call(
            &session,
            "new-cell",
            vec![
                Expr::String("/".to_owned()),
                Expr::String("probe".to_owned()),
                Expr::Call {
                    operator: Box::new(Expr::Symbol(Symbol::new("server-registry-lock-probe"))),
                    args: Vec::new(),
                },
            ],
        ),
    );

    let report = realize_expr(
        &server,
        &mut cx,
        runtime_call(
            &session,
            "calculate",
            vec![Expr::String("/probe".to_owned())],
        ),
    );

    assert!(error_code(&report).is_none(), "{report:?}");
    assert!(
        observed_unlocked.load(Ordering::Acquire),
        "server registry lock spanned SIM evaluation"
    );
}

#[test]
fn server_frames_preserve_correlation_and_use_existing_adapters() {
    let server = ExpressionTreeServer::new(
        ServerAddress::Local,
        vec![Symbol::qualified("codec", "lisp")],
        Arc::new(sim_lib_server::DeterministicWallClock::new(10, 1)),
        Default::default(),
    )
    .unwrap();
    let mut cx = full_cx();
    let mut frame = server_frame_from_request(
        &mut cx,
        &Symbol::qualified("codec", "lisp"),
        request(build::map(vec![
            (
                "op",
                Expr::Symbol(Symbol::qualified("expr-tree-server", "create")),
            ),
            ("storage", build::text("framed")),
        ])),
    )
    .unwrap();
    frame.msg_id = Some(91);
    let reply = server.answer(&mut cx, frame).unwrap();
    assert_eq!(reply.correlate, Some(91));
    let value = eval_reply_from_frame(&mut cx, &reply).unwrap().value;
    assert!(matches!(
        value.object().as_expr(&mut cx).unwrap(),
        Expr::Symbol(symbol)
            if symbol.namespace.as_deref() == Some("expr-tree/session")
    ));
    assert!(EvalSite::as_eval_fabric(&server).is_some());
}

struct RegistryLockProbe {
    server: Arc<ExpressionTreeServer>,
    observed_unlocked: Arc<AtomicBool>,
}

impl Object for RegistryLockProbe {
    fn display(&self, _cx: &mut Cx) -> sim_kernel::Result<String> {
        Ok("#<server-registry-lock-probe>".to_owned())
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

impl ObjectCompat for RegistryLockProbe {
    fn as_callable(&self) -> Option<&dyn Callable> {
        Some(self)
    }
}

impl Callable for RegistryLockProbe {
    fn call(&self, cx: &mut Cx, _args: Args) -> sim_kernel::Result<Value> {
        self.observed_unlocked.store(
            self.server.registry_is_unlocked_for_test(),
            Ordering::Release,
        );
        cx.factory().string("unlocked".to_owned())
    }
}