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).

LayerCostCoversIn CI
ctestseconds once builtrules, data model, registries, editor logic, shipped worlds and demosyes
Python world checkerssecondsreachability, campaign structure, water, creature and lift timingno
Golden frames and traceabout a minute, needs a displayrendering and movement did not changeno
Autoplay playthroughsminutes per mission; about two hours for the campaignevery mission can still be finishedno
Live QA scriptsmanual review of screenshotswhat it looks like, menus and editor flowsno

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.

AreaTestsWhat they check
Movement and rulesVerifyBlupiMovement, VerifyInteractionSystemBlupiController 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 dataVerifyShippedWorlds, VerifyBlockDefinitionsCsv, VerifyDefinitionRegistries, VerifyMoveObjectTypesCna, VerifyBigDecorParsingCnaevery 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 modelsVerifyGetObjIcon, VerifyObjIconAgainstReferenceDoc, VerifyObjectModelTableobject animation frames (a regression lock, and a cross-check against mobile-eggbert-reference/08-animations.md); the avatars3d/objects/ manifests
Rendering supportVerifyTileUvBounds, VerifyTileCoverage, VerifyTileFaces, VerifyTerrainLighting, VerifyTerrainAnimDivisor, VerifyUnverifiedRenderMappingpure functions: atlas rectangles, cut-out versus opaque tiles, directional tile faces, terrain light, animation divisors
3D effectsVerifyEffectParticles, VerifySceneEffects, VerifyUltraparticles stepped by ticks; shadows, lights and occlusion decided without a GPU; Ultra's CPU-side computations
Camera and inputVerifyCameraShake, VerifyChaseCameraYaw, VerifyGEInputPadcamera state machines; on-screen pad and menu hit-testing with synthetic mouse state
EditorVerifyGEWorldEditor, VerifyGEEditorPalette, VerifyGEEditorStorage, VerifyGEEditorObjects, VerifyGEEditorDevicesplacement, undo, palette, slots and backups, objects, gamepad and touch controls; run in the build tree's verify-run/
Harnesses and savesVerifyAutoplay, VerifyGESaveData, VerifyDemoReplay, VerifyDemoFiles, VerifyDemoReplaysthe 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

  1. Write tools/VerifyYourThing.cpp in the check(cond, "what") pattern of tools/VerifyCameraShake.cpp. Keep it deterministic, without audio or windows.
  2. In CMakeLists.txt, add an add_executable with the sources it needs (plus ${GE_SHARED_SOURCES} for the world model), the include directories include and src, and an add_test inside if(BUILD_TESTING), with WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} when it opens repository paths. Tests that touch CNA types copy the link pattern of VerifyInteractionSystem; editor tests use ge_add_editor_verify.
  3. For a world-model unit test, add a TEST under tests/GalaxyEggbert/ and list the file in GalaxyEggbertWorldsTests.

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.

Re-baselining Only after an intended change of look or physics, never to silence a failure. Compare the new images with the old ones (and read the trace diff the script prints), then commit the new references on their own with the reason. 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).

CommandMeaning
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 DEGtype 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, mortalrecord a pass or failure; from mortal on a death is traced, not a failure
log TEXT, trace N, shots N, end, include FILEtrace 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...).

Python world checkers

Run from the repository root; all are read-only.

ToolChecks
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.pythe 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.pyevery 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/*.vwra 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 changedRun at least
a world generatorthe 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 effectsctest, golden frames (all three levels), a live look at the affected worlds (world998 shows every block and object)
the editorthe editor verifiers, the relevant tools/qa_steps/editor_*.txt
the world or record formatctest, and update tools/vwr_inspect.py and tools/worldkit.py with it; every shipped world must still load
a block's class in BlockDefinitionRegistryrebuild, refresh tools/data/blocks.csv (tutorial), then the world checks