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

nordisk/facett

facett-map/tests/one_gpu_turn_is_the_only_one.rs

//! **ONE device turn for this crate's tests, and every file that opens a device takes it.**
//!
//! # The defect, measured on oden 2026-08-27
//!
//! `egui_kittest::Harness::render()` brings a wgpu device up. libtest runs `#[test]`
//! bodies on parallel threads. Two bring-ups at once take the whole test binary out with
//! `signal: 11, SIGSEGV` — not an assertion failure, a hard crash whose output looks
//! nothing like the test that was running, and which fails the *file*, not the test.
//!
//! The hazard is intermittent, which is why it was survivable and why it kept coming
//! back. Taking the lock back OUT of the two files that had it, default runner, five runs
//! each:
//!
//! ```text
//!   rail_tier_pixels          SIGSEGV 2/5
//!   place_label_tier_pixels   SIGSEGV 3/5
//! ```
//!
//! and the other door, `common::headless_device`, over eight runs each:
//!
//! ```text
//!   line_gpu (7 tests, 7 devices)   SIGSEGV 1/8
//! ```
//!
//! A file that came back green eight times is **not** a file that is safe — five of the
//! unlocked kittest files did exactly that (0/40 SIGSEGV) at a rate the two probed files
//! show is real. So the rule is not "the files that have crashed take the turn", it is
//! "every file that brings a device up takes it", and this guard is what keeps that true.
//!
//! # What went wrong the first time, and what this file forbids
//!
//! Six copies of the same two-line `static … Mutex<()>` existed at once — five declared
//! per file as `GPU_TURN`, plus `common/one_gpu.rs`, which arrived last and retired none
//! of them. A twinned *lock* is worse than a twinned helper: two mutexes over one device
//! serialise nothing at all, and the second one reads as coverage. Worse still, Rust
//! makes the twin invisible — `#[path = "common/one_gpu.rs"] mod one_gpu;` at a test
//! binary's root and `mod common;` → `common::one_gpu` compile **two different statics
//! out of one file**. So:
//!
//! 1. `common/one_gpu.rs` is the only place a device turn may be declared;
//! 2. a file that says `mod common;` reaches it as `common::one_gpu` and must NOT also
//!    `#[path]`-include it — that is the invisible twin;
//! 3. every file that brings a device up from more than one `#[test]` takes the turn,
//!    either by holding it itself or by going through `common::headless_device`, which
//!    holds it for the bring-up.
//!
//! A single-`#[test]` binary is exempt from (3) and only from (3): it cannot race itself,
//! separate test binaries are separate processes, and the static is per binary.
//!
//! Run: `cargo test -p facett-map --test one_gpu_turn_is_the_only_one`

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

/// This file itself — it quotes every pattern it forbids, so it must never judge itself.
const SELF: &str = "one_gpu_turn_is_the_only_one.rs";

/// The one file allowed to declare a device turn.
const THE_ONE_WRITER: &str = "common/one_gpu.rs";

struct TestFile {
    name: String,
    text: String,
}

impl TestFile {
    fn tests(&self) -> usize {
        self.text.matches("#[test]").count()
    }
    /// Does this file bring a device up through `egui_kittest`? `Harness::render()` and
    /// `snapshot()` are the two that do; `Harness::run()` drives egui with no device at
    /// all, which is why files like `label_collision.rs` are not on this list.
    fn kittest_device(&self) -> bool {
        self.text.contains(".render()")
            || self.text.contains(".snapshot(")
            || self.text.contains("create_render_state(")
    }
    /// Does it bring a raw wgpu device up?
    fn raw_device(&self) -> bool {
        self.text.contains("headless_device(")
            || self.text.contains("device_or_skip(")
            || self.text.contains("request_device(")
    }
    fn holds(&self) -> bool {
        self.text.contains("one_gpu::hold()")
    }
    fn says_mod_common(&self) -> bool {
        self.text.lines().any(|l| l.trim() == "mod common;")
    }
    fn path_includes_one_gpu(&self) -> bool {
        self.text.contains("#[path = \"common/one_gpu.rs\"]")
    }
    /// A `static … Mutex<()>` — the shape of a hand-rolled device turn.
    fn declares_a_turn(&self) -> bool {
        self.text.contains("Mutex::new(())")
    }
    /// Does it reach the SHARED turn in `facett-core` instead of declaring one? Since
    /// 2026-08-31 that is where the lock lives: `facett-core`'s own device probes and
    /// `facett-map3d`'s tests could not take a lock that sat in this crate's test
    /// directory, and both then produced exactly the failures it exists to prevent (a
    /// 13-minute deadlock and a `signal: 11`). Delegating is the RIGHT answer for the one
    /// writer; declaring a second static is still the wrong one for everybody.
    fn delegates_to_core(&self) -> bool {
        self.text.contains("facett_core::render::gputurn")
    }
}

fn tests_dir() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("tests")
}

/// Every `tests/*.rs` plus `tests/common/*.rs`, by the name this file reports them under.
fn scan() -> BTreeMap<String, TestFile> {
    let dir = tests_dir();
    let mut out = BTreeMap::new();
    let mut roots: Vec<PathBuf> = std::fs::read_dir(&dir)
        .unwrap_or_else(|e| panic!("read {}: {e}", dir.display()))
        .filter_map(Result::ok)
        .map(|e| e.path())
        .filter(|p| p.extension().is_some_and(|x| x == "rs"))
        .collect();
    let common = dir.join("common");
    if common.is_dir() {
        roots.extend(
            std::fs::read_dir(&common)
                .unwrap_or_else(|e| panic!("read {}: {e}", common.display()))
                .filter_map(Result::ok)
                .map(|e| e.path())
                .filter(|p| p.extension().is_some_and(|x| x == "rs")),
        );
    }
    roots.sort();
    for p in roots {
        let name = if p.parent().is_some_and(|d| d.ends_with("common")) {
            format!("common/{}", p.file_name().unwrap().to_string_lossy())
        } else {
            p.file_name().unwrap().to_string_lossy().into_owned()
        };
        if name == SELF {
            continue;
        }
        let text = std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {p:?}: {e}"));
        out.insert(name.clone(), TestFile { name, text });
    }
    out
}

/// Non-vacuity, asserted once and used by every test below: a scan that stopped seeing
/// this directory would satisfy every set-difference in this file.
fn scanned() -> BTreeMap<String, TestFile> {
    let files = scan();
    assert!(
        files.len() >= 25,
        "the scan found only {} test sources under {:?} — it is not looking at facett-map, \
         so its silence proves nothing",
        files.len(),
        tests_dir()
    );
    assert!(
        files.contains_key(THE_ONE_WRITER),
        "{THE_ONE_WRITER} is gone — the shared turn has no declaration, and every \
         `one_gpu::hold()` below is naming something that no longer exists"
    );
    let device_files = files.values().filter(|f| f.kittest_device() || f.raw_device()).count();
    assert!(
        device_files >= 12,
        "only {device_files} of {} test files look like they open a device — the pattern \
         list has stopped matching this crate, and the rules below would pass vacuously",
        files.len()
    );
    files
}

/// Unit mutexes in this directory that are **not** device turns, with the reason each
/// one exists. An excuse with no words is not an excuse, and a stale one is worse than
/// none: both are asserted below.
///
/// `std::sync::Mutex<()>` is the shape a hand-rolled device turn takes, and it is also
/// the shape of every other "these two tests must not overlap" lock. The difference is
/// WHAT is being protected, and only a sentence can say that — so the sentence is here,
/// where the guard can read it, rather than in a comment nobody re-reads.
const NOT_A_DEVICE_TURN: &[(&str, &str)] = &[
    (
        "gpu_clock.rs",
        "LANE_LOCK serialises the process-global LANE RECORD (`lane::lane_use()`), not a \
         device: both tests read a byte-count delta off one global counter and would smear \
         each other's uploads. Its own device bring-up (`feature_device`) takes the shared \
         turn separately, and must keep doing so.",
    ),
    (
        "zero_copy_gpu_lane.rs",
        "SERIAL serialises HEAP MEASUREMENT under a counting global allocator — the \
         thread-local counters are what make the numbers correct and this only keeps the \
         fixture builds from thrashing each other. No device is opened in this file.",
    ),
];

/// **RED-when-broken: the device turn is declared in exactly one place.**
///
/// RED PROVEN on oden 2026-08-27: putting `static GPU_TURN: std::sync::Mutex<()> =
/// std::sync::Mutex::new(());` back into `paint_order_pixels.rs` fails with
/// `["paint_order_pixels.rs"] declare their own unit mutex`.
#[test]
fn only_common_one_gpu_declares_a_device_turn() {
    let files = scanned();
    let one = &files[THE_ONE_WRITER];
    assert!(
        one.declares_a_turn() || one.delegates_to_core(),
        "{THE_ONE_WRITER} neither declares a `Mutex::new(())` nor re-exports \
         `facett_core::render::gputurn` — the one writer is empty, and this whole guard \
         would then report every file clean"
    );
    assert!(
        one.text.matches("Mutex::new(())").count() <= 1,
        "{THE_ONE_WRITER} declares more than one turn — the shared lock must be a single \
         static, or the files that take `hold()` are not all taking the same one"
    );
    // Delegation and declaration are alternatives, never both: a file that re-exports the
    // shared turn AND keeps a local static hands half its callers the wrong lock.
    assert!(
        !(one.declares_a_turn() && one.delegates_to_core()),
        "{THE_ONE_WRITER} both declares a local `Mutex::new(())` and re-exports \
         `facett_core::render::gputurn` — two locks over one device, which is the exact \
         shape that let six copies coexist while the crashes went on"
    );

    // An excuse with no words is not an excuse.
    let thin: Vec<&str> = NOT_A_DEVICE_TURN
        .iter()
        .filter(|(_, why)| why.trim().len() < 40)
        .map(|(f, _)| *f)
        .collect();
    assert!(thin.is_empty(), "NOT_A_DEVICE_TURN entries whose reason is a shrug: {thin:?}");

    // A stale excuse says a gap is accepted when the gap is gone, and the next real twin
    // hides behind it.
    let stale: Vec<&str> = NOT_A_DEVICE_TURN
        .iter()
        .filter(|(f, _)| files.get(*f).is_none_or(|tf| !tf.declares_a_turn()))
        .map(|(f, _)| *f)
        .collect();
    assert!(
        stale.is_empty(),
        "NOT_A_DEVICE_TURN excuses {stale:?}, which no longer declare a unit mutex (or no \
         longer exist) — delete the lines, or the list rots into a blanket"
    );

    let excused: std::collections::BTreeSet<&str> =
        NOT_A_DEVICE_TURN.iter().map(|(f, _)| *f).collect();
    let twins: Vec<&str> = files
        .values()
        .filter(|f| {
            f.name != THE_ONE_WRITER && f.declares_a_turn() && !excused.contains(f.name.as_str())
        })
        .map(|f| f.name.as_str())
        .collect();
    assert!(
        twins.is_empty(),
        "{twins:?} declare their own unit mutex. Six copies of the DEVICE turn existed at \
         once on 2026-08-27 and the crashes went on anyway: two mutexes over one device \
         serialise nothing, and the second one reads as coverage. If this is the device \
         turn, take `{THE_ONE_WRITER}`'s instead. If it protects something else, say what \
         in `NOT_A_DEVICE_TURN` — in a sentence, where this guard can read it."
    );
}

/// **RED-when-broken: no binary compiles two copies of the shared static.**
///
/// `#[path = "common/one_gpu.rs"] mod one_gpu;` at a test binary's root and `mod common;`
/// → `common::one_gpu` are two DIFFERENT modules built from one file, each with its own
/// `static`. A file doing both takes two different locks and is exactly as unprotected as
/// a file taking none — while looking, in every grep, like it is protected twice.
///
/// RED PROVEN on oden 2026-08-27: adding the `#[path]` include back to
/// `fill_extent_gate.rs` (which says `mod common;`) fails with `fill_extent_gate.rs
/// declares BOTH`.
#[test]
fn no_file_reaches_the_shared_turn_by_two_names_at_once() {
    let files = scanned();
    let both: Vec<&str> = files
        .values()
        .filter(|f| f.name != THE_ONE_WRITER && f.says_mod_common() && f.path_includes_one_gpu())
        .map(|f| f.name.as_str())
        .collect();
    assert!(
        both.is_empty(),
        "{both:?} declare BOTH `mod common;` and `#[path = \"common/one_gpu.rs\"]`. That \
         compiles TWO statics out of one file into one test binary, and two mutexes over \
         one device serialise nothing. A file with `mod common;` says \
         `common::one_gpu::hold()`; a file without it `#[path]`-includes the module and \
         says `one_gpu::hold()`. Never both."
    );

    // Non-vacuity in both directions: this rule is a set difference, and it would pass on
    // a crate where NOBODY reached the turn either way.
    let by_path = files.values().filter(|f| f.path_includes_one_gpu()).count();
    let by_common = files.values().filter(|f| f.says_mod_common()).count();
    assert!(
        by_path >= 5 && by_common >= 5,
        "only {by_path} file(s) reach the turn by `#[path]` and {by_common} by \
         `mod common;` — with one route empty this rule can never fire"
    );
}

/// **RED-when-broken: every file that opens a device from more than one `#[test]` takes
/// the turn.**
///
/// The two routes are equally accepted, because both end at the same static:
/// * the file holds it itself (`one_gpu::hold()`), which the `egui_kittest` render lane
///   must do — `headless_device` is not on that path;
/// * or it goes through `common::headless_device`, which takes the turn for the bring-up,
///   which is what `mod common;` buys the raw-wgpu files.
///
/// RED PROVEN on oden 2026-08-27: deleting the `let _gpu = one_gpu::hold();` from
/// `render_proof.rs::assert_non_blank` fails with `render_proof.rs renders from 7
/// #[test]s and never takes the turn`.
#[test]
fn every_file_that_opens_a_device_from_two_tests_takes_the_turn() {
    let files = scanned();
    let mut naked: Vec<String> = Vec::new();
    let mut covered = 0usize;
    let mut exempt_single = 0usize;

    for f in files.values() {
        if f.name == THE_ONE_WRITER || !(f.kittest_device() || f.raw_device()) {
            continue;
        }
        // A binary with one `#[test]` cannot race itself, and separate test binaries are
        // separate processes. The static is per binary, so this exemption is exact.
        if f.tests() < 2 {
            exempt_single += 1;
            continue;
        }
        let ok = if f.kittest_device() {
            // The kittest render lane never passes through `headless_device`, so
            // `mod common;` buys it nothing. It must hold the turn itself.
            f.holds()
        } else {
            f.holds() || f.says_mod_common()
        };
        if ok {
            covered += 1;
        } else {
            naked.push(format!(
                "  {:32} {} device from {} #[test]s, and never takes the turn",
                f.name,
                if f.kittest_device() { "kittest" } else { "raw wgpu" },
                f.tests()
            ));
        }
    }

    assert!(
        covered >= 10,
        "only {covered} file(s) were found to take the turn — the detection has stopped \
         matching, and an empty left-hand side passes every check below"
    );
    assert!(
        naked.is_empty(),
        "{} test file(s) bring a wgpu device up from more than one `#[test]` and take no \
         turn. That is a `signal: 11` waiting for the scheduler, not a test failure — \
         MEASURED on oden 2026-08-27 at 2/5, 3/5 and 1/8 on three different files:\n\n{}\n\n\
         Fix: `mod common;` files say `common::one_gpu::hold()`, the rest \
         `#[path = \"{THE_ONE_WRITER}\"] mod one_gpu;` then `one_gpu::hold()`. \
         ({covered} file(s) already do; {exempt_single} are single-`#[test]` binaries and \
         cannot race themselves.)",
        naked.len(),
        naked.join("\n"),
    );
}