sim-lib-topology 0.1.4

Data-driven topology engine.
Documentation
use std::{sync::Arc, time::Duration};

use sim_kernel::{
    Args, CapabilityName, Consistency, Cx, DefaultFactory, EagerPolicy, Error, EvalFabric,
    EvalMode, EvalRequest, Expr, Symbol, eval_fabric_capability,
};
use sim_shape::{ExprKind, ExprKindShape, shape_value};

use crate::{
    Edge, Graph, Node, PortRef, TopologyConnection, connection_from_graph, install_topology_lib,
    text::graph_to_expr, topology_run_capability, topology_site_symbol,
};

#[test]
fn topology_connection_can_be_used_as_eval_fabric() {
    let mut cx = runtime_cx();
    let connection = connection_from_graph(&mut cx, &identity_graph()).expect("connection");
    assert_eq!(connection.site_kind(), "topology");

    let output = connection
        .request(
            &mut cx,
            Expr::String("request-ok".to_owned()),
            None,
            Vec::new(),
        )
        .expect("topology request")
        .object()
        .as_expr(&mut cx)
        .expect("expr output");
    assert_eq!(output, Expr::String("request-ok".to_owned()));
}

#[test]
fn topology_lib_registers_callable_site_export() {
    let mut cx = runtime_cx();
    install_topology_lib(&mut cx).expect("install topology lib");
    let site_symbol = topology_site_symbol();
    let site = cx
        .registry()
        .site_by_symbol(&site_symbol)
        .cloned()
        .expect("topology site export");
    let graph = cx
        .factory()
        .expr(graph_to_expr(&identity_graph()))
        .expect("graph value");
    let callable = site
        .object()
        .as_callable()
        .expect("topology site is callable");
    let connection = callable
        .call(&mut cx, Args::new(vec![graph]))
        .expect("site builds connection");

    assert!(
        connection
            .object()
            .downcast_ref::<TopologyConnection>()
            .is_some()
    );
    assert!(cx.registry().sites().contains_key(&site_symbol));
}

#[test]
fn topology_connection_honors_request_capabilities_and_trace() {
    let mut cx = runtime_cx();
    cx.grant(CapabilityName::new("client-cap"));
    let connection = connection_from_graph(&mut cx, &identity_graph()).expect("connection");
    let input = Expr::List(vec![Expr::Symbol(Symbol::new("payload"))]);
    let reply = connection
        .realize(
            &mut cx,
            EvalRequest {
                expr: input.clone(),
                result_shape: None,
                required_capabilities: vec![CapabilityName::new("client-cap")],
                deadline: None,
                consistency: Consistency::LocalFirst,
                mode: EvalMode::Eval,
                answer_limit: None,
                stream_buffer: None,
                stream: false,
                trace: true,
            },
        )
        .expect("topology reply");

    assert_eq!(reply.value.object().as_expr(&mut cx).unwrap(), input);
    assert!(reply.trace.is_some());
}

#[test]
fn topology_connection_requires_client_capabilities() {
    let mut cx = runtime_cx();
    let connection = connection_from_graph(&mut cx, &identity_graph()).expect("connection");
    let error = match connection.realize(
        &mut cx,
        EvalRequest {
            expr: Expr::String("blocked".to_owned()),
            result_shape: None,
            required_capabilities: vec![CapabilityName::new("client-cap")],
            deadline: None,
            consistency: Consistency::LocalFirst,
            mode: EvalMode::Eval,
            answer_limit: None,
            stream_buffer: None,
            stream: false,
            trace: false,
        },
    ) {
        Ok(_) => panic!("client capability should be required"),
        Err(error) => error,
    };

    let Error::CapabilityDenied { capability } = error else {
        panic!("unexpected client-cap error: {error}");
    };
    assert_eq!(capability, CapabilityName::new("client-cap"));
}

#[test]
fn topology_connection_requires_topology_run_capability() {
    let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
    let connection = connection_from_graph(&mut cx, &identity_graph()).expect("connection");

    let error = connection
        .request(
            &mut cx,
            Expr::String("blocked".to_owned()),
            None,
            Vec::new(),
        )
        .expect_err("topology run should require capability");
    let Error::CapabilityDenied { capability } = error else {
        panic!("unexpected topology-run error: {error}");
    };
    assert_eq!(capability, topology_run_capability());
}

#[test]
fn topology_connection_rejects_wrong_result_shape() {
    let mut cx = runtime_cx();
    let connection = connection_from_graph(&mut cx, &identity_graph()).expect("connection");
    let mut request = eval_request(Expr::String("wrong-shape".to_owned()));
    request.result_shape = Some(shape_value(
        Symbol::qualified("test", "bool-result"),
        Arc::new(ExprKindShape::new(ExprKind::Bool)),
    ));

    let error = match connection.realize(&mut cx, request) {
        Ok(_) => panic!("result shape should reject string"),
        Err(error) => error,
    };

    assert_eval_error_contains(error, "request result");
}

#[test]
fn topology_connection_rejects_unsupported_request_mode() {
    let mut cx = runtime_cx();
    let connection = connection_from_graph(&mut cx, &identity_graph()).expect("connection");
    let mut request = eval_request(Expr::String("logic".to_owned()));
    request.mode = EvalMode::Logic;

    let error = match connection.realize(&mut cx, request) {
        Ok(_) => panic!("logic mode should be rejected"),
        Err(error) => error,
    };

    assert_eval_error_contains(error, "unsupported eval mode");
}

#[test]
fn topology_connection_rejects_zero_answer_limit() {
    let mut cx = runtime_cx();
    let connection = connection_from_graph(&mut cx, &identity_graph()).expect("connection");
    let mut request = eval_request(Expr::String("limit".to_owned()));
    request.answer_limit = Some(0);

    let error = match connection.realize(&mut cx, request) {
        Ok(_) => panic!("zero answer limit should be rejected"),
        Err(error) => error,
    };

    assert_eval_error_contains(error, "answer_limit");
}

#[test]
fn topology_connection_rejects_stream_request() {
    let mut cx = runtime_cx();
    let connection = connection_from_graph(&mut cx, &identity_graph()).expect("connection");
    let mut request = eval_request(Expr::String("stream".to_owned()));
    request.stream = true;
    request.stream_buffer = Some(8);

    let error = match connection.realize(&mut cx, request) {
        Ok(_) => panic!("streaming replies should be rejected"),
        Err(error) => error,
    };

    assert_eval_error_contains(error, "streaming replies");
}

#[test]
fn topology_connection_rejects_deadline() {
    let mut cx = runtime_cx();
    let connection = connection_from_graph(&mut cx, &identity_graph()).expect("connection");
    let mut request = eval_request(Expr::String("deadline".to_owned()));
    request.deadline = Some(Duration::from_millis(1));

    let error = match connection.realize(&mut cx, request) {
        Ok(_) => panic!("deadline should be rejected"),
        Err(error) => error,
    };

    assert_eval_error_contains(error, "deadline");
}

fn runtime_cx() -> Cx {
    let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
    cx.grant(eval_fabric_capability());
    cx.grant(topology_run_capability());
    cx
}

fn eval_request(expr: Expr) -> EvalRequest {
    EvalRequest {
        expr,
        result_shape: None,
        required_capabilities: Vec::new(),
        deadline: None,
        consistency: Consistency::LocalFirst,
        mode: EvalMode::Eval,
        answer_limit: None,
        stream_buffer: None,
        stream: false,
        trace: false,
    }
}

fn assert_eval_error_contains(error: Error, expected: &str) {
    let Error::Eval(message) = error else {
        panic!("unexpected error: {error}");
    };
    assert!(
        message.contains(expected),
        "expected {message:?} to contain {expected:?}"
    );
}

fn identity_graph() -> Graph {
    let mut graph = Graph::minimal("identity-topology");
    graph.nodes = vec![Node::named("in", "in"), Node::named("out", "out")];
    graph.edges = vec![Edge::new(0, PortRef::output("in"), PortRef::input("out"))];
    graph
}