OXYGENHUB

Client and Testing

Andy McCall edited this page · 2 revisions

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:

LevelMethods
0.1status(), frame(), regs(), mem(addr, len, bank=None), step(frames=1), pause(), resume(), screenshot(), screenshot_ppm(), wait_ready(), run_to_frame(target)
0.2key(text=… or code=…, down=None), reset()
0.3mem_write(addr, data, bank=None), audio()
0.4pointer(x, y or dx, dy, buttons), pointer_get()
0.5pad(buttons, index=0, connected=None), pad_get(index=0)
0.6capabilities(), 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.