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}");
}
}