Client & Testing
client/emu_control.py is the shared client every port's pytest suite imports.
Stdlib only, plus optional Pillow for .screenshot().
Usage
from emu_control import EmuControl
emu = EmuControl(port=8080) # emulator started with the control-port flag
emu.wait_ready() # poll /status through the boot race
emu.pause()
emu.step(frames=120) # deterministic: exactly 120 frames
assert emu.regs()["pc"] == 0xC000
data = emu.mem(0x0400, 256, bank=1)
assert data[0] == 0x2A
img = emu.screenshot() # PIL.Image (needs Pillow)
Methods, by the level that added them:
| Level | Methods |
|---|---|
| 0.1 | status(), frame(), regs(), mem(addr, len, bank=None), step(frames=1), pause(), resume(), screenshot(), screenshot_ppm(), wait_ready(), run_to_frame(target) |
| 0.2 | key(text=… or code=…, down=None), reset() |
| 0.3 | mem_write(addr, data, bank=None), audio() |
| 0.4 | pointer(x, y or dx, dy, buttons), pointer_get() |
| 0.5 | pad(buttons, index=0, connected=None), pad_get(index=0) |
| 0.6 | capabilities(), perf(windows=, zones=, frames=, since=, **platform), perf_config(**settings) |
status() fails fast on a contract MAJOR mismatch. Gate a test on what the
server offers: if "perf" not in emu.capabilities(): pytest.skip(...).
Writing reliable tests
Prefer state assertions over pixels
Assert game state via /mem (sprite table, actor positions, score) whenever you
can. It's stable and specific; a pixel diff is neither.
Screenshots: smoke and artifacts, not golden frames
Full-frame pixel-exact diffs flake even at a fixed frame — blink, sprite anim phase, fright flash and RNG all move. Two patterns that hold up:
- "Did it render?" — assert a bounding box isn't all background. This catches the no-sprites class of bug that motivated the whole project.
- Failure artifact — save the PPM on failure so a human can eyeball it.
If you truly need golden images, capture on a known frame with blink/anim/RNG pinned by a TESTMODE.
Determinism via pause + step
emu.pause()
a = emu.frame()
b = emu.step(3)
assert b == emu.frame() # halted at a definite frame
assert emu.frame() - a > 0 # advanced
Equal-size steps advance the counter by equal amounts — that's your determinism check. Do state/screenshot reads while paused so nothing tears.
Running the emulator under CI
You need video stepping alive but no real display. On the x16 that's
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy with no headless flag — no
Xvfb required. See Per-Platform Notes.
import os, subprocess
env = dict(os.environ, SDL_VIDEODRIVER="dummy", SDL_AUDIODRIVER="dummy")
proc = subprocess.Popen([EMU, "-rom", ROM, "-warp", "-controlport", "8080"], env=env)
emu = EmuControl(8080); emu.wait_ready()
# ... assertions ...
proc.terminate()
Performance as a test
From 0.6, a frame's cost is a number a test can hold a program to:
emu.perf_config(enabled=True, reset=True)
emu.pause(); emu.step(300)
p = emu.perf(windows=["session"])
assert p["windows"][0]["overruns"] == 0
assert p["windows"][0]["budgetWork"]["p99"] < 0.85 * p["budget"]["budgetCycles"]
See Performance Metrics for the fields, zones and the graphs they make.