Gunnar

nordisk/holger-egui

tests/law4_no_opengl_backend.rs

//! **LAW #4 — holger's GUIs render on WebGPU, never OpenGL.**
//!
//! The nordisk rule is "modern platforms only: WebGPU everywhere, no WebGL/glow". holger
//! had ONE real violation and one crate that merely left the choice implicit.
//!
//! **`server/ui/Cargo.toml` was the real one.** It said
//! `eframe = { default-features = false, features = ["default_fonts", "glow", …] }` —
//! `glow` on, `wgpu` absent — and `main.rs` passed `NativeOptions { ..Default::default() }`
//! with no `renderer` field. From eframe 0.34.3 `src/epi.rs` `impl Default for Renderer`:
//!
//! | `glow` | `wgpu` | `Renderer::default()`                          |
//! |--------|--------|------------------------------------------------|
//! | on     | off    | `Glow`                                         |
//! | off    | on     | `Wgpu`                                         |
//! | on     | on     | `Wgpu` — verbatim: "pick the better of the two" |
//! | off    | off    | `compile_error!`                               |
//!
//! `server/ui` sat in the first row, so the holger operator GUI really did render OpenGL,
//! chosen by one word in a manifest with nothing in the source saying so.
//!
//! ## A RETRACTION, kept here on purpose
//!
//! An earlier version of this guard claimed `mannequin/Cargo.toml`'s bare
//! `eframe = "0.34"` was a second, *invisible* violation, on the premise that eframe's
//! default feature set includes `glow`. **That premise is false.** eframe 0.34.3's
//! `default` is:
//!
//! ```text
//! default = ["accesskit", "default_fonts", "wayland", "web_screen_reader", "wgpu",
//!            "winit/default", "x11"]
//! ```
//!
//! `glow` is NOT a default; `wgpu` IS. A bare `eframe = "0.34"` therefore renders **WebGPU**,
//! and `holger-mannequin` was already doing so. Measured, not reasoned:
//! `cargo tree -p holger-mannequin -e features -i glow` against the pre-fix manifest
//! attributes `eframe feature "wgpu"` to `eframe feature "default"`, and attributes
//! `eframe feature "glow"` to **`nornir-robotui v0.2.6` alone** — a path dep on the sibling
//! edda checkout, both before and after the fix.
//!
//! **That last attribution is now HISTORY, and the egui 0.35 migration is what ended it.**
//! `nornir-robotui 0.2.6` was the *unpublished* sibling-edda copy holger reached through a
//! `[patch.crates-io]`; the migration dropped that patch for the published, egui-0.35,
//! glow-free `0.3.0`. Re-measured 2026-08-01 on that graph, `glow v0.17.0` has exactly ONE
//! root in the whole workspace and it is `wgpu-hal v29.0.3` — wgpu's own optional GL
//! backend, which is not eframe's renderer and not something a manifest here selects:
//!
//! ```text
//! glow v0.17.0
//! └── wgpu-hal v29.0.3
//!     ├── wgpu v29.0.3 → egui-wgpu v0.35.0 → eframe v0.35.0 → holger-mannequin
//!     │                                                     → nornir-robotui v0.3.0
//!     ├── wgpu-core v29.0.3
//!     └── wgpu-core-deps-windows-linux-android v29.0.3
//! ```
//!
//! So the `eframe feature "glow"` edge is GONE from holger's tree entirely — robotui no
//! longer asks for it. (`egui_glow` still has a `Cargo.lock` ENTRY, because eframe declares
//! it as an optional dep and the lock records those; nothing links it —
//! `cargo tree --workspace -i egui_glow --target all` prints "nothing to print". It used to
//! appear TWICE, at 0.34 and 0.35, and is now down to one dormant line.)
//!
//! So mannequin's change was housekeeping, not a rescue: it pins the backend explicitly
//! instead of inheriting it. The retraction is written down rather than quietly edited out
//! because a guard whose stated reason is wrong teaches the next reader the wrong rule.
//!
//! ## Why `default-features = false` is still asserted
//!
//! Not because the defaults are dangerous today — they are `wgpu`. Because inheriting them
//! means this crate's renderer is decided by eframe's release notes rather than by holger:
//! upstream changed which backend is default at least once already, and a target-specific
//! default (as `nornir-viz-web` has) can differ per platform. Pinning it makes the tree
//! state its own backend. That is a real reason, and it is the one this guard now gives.
//!
//! This test asserts both halves:
//!
//!   1. **every manifest in the repo** that declares `eframe` names `wgpu`, never `glow`,
//!      and pins `default-features = false`; and
//!   2. **every `NativeOptions` in the repo** names its renderer explicitly, because with
//!      `glow` gone `Renderer::default()` resolves to `Wgpu` anyway and a future edit could
//!      delete the explicit line and leave the renderer implicit again. An implicit renderer
//!      is the disease this law is about.
//!
//! 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.
//!
//! Sibling guards, same law, same shape: `knut/crates/knut-popviz/tests/`, `korp/tests/`
//! and `nornir/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 sibling checkouts are off their parked
//! feature branches, is `nornir-testmatrix::gatedtests`, which already walks workspace
//! members for a different law.)

// `repo_root` — the directory whose `Cargo.toml` declares `[workspace]` — used to live
// here. It is now `common::repo_root`, shared with `tier_ladder.rs` and `thin_client.rs`
// (LAW #5: those two had their own, weaker, `.nth(2)` version of the same idea). Walking
// up to the workspace manifest is what lets one test file in one crate cover `mannequin/`,
// a sibling member, and where the bare-eframe line lived.
mod common;
use common::repo_root;

/// Every file in the repo matching `want`, as `(path relative to root, contents)`.
///
/// Walks the tree rather than the `[workspace] members` list on purpose: members here is
/// a mix of literals and the `server/repository/*` glob, and any future GUI crate added
/// outside it 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.
    // holger had 36 manifests when this guard was written (root + traits + server/* +
    // server/repository/* + mannequin + …).
    assert!(
        out.len() >= 20,
        "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, with comments excluded so the prose
/// ABOUT `glow` in these manifests cannot make this test pass or fail.
///
/// Returns EVERY match, not the first: a manifest can be target-split into a wasm32 and a
/// native eframe line (nornir's `nornir-viz-web` has exactly that shape), and a `find()`
/// would silently check one and ignore the other.
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 checked: Vec<String> = Vec::new();

    for (path, src) in all_manifests() {
        for line in eframe_lines(&src) {
            checked.push(path.clone());

            assert!(
                !line.contains("\"glow\""),
                "LAW #4: {path} must not link eframe's OpenGL backend. With `glow` on and \
                 `wgpu` OFF — which is exactly how both of holger's GUIs were declared — \
                 eframe resolves `Renderer::default()` to `Glow`, so this one word in a \
                 manifest decides that the shipped binary is OpenGL with nothing in the \
                 source saying so. Offending line:\n  {line}"
            );
            // This catches the BARE shape (`eframe = "0.34"`, as mannequin had), which names
            // neither backend. Note what it is and is NOT: eframe 0.34.3's defaults are
            // ["accesskit", "default_fonts", "wayland", "web_screen_reader", "wgpu",
            // "winit/default", "x11"] — `wgpu`, NOT `glow` — so a bare line renders WebGPU
            // and is not itself a violation. It is asserted anyway because inheriting the
            // defaults means an upstream release note, not this repo, decides the renderer.
            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 eframe's defaults is not a \
                 GL bug today — 0.34.3's defaults include `wgpu`, not `glow` — but it means \
                 the backend is decided by eframe's release notes rather than by this repo, \
                 and eframe has already changed which backend is default once. Pin it and the \
                 tree states its own renderer. Offending line:\n  {line}"
            );
            assert!(
                line.contains("\"wgpu\""),
                "LAW #4: {path} must ASK for the WebGPU backend explicitly. Dropping both \
                 eframe backends is a `compile_error!` in eframe, so an eframe line naming \
                 neither cannot be right. Offending line:\n  {line}"
            );
        }
    }

    // Non-vacuity: holger declares eframe in exactly two manifests today — `server/ui`
    // (the operator GUI) and `mannequin` (the test-dummy GUI). If this list shrinks, the
    // detector stopped finding dependency lines; it does not mean a GUI went away.
    assert!(
        checked.len() >= 2,
        "expected at least the two known eframe manifests (server/ui, mannequin); inspected \
         {checked:?} — a shrunken list means the detector stopped finding dependency lines, \
         not that the violations were fixed"
    );
}

/// **Every `NativeOptions` in the repo must name its renderer.**
///
/// With `glow` gone from the manifests, `eframe::Renderer::default()` resolves to `Wgpu`,
/// so deleting the explicit `renderer:` lines would leave holger still rendering WebGPU
/// *today* — and leave its backend decided, once again, by nothing but feature
/// resolution. That implicitness IS the bug this law is about, so the explicit line is
/// asserted even though it currently agrees with the default.
#[test]
fn every_native_options_names_the_wgpu_renderer() {
    let mut sites = 0usize;

    for (path, src) in repo_files("main.rs")
        .into_iter()
        .chain(repo_files("gui.rs"))
    {
        for (i, line) in src.lines().enumerate() {
            if !line.contains("NativeOptions {") {
                continue;
            }
            sites += 1;
            // The `renderer:` field must appear inside this struct literal. The literals
            // in this repo are a handful of lines; 30 covers them with room to spare.
            let body: String = src.lines().skip(i).take(30).collect::<Vec<_>>().join("\n");
            assert!(
                body.contains("renderer: eframe::Renderer::Wgpu"),
                "LAW #4: the `eframe::NativeOptions` at {path}:{} must name \
                 `renderer: eframe::Renderer::Wgpu` explicitly. Leaving it to \
                 `Renderer::default()` is what let holger's manifests decide the renderer \
                 with nothing in the source saying so.",
                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)"
        );
    }

    // Non-vacuity: holger has three `NativeOptions` literals today — two in
    // `server/ui/src/main.rs` (live + `--demo`) and one in `mannequin/src/gui.rs`. Zero
    // would mean the scanner found nothing and the loop above proved nothing.
    assert!(
        sites >= 3,
        "expected at least the 3 known NativeOptions sites (server/ui live, server/ui \
         --demo, mannequin gui); found {sites} — the scanner is broken, and 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 holger 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 holger's pre-fix lines, as they stood on origin/main at 64c67e9.
    const UI_BEFORE: &str = r#"eframe = { version = "0.34", optional = true, default-features = false, features = ["default_fonts", "glow", "wayland", "x11"] }"#;
    const MANNEQUIN_BEFORE: &str = r#"eframe = "0.34""#;

    // The loud shape: caught by the glow check.
    assert!(
        UI_BEFORE.contains("\"glow\""),
        "the detector must see glow in server/ui's pre-fix line"
    );
    assert!(
        !UI_BEFORE.contains("\"wgpu\""),
        "server/ui's pre-fix line asked for no WebGPU backend"
    );

    // The BARE shape, and the reason the `default-features = false` assertion exists.
    // mannequin's pre-fix line names NEITHER backend, so neither the glow check nor the
    // wgpu check can catch it; only the default-features assertion fires on it.
    //
    // What it was NOT: a hidden OpenGL violation. eframe 0.34.3's `default` is
    // ["accesskit", "default_fonts", "wayland", "web_screen_reader", "wgpu",
    //  "winit/default", "x11"] — `wgpu`, not `glow` — so this line already rendered WebGPU.
    // Measured, not argued: with this exact line restored,
    // `cargo tree -p holger-mannequin -e features -i glow` attributes `eframe feature
    // "wgpu"` to `eframe feature "default"` and attributes `eframe feature "glow"` to
    // `nornir-robotui v0.2.6` ALONE. The glow in mannequin's tree was always the parked
    // edda path-dep, never this line.
    //
    // That parked path-dep is GONE as of the egui 0.35 migration (published robotui 0.3.0
    // is glow-free), so the `eframe feature "glow"` edge no longer exists in this
    // workspace at all — see the module doc above for the re-measured tree. The fixture
    // below still asserts what mannequin's OLD line did and did not say, which is a fact
    // about that string and stays true regardless.
    assert!(
        !MANNEQUIN_BEFORE.contains("\"glow\""),
        "mannequin's pre-fix line does NOT contain the word glow"
    );
    assert!(
        !MANNEQUIN_BEFORE.contains("\"wgpu\""),
        "nor does it name wgpu — it named no backend at all, which is the shape the \
         default-features assertion exists to catch"
    );
    assert!(
        !MANNEQUIN_BEFORE.contains("default-features = false"),
        "mannequin's pre-fix line inherited eframe's defaults (which are wgpu-based, so this \
         was housekeeping and not a GL rescue) — the `default-features = false` assertion is \
         the one that catches this shape"
    );

    // The extractor must pull both shapes intact out of a manifest body...
    assert_eq!(
        eframe_lines(&format!("[dependencies]\n{UI_BEFORE}\nserde = \"1\"\n")),
        vec![UI_BEFORE.to_owned()],
        "the extractor must pull a table-form eframe line intact"
    );
    assert_eq!(
        eframe_lines(&format!(
            "[dependencies]\n{MANNEQUIN_BEFORE}\negui = \"0.34\"\n"
        )),
        vec![MANNEQUIN_BEFORE.to_owned()],
        "the extractor must also pull the BARE `eframe = \"0.34\"` form — if it only matched \
         table-form `eframe = {{ … }}` lines, a bare declaration would stay invisible"
    );

    // ...and it must return EVERY line when a manifest is target-split, because a
    // `find()`-style extractor would check one and silently ignore the other.
    let two = format!(
        "[target.'cfg(target_arch = \"wasm32\")'.dependencies]\n{}\n\
         [target.'cfg(not(target_arch = \"wasm32\"))'.dependencies]\n{}\n",
        r#"eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow"] }"#,
        r#"eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow", "wayland", "x11", "accesskit"] }"#,
    );
    assert_eq!(
        eframe_lines(&two).len(),
        2,
        "the extractor must return EVERY eframe line, not just the first"
    );

    // 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 of holger's 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() >= 2,
        "the live walk must find holger's two 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}");
    }
}