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

nordisk/nornir

tests/law4_no_opengl_backend.rs

//! **LAW #4 — nornir's viz renders on WebGPU, never OpenGL/WebGL.**
//!
//! The nordisk rule is "modern platforms only: WebGPU everywhere, no WebGL/glow". nornir
//! broke it in three declarations across two crates, and the `nornir-viz-web` one is the
//! most literal violation of the law in the fleet:
//!
//!   * `Cargo.toml` (root) declared `eframe = { features = ["default_fonts", "glow", …] }`
//!     behind `viz` — so the Urðr Threads desktop viz rendered OpenGL;
//!   * `crates/nornir-viz-web/Cargo.toml` declared eframe **twice**, target-split, and
//!     *both* said `glow`. On wasm32, eframe's `glow` feature means **WebGL2** — the exact
//!     thing GFX_V2's Decision 0 dropped. nornir's public web viz was a WebGL page.
//!
//! In every case the source passed `NativeOptions`/`WebOptions` with no `renderer` field.
//! With `glow` on and `wgpu` off, eframe resolves `Renderer::default()` to `Glow`
//! (eframe 0.34.3, `epi.rs`), so the backend was chosen by a word in a manifest, the
//! source said nothing about it, and nothing in the tree stated which renderer shipped —
//! so nothing could disagree with it.
//!
//! ## Why this was worse than a law violation
//!
//! Zero facett facets use `glow` directly, and the ones that carry **wgpu custom passes**
//! only paint natively under a wgpu host — under glow they degrade to the CPU painter. So a
//! glow host was never rendering "the OpenGL version" of those panes; it was silently not
//! running their GPU lanes at all.
//!
//! Which facets those are, counted by `impl egui_wgpu::CallbackTrait` on facett `main`
//! rather than by reputation — an earlier version of this comment claimed ten and named
//! four crates that own no pass at all:
//!
//! | facet             | owns a wgpu pass?                                            |
//! |-------------------|--------------------------------------------------------------|
//! | facett-core       | YES — `render/gpu/graphcloud.rs` (`CloudPaintCallback`)       |
//! | facett-graph3d    | YES — `logo3d/gpu.rs`; `Graph3D` itself reuses core's cloud   |
//! | facett-map        | YES — `gpu/mod.rs` (`MapPaintCallback`)                       |
//! | facett-map3d      | YES — `gpu.rs` (`Map3dPaintCallback`)                         |
//! | facett-geomap     | borrows facett-map's callback; owns none                      |
//! | facett-flowsim    | YES, but only on the `gfx/flowsim-gpu-host` branch            |
//! | facett-demo       | no — it is a HOST that installs core's renderer               |
//! | facett-graphpan   | no — lowers through core's `CloudBuilder`                     |
//! | facett-graphview  | no — its `gpu.rs` is a vello scene builder, egui_wgpu-free    |
//! | facett-pipeline   | no — drives flowsim's pass, owns none                         |
//!
//! nornir's viz embeds graph3d, helix, warehousedeck and graphview. Of those, **graph3d** is
//! the one with a real GPU lane (through facett-core's graphcloud), so that is the pane this
//! fix actually lights up. graphview is vello-based and unaffected; helix and warehousedeck
//! own no wgpu pass. The honest claim is one pane, not four.
//!
//! ## What this file asserts
//!
//!   1. **every eframe line in the repo** names `wgpu`, never `glow`, with
//!      `default-features = false`. That last one is NOT because the defaults are glow —
//!      eframe 0.34.3's `default` is `["accesskit", "default_fonts", "wayland",
//!      "web_screen_reader", "wgpu", "winit/default", "x11"]`, i.e. wgpu, not glow, so a bare
//!      `eframe = "0.34"` already renders WebGPU. It is asserted because inheriting the
//!      defaults means an upstream release note decides this repo's renderer, and because a
//!      target-split manifest (viz-web has one) can inherit different defaults per platform.
//!   2. **every `NativeOptions`/`WebOptions` literal** names its renderer explicitly.
//!      With `glow` gone `Renderer::default()` resolves to `Wgpu` anyway, so deleting those
//!      lines would leave nornir rendering WebGPU *today* while making the backend, once
//!      again, a side effect of feature resolution. An implicit renderer is the disease.
//!
//! Manifest- and source-reading is deliberate: an `eframe::Renderer` assertion would need
//! a window, and a dependency-tree assertion would trip over `wgpu-hal`'s internal GLES
//! backend, which is a legitimately different thing. `glow` reachable *under* `wgpu-hal`
//! is NOT a violation; `glow` reachable via `eframe`/`egui_glow` is.
//!
//! ## What this file deliberately does NOT assert
//!
//! It does not shell out to `cargo tree`. nornir's *dev*-dependency tree still reaches
//! `glow` through `nornir-robotui 0.2.6`, resolved by path to the sibling edda checkout,
//! which is parked on a feature branch. edda `origin/main` already carries robotui 0.3.0
//! with glow deleted, but that release moved to eframe 0.35 while nornir pins the 0.34
//! stack, so consuming it is blocked on the egui 0.35 migration. A tree-based assertion
//! would therefore be RED for a reason no edit in this repo can fix, which is how guards
//! get commented out. The SHIPPED artifact is already clean and measurable:
//! `cargo tree -p nornir --features viz -i glow --edges normal` reaches glow only under
//! `wgpu-hal`.
//!
//! Sibling guards, same law, same shape: `knut/crates/knut-popviz/tests/`, `korp/tests/`
//! and `holger/server/ui/tests/` each carry a `law4_no_opengl_backend.rs`. Pure `std`, no
//! dev-dependency — a law guard must not be breakable by dependency resolution. (The
//! shared home for this walker, once the edda siblings are off their parked branches, is
//! `nornir-testmatrix::gatedtests`, which already walks workspace members for another law.)

use std::path::PathBuf;

/// The repo root — the directory holding nornir's root `Cargo.toml`.
fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

/// Every file named `want` in the repo, as `(path relative to root, contents)`.
///
/// Walks the tree rather than the `[workspace] members` list on purpose. nornir's root
/// manifest lists its members, and a crate on disk outside that list (an excluded fixture,
/// a scratch crate) would be skipped by a members-driven walk — and any future GUI crate
/// added outside the list would be born uncovered. Walking means the next manifest to
/// declare `eframe` is covered the moment it lands, by construction (LAW #5 — one walker,
/// not a guard per copy).
fn repo_files(want: &str) -> Vec<(String, String)> {
    let root = repo_root();
    let mut out = Vec::new();
    let mut stack = vec![root.clone()];

    while let Some(dir) = stack.pop() {
        let Ok(entries) = std::fs::read_dir(&dir) else {
            continue;
        };
        for e in entries.flatten() {
            let p = e.path();
            let name = e.file_name().to_string_lossy().into_owned();
            if p.is_dir() {
                // Skip build output, git internals, agent worktrees and vendored deps. A
                // `.claude/worktrees/` checkout is a DIFFERENT commit of this same repo,
                // whose manifests must not be able to fail (or rescue) this guard.
                if matches!(
                    name.as_str(),
                    "target" | ".git" | ".claude" | "vendor" | "node_modules"
                ) {
                    continue;
                }
                stack.push(p);
            } else if name == want {
                if let Ok(src) = std::fs::read_to_string(&p) {
                    let rel = p
                        .strip_prefix(&root)
                        .map(|r| r.to_string_lossy().into_owned())
                        .unwrap_or_else(|_| name.clone());
                    out.push((rel, src));
                }
            }
        }
    }

    out.sort();
    out
}

/// Every `Cargo.toml` in the repo.
fn all_manifests() -> Vec<(String, String)> {
    let out = repo_files("Cargo.toml");
    // A non-vacuity floor: if the walk finds nothing, the loops over it pass having
    // inspected zero manifests — precisely the silent green this file exists to prevent.
    // nornir had 14 when this guard was written.
    assert!(
        out.len() >= 8,
        "found only {} Cargo.toml files under {} — the walk is broken, and a broken walk \
         makes this guard pass while checking nothing",
        out.len(),
        repo_root().display()
    );
    out
}

/// The `eframe = …` dependency lines in a manifest, comments excluded so the prose ABOUT
/// `glow` in these manifests cannot make this test pass or fail.
///
/// Returns EVERY match, not the first — and in this repo that is load-bearing, not
/// hypothetical: `crates/nornir-viz-web/Cargo.toml` declares eframe TWICE, once under
/// `cfg(target_arch = "wasm32")` and once under `cfg(not(…))`. A `find()`-style extractor
/// would check the wasm line and silently ignore the native one, or vice versa — and the
/// wasm one is the WebGL2 violation this law is most about.
fn eframe_lines(src: &str) -> Vec<String> {
    src.lines()
        .map(str::trim)
        .filter(|l| !l.starts_with('#'))
        .filter(|l| l.starts_with("eframe"))
        .map(str::to_owned)
        .collect()
}

#[test]
fn no_manifest_in_the_repo_ships_the_glow_backend() {
    let mut lines_checked = 0usize;
    let mut crates_checked: Vec<String> = Vec::new();

    for (path, src) in all_manifests() {
        for line in eframe_lines(&src) {
            lines_checked += 1;
            if !crates_checked.contains(&path) {
                crates_checked.push(path.clone());
            }

            assert!(
                !line.contains("\"glow\""),
                "LAW #4: {path} must not link eframe's OpenGL backend. With `glow` on and \
                 `wgpu` off, eframe resolves `Renderer::default()` to `Glow`, so this one \
                 word decides the shipped renderer with nothing in the source saying so — \
                 and on wasm32 `glow` means WebGL2, the thing Decision 0 dropped. It also \
                 decides what the viz can DRAW: facett's graph3d pane runs a wgpu custom pass \
                 (facett-core's graphcloud) that never executes under a glow host — it \
                 degrades to the CPU painter instead. Offending line:\n  {line}"
            );
            assert!(
                line.contains("default-features = false"),
                "LAW #4: {path}'s eframe dep must pin `default-features = false` so the \
                 renderer is the one this line NAMES. Inheriting the defaults is not a GL bug \
                 today — eframe 0.34.3's defaults are wgpu-based, not glow — but it hands the \
                 choice to eframe's release notes instead of to this repo, and a target-split \
                 manifest can inherit different defaults per platform. Offending \
                 line:\n  {line}"
            );
            assert!(
                line.contains("\"wgpu\""),
                "LAW #4: {path} must ASK for the WebGPU backend explicitly. Enabling neither \
                 eframe backend is a `compile_error!`, so an eframe line naming neither \
                 cannot be right. Offending line:\n  {line}"
            );
        }
    }

    // Non-vacuity, on BOTH axes. Two manifests declare eframe (the root, behind `viz`, and
    // `crates/nornir-viz-web`), but there are THREE lines, because viz-web is target-split.
    // Asserting only the crate count would let a regression hide in the second line of a
    // split manifest — which is exactly where nornir's WebGL2 violation lived.
    assert!(
        crates_checked.len() >= 2,
        "expected at least the 2 known eframe manifests (root, crates/nornir-viz-web); \
         inspected {crates_checked:?} — a shrunken list means the detector stopped finding \
         dependency lines, not that the violations were fixed"
    );
    assert!(
        lines_checked >= 3,
        "expected at least 3 eframe dependency LINES (root + viz-web's wasm32 and native \
         target-split pair); found {lines_checked}. Counting crates alone would miss a \
         regression in the second line of a split manifest, and the wasm32 line is the \
         WebGL2 one this law is most about"
    );
}

/// **Every renderer-carrying options literal must name its renderer.**
///
/// Covers `WebOptions` as well as `NativeOptions`: eframe 0.34's `WebOptions` carries the
/// same `renderer: Renderer` field, so the browser bundle's backend is just as much a
/// source-level choice as the desktop window's. Leaving it implicit is how nornir's web
/// viz ended up on WebGL2 with nothing in `main.rs` saying so.
#[test]
fn every_options_literal_names_the_wgpu_renderer() {
    let mut sites: Vec<String> = Vec::new();

    for (path, src) in repo_files("main.rs")
        .into_iter()
        .chain(repo_files("urdr-threads.rs"))
        .chain(repo_files("lib.rs"))
    {
        let lines: Vec<&str> = src.lines().collect();
        for (i, line) in lines.iter().enumerate() {
            if !(line.contains("NativeOptions {") || line.contains("WebOptions {")) {
                continue;
            }
            // `..Default::default()` alone is not a literal we can check for a field, but
            // `eframe::WebOptions::default()` is a call, not a struct literal — the
            // `{`-suffix match above already excludes it.
            let body = lines[i..]
                .iter()
                .take(30)
                .cloned()
                .collect::<Vec<_>>()
                .join("\n");
            assert!(
                body.contains("renderer: eframe::Renderer::Wgpu"),
                "LAW #4: the eframe options literal at {path}:{} must name \
                 `renderer: eframe::Renderer::Wgpu` explicitly. Leaving it to \
                 `Renderer::default()` is what let a manifest word decide nornir's renderer \
                 — and on the web path, decide that the page was WebGL2.",
                i + 1
            );
            sites.push(format!("{path}:{}", i + 1));
        }

        assert!(
            !src.contains("Renderer::Glow"),
            "LAW #4: {path} must not mention `Renderer::Glow` at all — not as a fallback, \
             not behind a cfg, not in a chooser. nornir-robotui had exactly such a \
             three-way chooser whose `Auto` arm MEANT 'wgpu if compiled, else glow', and \
             that silent fallback WAS the violation; it was deleted in robotui 0.3.0."
        );
    }

    // Non-vacuity: nornir has three such literals today — `src/bin/urdr-threads.rs`
    // (the desktop viz), `crates/nornir-viz-web/src/main.rs` native preview, and that same
    // file's `WebOptions` (the browser bundle). Zero would mean the scanner found nothing
    // and the loop above proved nothing.
    assert!(
        sites.len() >= 3,
        "expected at least the 3 known options literals (urdr-threads NativeOptions, \
         viz-web NativeOptions, viz-web WebOptions); found {sites:?} — a broken scanner \
         makes this guard pass while checking nothing"
    );
}

/// **The guard is not blind: prove the detector fires.**
///
/// A manifest-reading test is exactly the kind that quietly stops working — a renamed key,
/// a reformatted table, or an over-eager comment filter, and it inspects nothing while
/// still reporting `ok`. So the parser is run against the lines nornir really had before
/// this fix, and against the lines it has now, and the two must disagree.
#[test]
fn the_guard_itself_can_report_red() {
    // Verbatim nornir's three pre-fix lines, as they stood on origin/main at 3e78305.
    const ROOT_BEFORE: &str = r#"eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow", "wayland", "x11", "accesskit"], optional = true }"#;
    const WEB_WASM_BEFORE: &str = r#"eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow"] }"#;
    const WEB_NATIVE_BEFORE: &str = r#"eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow", "wayland", "x11", "accesskit"] }"#;

    for (what, line) in [
        ("root", ROOT_BEFORE),
        ("viz-web wasm32", WEB_WASM_BEFORE),
        ("viz-web native", WEB_NATIVE_BEFORE),
    ] {
        assert!(
            line.contains("\"glow\""),
            "the detector must see glow in the {what} pre-fix line"
        );
        assert!(
            !line.contains("\"wgpu\""),
            "the {what} pre-fix line asked for no WebGPU backend"
        );
    }

    // The extractor must return BOTH lines of the target-split viz-web manifest. This is
    // the shape that actually exists in this repo, and a `find()`-based extractor would
    // report green while checking only one of the two — with the WebGL2 line the likelier
    // one to be missed, since it sits in the wasm32 block.
    let split = format!(
        "[target.'cfg(target_arch = \"wasm32\")'.dependencies]\n{WEB_WASM_BEFORE}\n\
         wasm-bindgen = \"0.2\"\n\
         [target.'cfg(not(target_arch = \"wasm32\"))'.dependencies]\n{WEB_NATIVE_BEFORE}\n"
    );
    let found = eframe_lines(&split);
    assert_eq!(
        found,
        vec![WEB_WASM_BEFORE.to_owned(), WEB_NATIVE_BEFORE.to_owned()],
        "the extractor must return EVERY eframe line of a target-split manifest, in order"
    );

    // The BARE form must be caught too. holger's mannequin had one; contrary to an earlier
    // claim of mine it was NOT a hidden GL violation (eframe's defaults are wgpu-based), but
    // it does leave the backend inherited rather than named. nornir has no bare eframe line
    // today; this asserts the detector would catch one if a future edit introduced it.
    let bare = "[dependencies]\neframe = \"0.34\"\negui = \"0.34\"\n";
    let bare_line = eframe_lines(bare);
    assert_eq!(
        bare_line,
        vec!["eframe = \"0.34\"".to_owned()],
        "the extractor must catch the bare form"
    );
    assert!(
        !bare_line[0].contains("default-features = false"),
        "the bare form must fail the default-features assertion — that is the only check \
         that can catch a violation whose evidence is an ABSENCE"
    );

    // A manifest whose only mention of eframe is a COMMENT must be treated as declaring
    // none — otherwise the prose explaining why glow is forbidden could itself satisfy or
    // trip the guard. Both fixed manifests now carry exactly such prose.
    let commented = "[dependencies]\n# eframe = { features = [\"glow\"] }\nserde = \"1\"\n";
    assert!(
        eframe_lines(commented).is_empty(),
        "the extractor must ignore comments — otherwise the prose explaining why glow is \
         forbidden could itself satisfy or trip the guard"
    );

    // Finally: the extractor pulls the LIVE lines, and they are the fixed shape — so the
    // checks above are running against the real manifests and not only against constants.
    let live: Vec<String> = all_manifests()
        .iter()
        .flat_map(|(_, src)| eframe_lines(src))
        .collect();
    assert!(
        live.len() >= 3,
        "the live walk must find nornir's three eframe lines, found {live:?}"
    );
    for l in &live {
        assert!(
            !l.contains("\"glow\""),
            "live manifest is the post-fix shape: {l}"
        );
        assert!(l.contains("\"wgpu\""), "live manifest asks for wgpu: {l}");
    }
}