sim-nest 0.1.5

The SIM constellation umbrella facade: one crate that re-exports the runtime kernel, codecs, number domains, and libraries behind features.
Documentation
use std::sync::Arc;

use sim_kernel::{
    Args, CapabilityName, Cx, DefaultFactory, EagerPolicy, Expr, Symbol, Test, Value,
    browse_run_tests_capability,
};

use crate::runtime::{
    SimTest, TestExpected,
    browse::schema::{BROWSE_TEST_FIELDS, CARD_V2_FIELDS},
    install_core_runtime,
};

use super::support::table_value;

#[test]
fn sim_test_description_uses_fixed_codec_aware_fields() {
    let mut cx = test_cx();
    let test = SimTest::new(
        Symbol::qualified("test", "value"),
        Symbol::qualified("test", "runtime"),
        Expr::Bool(true),
        TestExpected::Value(Expr::Bool(true)),
        vec![Symbol::qualified("core", "help")],
    )
    .as_example()
    .requiring(CapabilityName::new("demo"));

    let description = test.describe(&mut cx).unwrap();
    let description = expr(&mut cx, &description);

    assert_eq!(table_keys(&description), symbols(BROWSE_TEST_FIELDS));
    assert_eq!(
        table_value(&description, &Symbol::new("mode")),
        Some(&Expr::Symbol(Symbol::qualified("test", "value")))
    );
    assert_eq!(
        table_value(&description, &Symbol::new("expr-codec")),
        Some(&Expr::Symbol(Symbol::qualified("codec", "lisp")))
    );
    assert_eq!(
        table_value(&description, &Symbol::new("expected-codec")),
        Some(&Expr::Symbol(Symbol::qualified("codec", "lisp")))
    );
    assert_eq!(
        table_value(&description, &Symbol::new("example")),
        Some(&Expr::Bool(true))
    );
    assert_list_contains_symbol(
        table_value(&description, &Symbol::new("capabilities")).expect("capabilities"),
        Symbol::qualified("capability", "demo"),
    );
}

#[test]
fn subject_tests_and_examples_use_fixed_test_cards() {
    let mut cx = test_cx();
    register(&mut cx, example_test());
    register(
        &mut cx,
        SimTest::new(
            Symbol::qualified("test", "non-example"),
            Symbol::qualified("test", "runtime"),
            Expr::Bool(true),
            TestExpected::Truthy,
            vec![Symbol::qualified("core", "help")],
        ),
    );

    let subject = symbol_value(&cx, Symbol::qualified("core", "help"));
    let tests = call(&mut cx, Symbol::qualified("core", "tests"), vec![subject]);
    let tests = expr(&mut cx, &tests);
    let test_items = list_items(&tests);
    assert_eq!(test_items.len(), 2);
    assert!(test_items.iter().all(|item| {
        table_value(item, &Symbol::new("expr-codec"))
            == Some(&Expr::Symbol(Symbol::qualified("codec", "lisp")))
    }));

    let subject = symbol_value(&cx, Symbol::qualified("core", "help"));
    let examples = call(
        &mut cx,
        Symbol::qualified("core", "examples"),
        vec![subject],
    );
    let examples = expr(&mut cx, &examples);
    let example_items = list_items(&examples);
    assert_eq!(example_items.len(), 1);
    assert_eq!(
        table_value(&example_items[0], &Symbol::new("name")),
        Some(&Expr::Symbol(Symbol::qualified("test", "example")))
    );
    assert_eq!(
        table_value(&example_items[0], &Symbol::new("example")),
        Some(&Expr::Bool(true))
    );
}

#[test]
fn registered_test_is_browsable_and_roundtrip_metadata_is_visible() {
    let mut cx = test_cx();
    register(
        &mut cx,
        SimTest::new(
            Symbol::qualified("test", "roundtrip"),
            Symbol::qualified("test", "runtime"),
            Expr::String("stable".to_owned()),
            TestExpected::RoundTrip {
                codecs: vec![
                    Symbol::qualified("codec", "lisp"),
                    Symbol::qualified("codec", "json"),
                ],
            },
            vec![Symbol::qualified("core", "help")],
        ),
    );

    let tests = call(&mut cx, Symbol::qualified("core", "tests"), Vec::new());
    let tests = expr(&mut cx, &tests);
    let items = list_items(&tests);
    let roundtrip = test_by_name(items, Symbol::qualified("test", "roundtrip"));
    let codecs = table_value(roundtrip, &Symbol::new("codecs")).expect("codecs");
    assert_eq!(
        codecs,
        &Expr::List(vec![
            Expr::Symbol(Symbol::qualified("codec", "lisp")),
            Expr::Symbol(Symbol::qualified("codec", "json")),
        ])
    );

    let subject = symbol_value(&cx, Symbol::qualified("test", "roundtrip"));
    let card = call(&mut cx, Symbol::qualified("core", "browse"), vec![subject]);
    let card = expr(&mut cx, &card);
    assert_eq!(table_keys(&card), symbols(CARD_V2_FIELDS));
    assert_eq!(
        table_value(&card, &Symbol::new("kind")),
        Some(&Expr::Symbol(Symbol::qualified("core", "test")))
    );
}

#[cfg(feature = "codec-lisp")]
#[test]
fn value_tests_roundtrip_expr_and_expected_through_recorded_codecs() {
    let mut cx = test_cx();
    install_lisp_codec(&mut cx);
    cx.grant(browse_run_tests_capability());
    let test = SimTest::new(
        Symbol::qualified("test", "codec-value"),
        Symbol::qualified("test", "runtime"),
        Expr::Bool(true),
        TestExpected::Value(Expr::Bool(true)),
        vec![Symbol::qualified("core", "help")],
    );

    let report = test.run(&mut cx).unwrap();
    assert!(report.passed, "{:?}", report.detail);
}

#[test]
fn run_tests_requires_capability_but_test_descriptions_stay_visible() {
    let mut cx = test_cx();
    register(&mut cx, example_test());

    let subject = symbol_value(&cx, Symbol::qualified("core", "help"));
    let tests = call(&mut cx, Symbol::qualified("core", "tests"), vec![subject]);
    assert_eq!(list_items(&expr(&mut cx, &tests)).len(), 1);

    let subject = symbol_value(&cx, Symbol::qualified("core", "help"));
    let err = try_call(
        &mut cx,
        Symbol::qualified("core", "run-tests"),
        vec![subject],
    )
    .unwrap_err();
    assert!(matches!(
        err,
        sim_kernel::Error::CapabilityDenied { capability }
            if capability == browse_run_tests_capability()
    ));
}

#[cfg(feature = "codec-lisp")]
#[test]
fn run_tests_returns_effect_reports_and_updates_coverage() {
    let mut cx = test_cx();
    install_lisp_codec(&mut cx);
    cx.grant(browse_run_tests_capability());
    register(&mut cx, example_test());
    register(
        &mut cx,
        SimTest::new(
            Symbol::qualified("test", "failing"),
            Symbol::qualified("test", "runtime"),
            Expr::Bool(false),
            TestExpected::Truthy,
            vec![Symbol::qualified("core", "help")],
        ),
    );

    let subject = symbol_value(&cx, Symbol::qualified("core", "help"));
    let before = call(
        &mut cx,
        Symbol::qualified("core", "coverage"),
        vec![subject],
    );
    let before = expr(&mut cx, &before);
    assert_eq!(int_field(&before, "tests"), Some(2));
    assert_eq!(int_field(&before, "examples"), Some(1));
    assert_eq!(int_field(&before, "runnable"), Some(2));
    assert_eq!(
        table_value(&before, &Symbol::new("passed")),
        Some(&Expr::Nil)
    );

    let subject = symbol_value(&cx, Symbol::qualified("core", "help"));
    let reports = call(
        &mut cx,
        Symbol::qualified("core", "run-tests"),
        vec![subject],
    );
    let reports = expr(&mut cx, &reports);
    let reports = list_items(&reports);
    assert_eq!(reports.len(), 2);
    assert!(reports.iter().all(|report| {
        !list_items(table_value(report, &Symbol::new("events")).expect("events")).is_empty()
            && !matches!(
                table_value(report, &Symbol::new("effect")),
                Some(Expr::Nil) | None
            )
            && table_value(report, &Symbol::new("mode"))
                == Some(&Expr::Symbol(Symbol::qualified("test", "truthy")))
    }));

    let subject = symbol_value(&cx, Symbol::qualified("core", "help"));
    let after = call(
        &mut cx,
        Symbol::qualified("core", "coverage"),
        vec![subject],
    );
    let after = expr(&mut cx, &after);
    assert_eq!(int_field(&after, "passed"), Some(1));
    assert_eq!(int_field(&after, "failed"), Some(1));
    assert_eq!(int_field(&after, "skipped"), Some(0));
    assert!(!matches!(
        table_value(&after, &Symbol::new("last-run")),
        Some(Expr::Nil) | None
    ));
}

#[test]
fn run_tests_skips_tests_with_missing_test_capabilities() {
    let mut cx = test_cx();
    cx.grant(browse_run_tests_capability());
    register(
        &mut cx,
        SimTest::new(
            Symbol::qualified("test", "needs-demo"),
            Symbol::qualified("test", "runtime"),
            Expr::Bool(true),
            TestExpected::Truthy,
            vec![Symbol::qualified("core", "help")],
        )
        .requiring(CapabilityName::new("demo")),
    );

    let subject = symbol_value(&cx, Symbol::qualified("core", "help"));
    let reports = call(
        &mut cx,
        Symbol::qualified("core", "run-tests"),
        vec![subject],
    );
    let reports = expr(&mut cx, &reports);
    let reports = list_items(&reports);
    assert_eq!(reports.len(), 1);
    assert_eq!(
        table_value(&reports[0], &Symbol::new("passed")),
        Some(&Expr::Bool(false))
    );
    assert!(matches!(
        table_value(&reports[0], &Symbol::new("detail")),
        Some(Expr::String(detail)) if detail.contains("missing capability demo")
    ));

    let subject = symbol_value(&cx, Symbol::qualified("core", "help"));
    let coverage = call(
        &mut cx,
        Symbol::qualified("core", "coverage"),
        vec![subject],
    );
    let coverage = expr(&mut cx, &coverage);
    assert_eq!(int_field(&coverage, "runnable"), Some(0));
    assert_eq!(int_field(&coverage, "skipped"), Some(1));
}

#[cfg(feature = "codec-lisp")]
fn install_lisp_codec(cx: &mut Cx) {
    let codec_id = cx.registry_mut().fresh_codec_id();
    let lib = crate::codec_lisp::LispCodecLib::new(codec_id).unwrap();
    cx.load_lib(&lib).unwrap();
}

fn test_cx() -> Cx {
    let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
    install_core_runtime(&mut cx);
    cx
}

fn register(cx: &mut Cx, test: SimTest) {
    cx.registry_mut()
        .register_test(
            test.name.clone(),
            test.lib.clone(),
            Arc::new(test.clone()),
            test.subjects.clone(),
        )
        .unwrap();
}

fn example_test() -> SimTest {
    SimTest::new(
        Symbol::qualified("test", "example"),
        Symbol::qualified("test", "runtime"),
        Expr::Bool(true),
        TestExpected::Truthy,
        vec![Symbol::qualified("core", "help")],
    )
    .as_example()
}

fn call(cx: &mut Cx, symbol: Symbol, args: Vec<Value>) -> Value {
    try_call(cx, symbol.clone(), args).unwrap_or_else(|err| panic!("{symbol} failed: {err}"))
}

fn try_call(cx: &mut Cx, symbol: Symbol, args: Vec<Value>) -> sim_kernel::Result<Value> {
    cx.call_function(&symbol, Args::new(args))
}

fn symbol_value(cx: &Cx, symbol: Symbol) -> Value {
    cx.factory().symbol(symbol).unwrap()
}

fn expr(cx: &mut Cx, value: &Value) -> Expr {
    value.object().as_expr(cx).unwrap()
}

fn table_keys(expr: &Expr) -> Vec<Symbol> {
    let Expr::Map(entries) = expr else {
        panic!("expected map");
    };
    entries
        .iter()
        .map(|(key, _)| match key {
            Expr::Symbol(symbol) => symbol.clone(),
            other => panic!("expected symbol key, got {other:?}"),
        })
        .collect()
}

fn symbols(fields: &[&str]) -> Vec<Symbol> {
    fields.iter().map(|field| Symbol::new(*field)).collect()
}

fn list_items(expr: &Expr) -> &[Expr] {
    let Expr::List(items) = expr else {
        panic!("expected list");
    };
    items
}

fn test_by_name(tests: &[Expr], expected: Symbol) -> &Expr {
    tests
        .iter()
        .find(|test| {
            table_value(test, &Symbol::new("name")) == Some(&Expr::Symbol(expected.clone()))
        })
        .unwrap_or_else(|| panic!("missing test {expected}"))
}

fn int_field(expr: &Expr, name: &str) -> Option<i64> {
    match table_value(expr, &Symbol::new(name)) {
        Some(Expr::Number(number)) => number.canonical.parse().ok(),
        _ => None,
    }
}

fn assert_list_contains_symbol(expr: &Expr, expected: Symbol) {
    assert!(
        list_items(expr)
            .iter()
            .any(|item| matches!(item, Expr::Symbol(symbol) if symbol == &expected))
    );
}