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

nordisk/nornir

tests/self_emitters_viz.rs

//! **Self-emitter matrix — viz RENDER-path emit-fallback surfaces.**
//!
//! nornir wiring its OWN [`nornir_testmatrix::functional_status`] across the
//! "can't-observe-from-outside" *render* surfaces: the egui paints and the
//! screenshot PNG write. Built ONLY under `--features testmatrix` (the whole file
//! is gated), so a release `cargo test` never compiles the emitters.
//!
//! EMIT-DOCTRINE this file obeys — the **emit-fallback** branch: the egui paint /
//! screenshot PNG write is a side effect that can't be read from outside. The
//! emit is implanted INSIDE the render/side-effect fn (gated
//! `#[cfg(feature = "testmatrix")]`); here we drive that fn HEADLESSLY with REAL
//! injected input, then READ BACK the emitted row from the in-process buffer via
//! [`nornir_testmatrix::drain_functional_rows`] (NOT a JSON sink — nornir's own
//! self-tests run in-process and drain the process-global buffer). Inject-assert:
//! a real graph / a real frame in, a real green non-blank row drained back out.
//!
//! Run:
//!   cargo test --features "viz,testmatrix" --test self_emitters_viz -- --test-threads=1
#![cfg(all(feature = "viz", feature = "testmatrix"))]

use std::path::PathBuf;

use nornir::viz::UrdrThreadsApp;

const WS: &str = "self-emit-viz";

/// A fresh fat app over a throwaway local warehouse under `dir`. Local mode (a
/// real warehouse path) is what the funnel demo + screenshot warehouse-persist
/// paths need; the render emit-fallbacks fire regardless.
fn local_app(dir: &std::path::Path) -> UrdrThreadsApp {
    let wh = dir.join(".nornir").join("warehouse");
    std::fs::create_dir_all(&wh).unwrap();
    UrdrThreadsApp::with_repos(wh, WS.to_string(), dir.to_path_buf(), vec![])
}

/// Drain + return only the rows whose `test_name` matches `check`. Clears the
/// process-global buffer (so a sibling test never reads ours, and we never read
/// theirs) — callers drain FIRST to clear any leftovers, then drive, then drain.
fn drained_for(check: &str) -> Vec<nornir_testmatrix::TestResultRow> {
    nornir_testmatrix::drain_functional_rows()
        .into_iter()
        .filter(|r| r.test_name == check)
        .collect()
}

// ════════════════════════════════════════════════════════════════════════════
// emit-fallback: the egui paints (CallGraph + Funnel-3D). Inject a real graph /
// real plan, headlessly DRAW both tabs, then READ BACK the rows implanted inside
// the render fns. ok ⇔ the scene painted non-blank.
// ════════════════════════════════════════════════════════════════════════════
#[test]
fn viz_callgraph_and_funnel3d_render_emit_fallback_roundtrip() {
    // SAFETY: set before the app/harness build; tests in this file are independent.
    unsafe { std::env::set_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG", "1") };
    let dir = std::env::temp_dir().join(format!("nornir-self-emit-render-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    let mut app = local_app(&dir);

    // Clear any leftover buffer rows, then drive the real render paths.
    let _ = nornir_testmatrix::drain_functional_rows();
    app.test_render_callgraph_and_funnel3d_nonblank();
    // Drain ONCE; the buffer is cleared by the drain so split it by test_name.
    let all = nornir_testmatrix::drain_functional_rows();

    // ── CallGraph: a 6-node ring graph painted ⇒ a green non-blank row.
    let cg = all
        .iter()
        .filter(|r| r.test_name == "callgraph_rendered_non_blank")
        .last()
        .expect("a callgraph render row must be in the drained buffer");
    assert_eq!(
        cg.status,
        nornir_testmatrix::status::PASS,
        "callgraph draw must paint a non-blank scene: {}",
        cg.message
    );
    assert_eq!(cg.suite, "viz/callgraph (egui draw)");
    assert!(
        cg.message.contains("6 node circle(s)"),
        "the row carries the REAL painted count: {}",
        cg.message
    );

    // ── Funnel-3D: a real demo plan projected + painted ⇒ a green non-blank row.
    let f3d = all
        .iter()
        .filter(|r| r.test_name == "funnel3d_rendered_non_blank")
        .last()
        .expect("a funnel3d render row must be in the drained buffer");
    assert_eq!(
        f3d.status,
        nornir_testmatrix::status::PASS,
        "funnel3d draw must paint a non-blank scene: {}",
        f3d.message
    );
    assert_eq!(f3d.suite, "viz/funnel3d (egui/3D draw)");
    assert!(
        f3d.message.contains("node sphere(s)"),
        "the row carries the REAL painted/total count: {}",
        f3d.message
    );

    let _ = std::fs::remove_dir_all(&dir);
    // SAFETY: cleared after the test's single-threaded body finished.
    unsafe { std::env::remove_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG") };
}

// ════════════════════════════════════════════════════════════════════════════
// emit-fallback: the DepGraph egui paint. Inject a real dep-graph snapshot, draw
// the DepGraph tab headlessly, and read back the `dep_graph_rendered_non_blank`
// row implanted inside `draw_dep_graph`.
// ════════════════════════════════════════════════════════════════════════════
#[test]
fn viz_dep_graph_render_emit_fallback_roundtrip() {
    use std::collections::BTreeMap;

    use nornir::release::pipeline::{RepoGitState, RepoReleaseRecord, persist_lineage};
    use nornir::warehouse::dep_graph::{CrossRepoEdge, WorkspaceGraph, record_dep_graph};
    use nornir::warehouse::iceberg::IcebergWarehouse;
    use uuid::Uuid;

    const REPOS: [&str; 3] = ["alpha", "beta", "gamma"];

    // SAFETY: set before the app/harness build; tests in this file are independent.
    unsafe { std::env::set_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG", "1") };
    let dir =
        std::env::temp_dir().join(format!("nornir-self-emit-depgraph-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    let wh_path = dir.join(".nornir").join("warehouse");
    std::fs::create_dir_all(&wh_path).unwrap();

    // Seed a REAL dep-graph snapshot + release lineage so the Timeline carries
    // repos for `draw_dep_graph` to lay out into painted node cards.
    let wh = IcebergWarehouse::open(&wh_path).expect("open warehouse");
    let edges = vec![
        CrossRepoEdge {
            from: REPOS[1].into(),
            to: REPOS[0].into(),
            via: ["alpha-core".to_string()].into_iter().collect(),
            kind: Default::default(),
        },
        CrossRepoEdge {
            from: REPOS[0].into(),
            to: REPOS[2].into(),
            via: ["gamma-leaf".to_string()].into_iter().collect(),
            kind: Default::default(),
        },
    ];
    let graph = WorkspaceGraph::from_query_parts(BTreeMap::new(), edges);
    let snapshot_id = wh
        .block_on(record_dep_graph(&wh, WS, &graph))
        .expect("record dep graph");
    let release_id = Uuid::from_u128(0x5e5f_0000_0000_0000_0000_0000_0000_0001u128);
    let lineage: Vec<RepoReleaseRecord> = REPOS
        .iter()
        .enumerate()
        .map(|(i, r)| RepoReleaseRecord {
            repo: (*r).to_string(),
            build_order_idx: i,
            git: RepoGitState {
                sha: format!("{r}0000000sha"),
                branch: "main".into(),
                dirty: false,
            },
            gate_status: "succeeded".into(),
            tests_passed: 2,
            tests_failed: 0,
            published_versions: vec![((*r).to_string(), "0.1.0".into())],
            tantivy_snapshot_id: None,
            dwarf_snapshot_id: None,
        })
        .collect();
    wh.block_on(persist_lineage(
        &wh,
        release_id,
        WS,
        &snapshot_id,
        &lineage,
        false,
    ))
    .expect("persist_lineage");
    drop(wh);

    let mut app = UrdrThreadsApp::with_repos(
        wh_path,
        WS.to_string(),
        dir.clone(),
        REPOS.iter().map(|r| r.to_string()).collect(),
    );

    let _ = nornir_testmatrix::drain_functional_rows();
    // Select the DepGraph tab + draw it headlessly via the public render driver.
    app.test_render_dep_graph_nonblank();
    let rows = drained_for("dep_graph_rendered_non_blank");

    let dg = rows
        .last()
        .expect("a dep_graph render row must be in the drained buffer");
    assert_eq!(
        dg.status,
        nornir_testmatrix::status::PASS,
        "dep-graph draw must paint a non-blank scene: {}",
        dg.message
    );
    assert_eq!(dg.suite, "viz/graph (DepGraphView egui draw)");
    assert!(
        dg.message.contains("node card(s)"),
        "the row carries the REAL painted node/edge count: {}",
        dg.message
    );

    let _ = std::fs::remove_dir_all(&dir);
    // SAFETY: cleared after the test's single-threaded body finished.
    unsafe { std::env::remove_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG") };
}

// ════════════════════════════════════════════════════════════════════════════
// emit-fallback: the 🧵 Timeline egui paint. Seed a real release lineage so the
// Timeline carries release nodes, render every tab headlessly (which paints the
// Timeline tab), then read back the `timeline_rendered` row implanted in the
// Timeline draw arm — GREEN because it painted non-blank release nodes.
// ════════════════════════════════════════════════════════════════════════════
#[test]
fn viz_timeline_render_emit_fallback_roundtrip() {
    use std::collections::BTreeMap;

    use nornir::release::pipeline::{RepoGitState, RepoReleaseRecord, persist_lineage};
    use nornir::warehouse::dep_graph::{WorkspaceGraph, record_dep_graph};
    use nornir::warehouse::iceberg::IcebergWarehouse;
    use uuid::Uuid;

    const REPOS: [&str; 2] = ["alpha", "beta"];

    // SAFETY: set before the app/harness build; tests in this file are independent.
    unsafe { std::env::set_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG", "1") };
    let dir =
        std::env::temp_dir().join(format!("nornir-self-emit-timeline-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    let wh_path = dir.join(".nornir").join("warehouse");
    std::fs::create_dir_all(&wh_path).unwrap();

    let wh = IcebergWarehouse::open(&wh_path).expect("open warehouse");
    let graph = WorkspaceGraph::from_query_parts(BTreeMap::new(), vec![]);
    let snapshot_id = wh
        .block_on(record_dep_graph(&wh, WS, &graph))
        .expect("record dep graph");
    let release_id = Uuid::from_u128(0x71_0000_0000_0000_0000_0000_0000_0001u128);
    let lineage: Vec<RepoReleaseRecord> = REPOS
        .iter()
        .enumerate()
        .map(|(i, r)| RepoReleaseRecord {
            repo: (*r).to_string(),
            build_order_idx: i,
            git: RepoGitState {
                sha: format!("{r}0000000sha"),
                branch: "main".into(),
                dirty: false,
            },
            gate_status: "succeeded".into(),
            tests_passed: 1,
            tests_failed: 0,
            published_versions: vec![((*r).to_string(), "0.1.0".into())],
            tantivy_snapshot_id: None,
            dwarf_snapshot_id: None,
        })
        .collect();
    wh.block_on(persist_lineage(
        &wh,
        release_id,
        WS,
        &snapshot_id,
        &lineage,
        false,
    ))
    .expect("persist_lineage");
    drop(wh);

    let mut app = UrdrThreadsApp::with_repos(
        wh_path,
        WS.to_string(),
        dir.clone(),
        REPOS.iter().map(|r| r.to_string()).collect(),
    );

    let _ = nornir_testmatrix::drain_functional_rows();
    // Render every tab headlessly — the Timeline arm paints + emits its row.
    app.render_all_tabs_headless();
    let rows = drained_for("timeline_rendered");
    let row = rows
        .last()
        .expect("a timeline_rendered row must be in the drained buffer");
    assert_eq!(
        row.status,
        nornir_testmatrix::status::PASS,
        "the Timeline painted non-blank release nodes: {}",
        row.message
    );
    assert_eq!(row.suite, "nornir/viz/timeline");
    assert!(
        row.message.contains("release node(s) painted"),
        "the row carries the REAL painted lane/node count: {}",
        row.message
    );

    let _ = std::fs::remove_dir_all(&dir);
    // SAFETY: cleared after the test's single-threaded body finished.
    unsafe { std::env::remove_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG") };
}

// ════════════════════════════════════════════════════════════════════════════
// ════════════════════════════════════════════════════════════════════════════
// emit-fallback: the PNG-write side effect. Feed a synthetic non-blank frame into
// `handle_screenshot_image` via the testmatrix hook, then READ BACK the row
// implanted inside the fn — and confirm the object-state half (PNG on disk).
// ════════════════════════════════════════════════════════════════════════════
#[test]
fn viz_screenshot_png_written_emit_fallback_roundtrip() {
    // SAFETY: set before the app/harness build; tests in this file are independent.
    unsafe { std::env::set_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG", "1") };
    let dir = std::env::temp_dir().join(format!("nornir-self-emit-png-{}", std::process::id()));
    let _ = std::fs::create_dir_all(&dir);
    let png = dir.join("shot.png");
    let mut app = UrdrThreadsApp::with_repos(dir.clone(), WS.to_string(), PathBuf::new(), vec![]);

    let _ = nornir_testmatrix::drain_functional_rows();
    // Feed a synthetic non-blank frame straight into the GPU-frame path.
    app.test_feed_screenshot_frame(64, 48, Some(png.to_string_lossy().into_owned()));

    // 1. object-state: the PNG exists on disk + state_json points at it.
    assert!(png.exists(), "screenshot PNG must be written to disk");
    let last = app.state_json()["screenshot"]["last_path"]
        .as_str()
        .map(String::from);
    assert_eq!(last.as_deref(), Some(png.to_string_lossy().as_ref()));

    // 2. emit-fallback: the row implanted INSIDE handle_screenshot_image landed.
    let rows = drained_for("screenshot_command_pending_then_png_written");
    let row = rows
        .last()
        .expect("the screenshot emit-fallback row must be present in the buffer");
    assert_eq!(
        row.status,
        nornir_testmatrix::status::PASS,
        "the PNG was actually written non-empty: {}",
        row.message
    );
    assert_eq!(row.suite, "viz/app self-screenshot (LAW4b egui capture)");
    assert!(
        row.message.contains("64x48px") && row.message.contains("bytes PNG written"),
        "the row carries the REAL dimensions + byte count: {}",
        row.message
    );

    let _ = std::fs::remove_dir_all(&dir);
    // SAFETY: cleared after the test's single-threaded body finished.
    unsafe { std::env::remove_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG") };
}