Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-recipe/tests/tree_laws.rs

//! **The tree laws over gunnar's forest** — `check_bijective`, `law_path_flag`
//! and `law_decision_tree` from `nornir_build_thing::laws`, run over every
//! variant's settings tree, with the installer tree (the deepest one in the
//! constellation: wireguard ▸ hub_conf, dhcp ▸ static_ip/gateway, the kernel
//! edge tunnr/tunnr-kernel with its presets) named and counted out loud.
//!
//! # Why this file was feature-gated once (`--features tree-laws`, retired)
//!
//! The `bijection-laws` story of the same week, one pin later. This crate
//! resolves `nornir-build-thing` from the REGISTRY (the Cargo.toml header:
//! the reuse rule's "builds standalone from a fresh clone" forbids a sibling path), and the
//! settings tree — `tree`, `flags`, the tree-level laws, ops 11–13 over the
//! wasm ABI — ships in 0.1.10, which on 2026-08-26 was on edda main and NOT
//! published. A workspace-root `[patch.crates-io]` onto `../edda` is what cargo
//! offers for exactly this, and it was REFUSED: a patch table is a structural
//! publishing blocker in this constellation, and `cargo publish` verifies
//! against the registry, which would not compile a field only the sibling has.
//!
//! So the gate was committed and one line away, and it RAN once on this box
//! (2026-08-26) through a scratch clone of edda at `5c5b7cb`:
//!
//! ```text
//! cargo --config 'patch.crates-io.nornir-build-thing.path="<clone>/crates/nornir-build-thing"' \
//!     update -p nornir-build-thing
//! cargo test -p gunnar-build-thing --features tree-laws --test tree_laws -- --nocapture
//! git checkout -- Cargo.lock
//! ```
//!
//! `nornir-build-thing 0.1.10` reached crates.io on 2026-08-27 (wave-3 decision
//! R1). The pin is a plain bump, the feature is deleted, and this file is now
//! just a test — which is the whole point of writing the gate rather than
//! waiting for the registry.

use std::path::{Path, PathBuf};

use gunnar_recipe::GunnarBuildThing;
use nornir_build_thing::flags::flags_of;
use nornir_build_thing::laws;
use nornir_build_thing::plugin::BuildThing;
use nornir_build_thing::tree::SettingsTree;
use nornir_build_thing::variant::BuildVariantSet;

fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(Path::parent)
        .expect("crates/gunnar-build-thing has two ancestors")
        .to_path_buf()
}

/// Every law, through the trait gate — the tree laws included from 0.1.10.
#[test]
fn every_law_holds_over_the_rust_mirror() {
    let problems = laws::check_bijective(&GunnarBuildThing);
    assert!(
        problems.is_empty(),
        "gunnar breaks {} law(s):\n  {}",
        problems.len(),
        problems.join("\n  ")
    );
}

/// Law C (path ↔ flag) and law D (decision ↔ tree) named on their own, so a
/// failure says which of the two it is without reading the joined report.
#[test]
fn the_path_flag_and_decision_tree_laws_hold_on_their_own() {
    let c = laws::law_path_flag(&GunnarBuildThing);
    assert!(c.is_empty(), "law C:\n  {}", c.join("\n  "));
    let d = laws::law_decision_tree(&GunnarBuildThing);
    assert!(d.is_empty(), "law D:\n  {}", d.join("\n  "));
}

/// The installer tree is the one that makes the laws mean something here:
/// three features at the root (wireguard, dhcp, the kernel edge), settings
/// under them, the kernel presets as a `val`. Counted and printed, and held
/// to a floor so a tree that silently flattened to nothing cannot pass.
#[test]
fn the_installer_tree_is_deep_and_every_flag_it_renders_round_trips() {
    let set = GunnarBuildThing.variants();
    let forest = SettingsTree::forest(&set);
    assert!(
        !forest.is_empty(),
        "gunnar declares variants, so it has trees"
    );

    let mut total_nodes = 0usize;
    let mut total_flags = 0usize;
    for tree in &forest {
        let nodes = tree.len();
        let flags = flags_of(tree);
        total_nodes += nodes;
        total_flags += flags.len();
        eprintln!(
            "{:<22} typ {:<10} roots {:>2} nodes {:>3} flags {:>3}",
            tree.variant,
            tree.typ.as_str(),
            tree.roots.len(),
            nodes,
            flags.len()
        );
        // Law C, by hand as well as by the trait: every rendered flag parses
        // back to itself.
        for f in &flags {
            let s = f.render();
            assert_eq!(
                nornir_build_thing::flags::Flag::parse(&s).expect("a rendered flag parses"),
                *f,
                "{s} did not round trip"
            );
        }
    }
    eprintln!(
        "forest: {} trees, {total_nodes} nodes, {total_flags} flags",
        forest.len()
    );

    let installer = forest
        .iter()
        .find(|t| t.variant == "gunnar-installer")
        .expect("the installer has a tree");
    let names: Vec<&str> = installer.roots.iter().map(|n| n.name()).collect();
    eprintln!("gunnar-installer roots: {names:?}");
    eprintln!("gunnar-installer paths: {:?}", installer.paths());
    assert!(
        installer.roots.len() >= 3,
        "the installer tree has {} root(s) — wireguard, dhcp and the kernel edge make three",
        installer.roots.len()
    );
    assert!(
        installer.len() >= 10,
        "the installer tree has {} node(s); with the kernel presets under the edge it is deeper",
        installer.len()
    );
    assert!(
        installer.roots.iter().any(|n| n.edge.is_some()),
        "no root of the installer tree carries the kernel edge (tunnr/tunnr-kernel)"
    );
}

/// The toml mirror, with the loader dwarves uses, through the set-level laws
/// that exist at 0.1.10 (A: declaration ↔ struct; A-tree: rows ↔ tree). The
/// trait-level laws reach it transitively through `toml_parity`'s equality.
#[test]
fn the_toml_mirror_obeys_the_set_level_laws_of_0_1_10() {
    let path = BuildVariantSet::conventional_path(&repo_root());
    let set = BuildVariantSet::load_file(&path)
        .unwrap_or_else(|e| panic!("load {}: {e:#}", path.display()));
    let mut problems = laws::law_declaration_struct(&set);
    problems.extend(laws::law_rows_tree(&set));
    assert!(
        problems.is_empty(),
        "the toml mirror breaks {} law(s):\n  {}",
        problems.len(),
        problems.join("\n  ")
    );
}