Testing & QA
Five layers of checks, from two seconds of ctest to a two-hour campaign playthrough, and which of them to run for which change (as of 2026-11-10).
| Layer | Cost | Covers | In CI |
|---|---|---|---|
| ctest | seconds once built | rules, data model, registries, editor logic, shipped worlds and demos | yes |
| Python world checkers | seconds | reachability, campaign structure, water, creature and lift timing | no |
| Golden frames and trace | about a minute, needs a display | rendering and movement did not change | no |
| Autoplay playthroughs | minutes per mission; about two hours for the campaign | every mission can still be finished | no |
| Live QA scripts | manual review of screenshots | what it looks like, menus and editor flows | no |
Compiling is not evidence of visual parity: look at the result.
ctest
ctest --test-dir build-cna-opengl3 -N # list the tests
ctest --test-dir build-cna-opengl3 -j4 --output-on-failure
ctest --test-dir build-cna-opengl3 -R VerifyShippedWorlds # one test
CMakeLists.txt registers 32 tool tests (23 with add_test, nine through
ge_add_editor_verify) and the GoogleTest target GalaxyEggbertWorldsTests, whose 83
TEST cases gtest_discover_tests turns into one ctest entry each. The easy-gl
dependency adds five tests of its own. A native build directory listed 120 tests on 2026-11-10;
CURRENT.md's last full verification line records 117/117 on 2026-11-09, before the latest
additions. VerifyDemoReplays is registered only when CMake finds xvfb-run.
GoogleTest: GalaxyEggbertWorldsTests
Sources in tests/GalaxyEggbert/: Block, BitPacking,
Chunk, World (serialization round trips, rejection of bad files),
BlockMetadata, MoveObjectRecord, BigDecorRecord,
HintZoneRecord, HubDoors. They link only the engine-independent model, no CNA.
Tool tests (tools/Verify*.cpp)
Plain main() programs that print PASS:/FAIL: lines and return
non-zero on failure.
| Area | Tests | What they check |
|---|---|---|
| Movement and rules | VerifyBlupiMovement, VerifyInteractionSystem | BlupiController and InteractionSystem against world999: walking, jumping, collision, swimming, vehicles, lifts, crates against the original's timing tables, pickups, hazards, dynamite. Run from the repository root |
| Worlds and data | VerifyShippedWorlds, VerifyBlockDefinitionsCsv, VerifyDefinitionRegistries, VerifyMoveObjectTypesCna, VerifyBigDecorParsingCna | every worlds3d/*.vwr loads, carries its file's mission number and survives load-save-load; tools/data/blocks.csv equals the C++ registry (DumpBlockDefinitions --check); the block and object registries; the last two parse the original's world files and need the sibling mobile-eggbert |
| Sprites and models | VerifyGetObjIcon, VerifyObjIconAgainstReferenceDoc, VerifyObjectModelTable | object animation frames (a regression lock, and a cross-check against mobile-eggbert-reference/08-animations.md); the avatars3d/objects/ manifests |
| Rendering support | VerifyTileUvBounds, VerifyTileCoverage, VerifyTileFaces, VerifyTerrainLighting, VerifyTerrainAnimDivisor, VerifyUnverifiedRenderMapping | pure functions: atlas rectangles, cut-out versus opaque tiles, directional tile faces, terrain light, animation divisors |
| 3D effects | VerifyEffectParticles, VerifySceneEffects, VerifyUltra | particles stepped by ticks; shadows, lights and occlusion decided without a GPU; Ultra's CPU-side computations |
| Camera and input | VerifyCameraShake, VerifyChaseCameraYaw, VerifyGEInputPad | camera state machines; on-screen pad and menu hit-testing with synthetic mouse state |
| Editor | VerifyGEWorldEditor, VerifyGEEditorPalette, VerifyGEEditorStorage, VerifyGEEditorObjects, VerifyGEEditorDevices | placement, undo, palette, slots and backups, objects, gamepad and touch controls; run in the build tree's verify-run/ |
| Harnesses and saves | VerifyAutoplay, VerifyGESaveData, VerifyDemoReplay, VerifyDemoFiles, VerifyDemoReplays | the autoplay parser; the save round trip; the demo format; every shipped demo parses and matches its world; the shipped demos replayed by the game (below) |
Adding a test
- Write
tools/VerifyYourThing.cppin thecheck(cond, "what")pattern oftools/VerifyCameraShake.cpp. Keep it deterministic, without audio or windows. - In
CMakeLists.txt, add anadd_executablewith the sources it needs (plus${GE_SHARED_SOURCES}for the world model), the include directoriesincludeandsrc, and anadd_testinsideif(BUILD_TESTING), withWORKING_DIRECTORY ${CMAKE_SOURCE_DIR}when it opens repository paths. Tests that touch CNA types copy the link pattern ofVerifyInteractionSystem; editor tests usege_add_editor_verify. - For a world-model unit test, add a
TESTundertests/GalaxyEggbert/and list the file inGalaxyEggbertWorldsTests.
Golden frames and trace
tests/golden/ holds three screenshots of the showcase world (golden_frame_0060.png,
_0120, _0180) and a movement trace (golden_trace.txt);
tests/golden/effects-medium/ and effects-high/ hold the same three frames with the
3D effects at those levels. The comparison is byte for byte.
xvfb-run -a tools/verify_golden_frames.sh build-cna-opengl3
xvfb-run -a tools/verify_golden_trace.sh build-cna-opengl3
verify_golden_frames.sh runs the game with --golden-capture three times
(GALAXY_EFFECTS=0, medium, high) and compares each set with its
references. verify_golden_trace.sh runs --golden-capture-trace, which also walks and
jumps Blupi through a fixed script and records position, vertical speed, ground state and animation per
tick. Both need a real or virtual display; neither is in ctest or CI.
GOLDEN_UPDATE=1 makes
verify_golden_frames.sh write the effects-level references; the classic ones are only replaced
by hand. Whether the files are byte-identical on other GPU drivers is not verified.Autoplay playthroughs
A script plays a mission tick for tick against the real game (--mission N --autoplay
<script>) and checks conditions; see Autoplay.
tools/autoplay.sh build-cna-opengl3 tools/autoplay/world012.txt 12 # one mission
tools/autoplay_all.sh build-cna-opengl3 # every worldNNN.txt
tools/autoplay_all.sh build-cna-opengl3 world011 world121 # chosen ones
tools/play_pass.sh build-cna-opengl3 <out-dir> # all, with contact sheets
autoplay.sh runs the game under its own Xvfb display from a run directory (default
build-probe/autoplay-run/, or AUTOPLAY_RUN=<dir>) whose asset directories link
to the repository's, with a fresh save and GALAXY_USER_DIR set to the run directory. It prints
autoplay_trace.txt and exits with the game's code: 0 when the script passed. Silent
(SDL_AUDIODRIVER=dummy) unless QA_SOUND=1; AUTOPLAY_WORLDS=<dir>
substitutes worlds3d/; AUTOPLAY_REALTIME=1 paces at 60 Hz instead of flat out;
AUTOPLAY_SHOTS=N saves a frame every N ticks. Extra game arguments follow the mission number.
autoplay_all.sh skips a script whose first line says BLOCKED.
tools/autoplay/ has one script per campaign world (world001.txt to
world199.txt); a theme hub's script (world010.txt...) includes its
missions and plays the hub as a chain to its golden key. campaign.txt (written by
python3 tools/autoplay_campaign.py) plays a new game through all twelve worlds and world 199.
probes/ holds one-off scripts, spare/ alternate routes.
The script language
The authoritative text is the header comment of src/GalaxyEggbert/Game/Autoplay.hpp. One
command per line, # comments, raw grid coordinates (a cell's centre is its integer value), yaw
in degrees (0 faces -Z, 90 faces +X).
| Command | Meaning |
|---|---|
wait N, hold N KEY... | no keys / hold keys (up down left right jump action crouch lookup) for N ticks of 1/60 s |
type WORD, face DEG | type letters (cheat words); turn on the spot |
go X Z [jump D] [tol T] [max N] [until COND] [with KEY]... | walk towards a point, optionally jumping once within D of it |
fly X Y Z ... | a go that also holds a height (helicopter, overcraft) |
until COND [max N], lift X Y Z, crate X Y Z [tol T] | wait for a condition, a lift platform or a crate at a place |
clear X Y Z R [lane x|z], near ... | wait until no enemy / an enemy is within R: time a pass by a patrol |
expect COND, mortal | record a pass or failure; from mortal on a death is traced, not a failure |
log TEXT, trace N, shots N, end, include FILE | trace and flow control |
A condition is FIELD OP VALUE with = != < <= > >=. Fields: x y z yaw
tick mtick mission treasures eggs lives keys dynamite vehicle ground dead ghost anim exit blitz temp switches
saws crush phase (vehicle is accepted by the parser but missing from the header comment).
Use mtick (ticks since the current mission started), never tick, in anything that
can run inside a chain. A command that runs out of ticks, or Blupi dying before mortal, fails the
run. tools/autoplay_search.py tries a wait {D} over a range and
tools/autoplay_chains.py re-times synced chains after a timing change.
Last recorded results: the handbook records a full pass of all 78 campaign scripts on 2026-11-07
(CURRENT.md still names 2026-11-06 as its baseline); the whole-campaign run
(campaign.txt) was last recorded on 2026-10-27.
Demo verification
The main-menu demos (Content/demos/demoNNN.gdemo, see Demos &
Recordings) are recorded input, so a simulation change can make them replay differently.
VerifyDemoFiles (VerifyDemoReplay --shipped) checks that every shipped demo parses
with the current simulation revision and names its world's current bytes; tools/verify_demos.sh
<build-dir> (ctest VerifyDemoReplays) replays each one in the game under Xvfb with
--verify-demo, compares its end state, and checks that the stand-in player files were left
untouched. When a demo reports "different", bump kDemoSimulationRevision
(DemoReplay.hpp) and record the demos again with tools/record_demo.sh.
Live QA
tools/qa_live.sh <build-dir> <out-dir> <steps-file> [game args] runs the game
under its own Xvfb display (needs xvfb-run, xdotool and ImageMagick's
import) and executes a step file: sleep S, shot NAME, key
KEYSYM..., keydown/keyup, click X Y,
mousedown/mouseup, move X Y, focus, type WORD,
resize W H. QA_LIVE_RUN, QA_LIVE_AVATARS, QA_LIVE_WORLDS
and QA_LIVE_SCREEN choose the run directory, substitute asset directories (to look at a
re-exported model or a probe world before it replaces the committed one) and size the display. Editor step
files live in tools/qa_steps/ (editor_playtest.txt,
editor_copy_paste.txt, editor_lift.txt, editor_backups.txt...).
tools/qa_sweep.sh <build-dir> [out-dir]loads every shipped world, walks two seconds, takes a screenshot and writes one line per world toresult.txt.tools/qa_original.sh <port-dir> <out-dir> <steps>drives a copy of the original game's Linux build with the same steps, to see what the original does where its source leaves it open.python3 tools/verify_lava_depth.py <build-dir>is a real-render regression (BD-0114: lava drawn over a translucent ghost Blupi).
Python world checkers
Run from the repository root; all are read-only.
| Tool | Checks |
|---|---|
tools/levelcheck.py worlds3d/worldNNN.vwr [--rides] | conservative reachability from the spawn to every treasure, key, egg, exit and (in a hub) sign: walking, falling, jumps, lifts, swimming, teleporters; also traps. Crates, dynamite, vehicles and timing are not modelled, so check a generator's "solved" variant for those |
tools/flightcheck.py ... [--mine-margin 0] | the same plus the helicopter |
tools/campaigncheck.py | the campaign's structure: twelve world signs, each hub's mission signs (4, 5, 4, 6, 8, 6, 5, 4, 5, 7, 5, 5), a spawn and one way out per mission |
tools/watercheck.py [worlds...] | every body of water has a way out for a swimming Blupi |
tools/audit/patrol_envelope.py, tools/audit/lift_envelope.py | every creature uses its type's original speed and rest; no lift is faster than the original's fastest |
tools/vwr_inspect.py [--summary] [--objects 7,21] worlds3d/*.vwr | a dump of header, blocks per type and height, objects, BigDecor and hint zones |
CI
.github/workflows/cna-ci.yml runs one job, "Build + ctest" on ubuntu-latest: it
checks out this repository and the siblings (cna, cna-engine, easy-gl, sharp-runtime, meta-gl) at pinned
commits, plus a sparse checkout of mobile-eggbert's worlds/ as test data, installs the
system packages, configures with -DGALAXY_EGGBERT_BUILD_CNA=ON and ccache, builds, and runs
ctest --output-on-failure. Every registered test runs. Golden files, playthroughs, live QA and
the web build are local, manual steps. .github/workflows/pages.yml deploys this website.
Which checks for which change
| You changed | Run at least |
|---|---|
| a world generator | the generator, levelcheck.py (and a solved variant), campaigncheck.py, watercheck.py, patrol_envelope.py, lift_envelope.py, VerifyShippedWorlds, the world's autoplay script, a live look |
gameplay (BlupiController, InteractionSystem, registries) | ctest, golden trace, the affected missions' playthroughs (ideally all), verify_demos.sh |
| rendering or the 3D effects | ctest, golden frames (all three levels), a live look at the affected worlds (world998 shows every block and object) |
| the editor | the editor verifiers, the relevant tools/qa_steps/editor_*.txt |
| the world or record format | ctest, and update tools/vwr_inspect.py and tools/worldkit.py with it; every shipped world must still load |
a block's class in BlockDefinitionRegistry | rebuild, refresh tools/data/blocks.csv (tutorial), then the world checks |