JNEXT
Skip to content

4.1 The test triplet

Every change that lands on main has to clear three layers of testing. The project calls them the triplet, and running them takes four commands:

$ make clean && make gui-release
$ make unit-test
$ ./build/test/fuse_z80_test build/test/fuse
$ JNEXT_TEST_JOBS=4 bash test/00regression/regression.sh

There must be no FAIL in any layer. Skips are acceptable only where the suite already declares them.

Rebuild first — always

The build at the top is not a courtesy, and it is not left to your good habits either. make regression declares unit-test-build, gui-release and sdl-release as real prerequisites, because the suite runs those exact binaries. Most rows run build/gui-release/jnext; the four rows that exercise the SDL-only frontend need build/sdl-release/jnext instead, because SdlApp is instantiated only in a build configured with ENABLE_QT_UI=OFF; and the rewind-func row runs build/test/rewind_test, which make clean deletes. Building all of them from the target is what makes "the tests ran against this source" true by construction rather than by discipline.

The rule was written after a 14-hour-old binary produced two bogus FAILs immediately before a version bump. Staleness cuts the other way too, and that direction is worse: an old binary can equally fail to contain the bug a test would have caught, and then it reports a pass it never earned.

In a fresh agent worktree, run make worktree-bootstrap before anything else. roms/* is git-ignored, so a new checkout has no SD-card image and cannot run the suites at all.

Layer 1 — the unit suites

make unit-test does not go looking for test binaries. It runs exactly the suites declared in test/unit-tests.conf, in parallel, each one bounded by a 300 s timeout. That manifest currently declares 90 suites totalling 6610 rows: the suite count is pinned in the file's own # expect: 90 line, and the row total is the sum of the per-suite counts in that same file. The committed dashboard, test/SUBSYSTEM-TESTS-STATUS.md, reports the same 6610. 4.2 explains what those pins are for.

These suites link the emulator libraries directly and drive the classes through their public API, which makes them fast and hermetic — sd_rom_extractor_test is the only one that reads the SD image. This is also where VHDL compliance is asserted, row by row. What they cannot catch is anything that only exists once the whole machine is assembled: they render no frame, open no window, and drive neither frontend.

make unit-test also pulls in the cheap structural gates ahead of the build — the tautological-assertion lint, the Makefile help lint, the traceability accounting, self-test and duplicate-ID checks, the documentation checks, and the hermetic half of the packaging contract tests. They live there rather than only in CI for a simple reason: a gate the inner loop never reaches is not a gate.

Layer 2 — the FUSE Z80 opcode suite

$ ./build/test/fuse_z80_test build/test/fuse

All 1356 rows pass. This is the FUSE project's data-driven opcode corpus, which covers every instruction, including undocumented behaviour and per-instruction cycle counts. It is the only thing that proves the CPU core itself is right — and it proves nothing else. The Z80N extensions are covered by z80n_test (85 rows), and everything Next-specific lives elsewhere.

Note that fuse_z80_test is also a declared row of test/unit-tests.conf, pinned at 1356 with its data directory passed as an argument, so make unit-test has already run it. The standalone invocation above runs the same binary against the same data; it is a convenience for reading that one number, not a third independent execution.

Layer 3 — the screenshot and functional regression

$ JNEXT_TEST_JOBS=4 bash test/00regression/regression.sh

This layer launches the real jnext binary headless and looks at what comes out of it. It reports 119 rows, made up of 2 preflight lints, 1 SD-image provisioning row, 65 screenshot rows and 51 functional rows. The last two numbers come from the # expect: 65 and # expect: 51 pins in regression_tests.conf and functional_tests.conf, and the arithmetic that ties them to the total is the harness's own end-of-run accounting assertion.

A screenshot row compares a whole frame, pixel for pixel, against a committed reference PNG. That is exactly as strong and as blunt as it sounds: it catches any rendering change anywhere in the pipeline, and when it fails it tells you the frame changed without telling you why. Functional rows cover the things a linked library cannot show at all — recording an MP4, an RZX round trip, audio underruns through a real device, the CLI's exit contracts, keypress delivery in both frontends.

JNEXT_TEST_JOBS=4 caps the screenshot launcher's parallelism, and it is worth keeping. The cap is not politeness towards the machine: audio-underrun-func and screenshot-paused-func are bounded by real-time pacing and start failing under CPU contention, so raising it makes the suite intermittently lie. Note also that the cap is a caller convention rather than a default — the launcher falls back to nproc when the variable is unset, so make regression on its own is uncapped. CI sets it explicitly.

What the triplet does not prove

It does not prove that the emulator matches real hardware anywhere no test row asserts that it does; making those gaps visible is the job of the traceability matrix (4.4). And it does not prove that the prose in the man page or in the guides is true — only that the generated outputs match their sources. 4.5 sets out exactly where that line sits.