sim-lib-topology 0.1.0

Data-driven topology engine.
Documentation
use std::collections::BTreeSet;
use std::sync::Arc;

use sim_kernel::{Cx, DefaultFactory, EagerPolicy, Expr, NumberLiteral, Symbol};

use crate::{
    browse::{
        topology_browse_symbols, topology_card_expr, topology_example_specs,
        topology_function_specs, topology_verb_specs,
    },
    install_topology_lib, parse_package, topology_run_capability,
};

const CARD_V2_FIELDS: [&str; 14] = [
    "subject",
    "kind",
    "help",
    "args",
    "result",
    "tests",
    "ops",
    "requires",
    "see-also",
    "shape-known",
    "facets",
    "coverage",
    "provenance",
    "freshness",
];

const HELP_FIELDS: [&str; 9] = [
    "subject",
    "kind",
    "summary",
    "detail",
    "exported-by",
    "stability",
    "capabilities",
    "demand",
    "see-also",
];

const TEST_FIELDS: [&str; 12] = [
    "name",
    "subjects",
    "lib",
    "mode",
    "expr",
    "expr-codec",
    "expected",
    "expected-codec",
    "expected-error",
    "codecs",
    "example",
    "capabilities",
];

const COVERAGE_FIELDS: [&str; 8] = [
    "tests", "examples", "runnable", "passed", "failed", "skipped", "last-run", "stale",
];

#[test]
fn topology_browse_cards_validate_against_card_schema() {
    let symbols = topology_browse_symbols();
    let unique = symbols.iter().cloned().collect::<BTreeSet<_>>();
    assert_eq!(unique.len(), symbols.len());
    assert_eq!(
        symbols.len(),
        topology_function_specs().len()
            + topology_verb_specs().len()
            + topology_example_specs().len()
            + 1
    );

    for spec in topology_function_specs() {
        let card = topology_card_expr(&Symbol::qualified(
            "topology/card",
            format!("function-{}", spec.symbol),
        ));
        assert_card_schema(
            &card,
            Symbol::qualified("topology", spec.symbol),
            Symbol::qualified("core", "function"),
        );
        assert_coverage(&card, 0, 0, false);
    }

    for spec in topology_verb_specs() {
        let card = topology_card_expr(&Symbol::qualified("topology/verb", spec.name));
        assert_card_schema(
            &card,
            Symbol::qualified("topology/verb", spec.name),
            Symbol::qualified("topology", "node-verb"),
        );
        assert_coverage(&card, 0, 0, false);
    }

    let package = topology_card_expr(&Symbol::qualified("topology", "package-format"));
    assert_card_schema(
        &package,
        Symbol::qualified("topology", "package-format"),
        Symbol::qualified("topology", "package-format"),
    );
    assert_coverage(
        &package,
        topology_example_specs().len() as u64,
        topology_example_specs().len() as u64,
        true,
    );

    for spec in topology_example_specs() {
        let card = topology_card_expr(&Symbol::qualified("topology/example", spec.name));
        assert_card_schema(
            &card,
            Symbol::qualified("topology/example", spec.name),
            Symbol::qualified("core", "test"),
        );
        assert_coverage(&card, 1, 1, true);
        assert_example_test_calls_topology_test(first_test(&card), spec.package);
    }
}

#[test]
fn topology_lib_installs_public_function_and_browse_surfaces() {
    let mut cx = test_cx();
    install_topology_lib(&mut cx).expect("installed topology lib");

    for spec in topology_function_specs() {
        cx.resolve_function(&Symbol::qualified("topology", spec.symbol))
            .expect("topology function export");
    }

    for symbol in topology_browse_symbols() {
        let value = cx.resolve_value(&symbol).expect("topology browse value");
        let expr = value.object().as_expr(&mut cx).expect("browse expr");
        assert_field_order(&expr, &CARD_V2_FIELDS);
    }
}

#[test]
fn generated_examples_parse_and_embedded_tests_run() {
    let mut cx = test_cx();
    cx.grant(topology_run_capability());
    install_topology_lib(&mut cx).expect("installed topology lib");

    for spec in topology_example_specs() {
        let package = parse_package(spec.package).expect("example package parses");
        let test_fn = cx
            .resolve_function(&Symbol::qualified("topology", "test"))
            .expect("topology/test");
        let value = cx
            .call_exprs(test_fn, vec![Expr::String(spec.package.to_owned())])
            .expect("topology/test runs");
        let report = value.object().as_expr(&mut cx).expect("test report expr");

        assert_eq!(
            field(&report, "graph"),
            &Expr::Symbol(package.graph.name.clone())
        );
        assert_eq!(field(&report, "passed"), &Expr::Bool(true));
        assert_eq!(
            field(&report, "total"),
            &number_expr(package.graph.tests.len() as u64)
        );
        assert_eq!(
            field(&report, "ok"),
            &number_expr(package.graph.tests.len() as u64)
        );
        assert_eq!(field(&report, "failed"), &number_expr(0));
    }
}

fn assert_card_schema(expr: &Expr, subject: Symbol, kind: Symbol) {
    assert_field_order(expr, &CARD_V2_FIELDS);
    assert_eq!(field(expr, "subject"), &Expr::Symbol(subject));
    assert_eq!(field(expr, "kind"), &Expr::Symbol(kind));
    assert_field_order(field(expr, "help"), &HELP_FIELDS);
    assert_field_order(field(expr, "coverage"), &COVERAGE_FIELDS);
    assert_allowed_freshness(field(expr, "freshness"));

    let Expr::List(tests) = field(expr, "tests") else {
        panic!("card tests must be a list");
    };
    for test in tests {
        assert_field_order(test, &TEST_FIELDS);
    }
}

fn assert_coverage(card: &Expr, tests: u64, examples: u64, runnable: bool) {
    let coverage = field(card, "coverage");
    assert_eq!(field(coverage, "tests"), &number_expr(tests));
    assert_eq!(field(coverage, "examples"), &number_expr(examples));
    assert_eq!(field(coverage, "runnable"), &Expr::Bool(runnable));
}

fn assert_example_test_calls_topology_test(test: &Expr, package: &str) {
    assert_eq!(
        field(test, "name"),
        &Expr::Symbol(Symbol::qualified(
            "topology/example",
            example_name_from_package(package)
        ))
    );
    assert_eq!(
        field(test, "expr-codec"),
        &Expr::Symbol(Symbol::qualified("codec", "lisp"))
    );
    let Expr::Call { operator, args } = field(test, "expr") else {
        panic!("example test expression must be a call");
    };
    assert_eq!(
        operator.as_ref(),
        &Expr::Symbol(Symbol::qualified("topology", "test"))
    );
    assert_eq!(args, &[Expr::String(package.to_owned())]);
    assert_eq!(field(test, "example"), &Expr::Bool(true));
}

fn first_test(card: &Expr) -> &Expr {
    let Expr::List(tests) = field(card, "tests") else {
        panic!("card tests must be a list");
    };
    tests.first().expect("example card has test")
}

fn assert_field_order(expr: &Expr, expected: &[&str]) {
    let Expr::Map(entries) = expr else {
        panic!("expected map, got {expr:?}");
    };
    let actual = entries
        .iter()
        .map(|(key, _)| match key {
            Expr::Symbol(symbol) if symbol.namespace.is_none() => symbol.name.to_string(),
            other => panic!("expected unqualified symbol key, got {other:?}"),
        })
        .collect::<Vec<_>>();
    let expected = expected
        .iter()
        .map(|field| (*field).to_owned())
        .collect::<Vec<_>>();
    assert_eq!(actual, expected);
}

fn field<'a>(expr: &'a Expr, name: &str) -> &'a Expr {
    let Expr::Map(entries) = expr else {
        panic!("expected map, got {expr:?}");
    };
    entries
        .iter()
        .find_map(|(key, value)| match key {
            Expr::Symbol(symbol) if symbol.namespace.is_none() && symbol.name.as_ref() == name => {
                Some(value)
            }
            _ => None,
        })
        .unwrap_or_else(|| panic!("missing field {name} in {expr:?}"))
}

fn assert_allowed_freshness(expr: &Expr) {
    let Expr::Symbol(symbol) = expr else {
        panic!("freshness must be a symbol");
    };
    assert!(
        ["unknown", "fresh", "stale", "live"].contains(&symbol.name.as_ref()),
        "unexpected freshness symbol: {symbol}"
    );
}

fn example_name_from_package(package: &str) -> String {
    let package = parse_package(package).expect("example package parses");
    package
        .name()
        .name
        .as_ref()
        .trim_start_matches("example-")
        .to_owned()
}

fn number_expr(value: u64) -> Expr {
    Expr::Number(NumberLiteral {
        domain: Symbol::new("i64"),
        canonical: value.to_string(),
    })
}

fn test_cx() -> Cx {
    Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory))
}