Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

Where a subsystem can be tested without a GPU or a window, it is. This is not an aspiration; it is why several types are shaped the way they are. This page lists the seams that make it possible and the tests that cross them.

What is deliberately not in the testable path

TestableDepends onEnabled by
Whole simulationnothingEngine has no surface
Player movementa CollisionWorldthe trait, and BoxWorld
Entity I/OEntityWorldhandlers take &mut EntityWorld only
Scriptsa WorldView snapshotscripts return ScriptActions
Rendering decisionsBsp + WorldMeshmesh.rs/lightmap.rs are CPU-only
Shadersnagasame compiler wgpu uses
Audio mixingbuffersmixer.rs has no device
Compilersplain dataeach stage is a pure function
The editorthe libraryegui is only the drawing layer

Movement against a hand-made world

crates/kerosene-physics/src/world.rs defines CollisionWorld, implemented by BspWorld for real maps and BoxWorld for tests. BoxWorld is a floor, a step, a wall and a volume; the tests in crates/kerosene-physics/src/movement/tests.rs (855 lines) pin down every rule — friction, the air-speed cap, stair stepping, sliding — against a world whose expected answer is obvious. Testing movement against a compiled map means debugging the map when a test fails.

The test-world feature exposes BoxWorld to other crates without compiling it into a release build.

Entity I/O without an engine

Class handlers are fn(&mut EntityWorld, EntityId, …). That is the whole reason the trait uses function pointers and not closures over an engine: a test can call EntityWorld::spawn, load_from_kv, fire_output, run, and assert on the result with no Engine in sight. crates/kerosene-entity/src/world/tests.rs does exactly that, including the queue ordering and generation-reuse cases.

Scripts as pure functions

crates/kerosene-script/src/lib.rs tests scripts against a hand-built WorldView and asserts on take_actions(). Because a script run is a pure function of the snapshot, the same script run twice on the same world does the same thing — which is a property that can be asserted, not hoped for.

Shaders through naga

crates/kerosene-render/src/gpu.rs has a test module (around line 1513) that parses and validates every WGSL string with naga, the same compiler wgpu uses internally. A typo therefore fails in CI on a headless runner rather than at pipeline creation on a machine with a display — the only machine where the bug would otherwise appear.

The whole pipeline: the engine’s integration tests

crates/kerosene-engine/tests/ holds three suites that go through the entire stack, because each crate can pass its own tests and still not add up to a level you can walk around:

  • playthrough.rs — build a map in memory, compile it through Cleave, load the .kbsp, spawn entities, move the player, fire inputs, walk through a door.
  • streaming.rs — a hall of four rooms; the far room’s crates are in a streamed visgroup and the section comes and goes as the player approaches and leaves.
  • acoustics.rs — the whole acoustic chain: a map compiled through Cleave, Umbra and Resonance, loaded by the engine and listened to, asserting that the room the compiler wrote is the room the engine hears and that a wall the compiler saw is a wall the mixer muffles.

The common fixture tests/common/mod.rs is the stock game as a Game impl, so these suites exercise the same class handlers a real project would.

---
config:
  layout: elk
---
flowchart LR
    lvl(["unit tests"]) --> crate["each crate tests its own logic<br/>against plain data"]
    crate --> integ["engine integration tests:<br/>playthrough, streaming, acoustics"]
    integ --> shader["naga shader validation"]
    shader --> ci["CI: fmt, clippy -D warnings, tests"]
    ci --> build["Windows + macOS: build only"]

    classDef proc fill:#FF6D00,color:#fff
    class lvl,crate,integ,shader,ci,build proc

The compiler tests

Each compiler has its own tests for the stage it owns — tools/cleave, tools/umbra, tools/radiance, tools/resonance — and the engine’s suites are where their outputs meet. Cleave is a library (tools/cleave/src/lib.rs, pipeline::compile), so a test builds a map without shelling out and without a temporary file in between. That is also why Chisel and Kiln can both drive the same compile.

CI

.github/workflows/ci.yml:

  • check (Linux) runs cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo xtask layers, cargo test --workspace, the Steam feature’s clippy and tests, and cargo doc on the published crate with warnings as errors.
  • platforms (Windows and macOS) runs cargo check --workspace --all-targets. The point of those two is to find out the tree still compiles there, which is the question nobody at a Linux desk can answer by hand.
  • bundle folds the workspace into the published crate (cargo xtask bundle), builds and packages it, then makes a game with it and plays it headless.
  • book checks the book’s relative links and builds it; licences runs cargo deny.

Every job must pass; none is allowed to fail. Each Rust job starts with .github/actions/setup, so they all build with the same toolchain and libraries.

Tool crates are compiled at opt-level = 2 in the dev profile (Cargo.toml), because a debug build of Cleave/Umbra/Radiance is brutally slow and an unoptimised Chisel 3D pane is the difference between an editor you can drag around and one you cannot.

Back to Architecture notes.