diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 08:17:38 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | 6f7c79650857da578b9e6f8c49c772eb7c40bcae (patch) | |
| tree | d5e9dcfe134f67d497dfbb4b31a64811b1f6d345 /test/gui_golden.py | |
| parent | ab668719385c94df3e274f71cb744f5917c1da7e (diff) | |
| download | pardes-6f7c79650857da578b9e6f8c49c772eb7c40bcae.tar.gz pardes-6f7c79650857da578b9e6f8c49c772eb7c40bcae.zip | |
GUI goldens settle on the window's idle, not two captures that agree
test/gui_golden.py hashed a scene once two captures 400 ms apart agreed;
on a loaded machine a slow frame agreed with itself before the scene's
change landed and every later hash moved up a scene. A test window now
writes a status file into its capture directory each loop step (gui.zig
writeTestStatus: step, frames captured, idle — nothing owes a frame,
nothing moves, nothing waits on the core's clock), and the harness waits
for an idle step two past the one current when it asks: a 9P write has
been served by then, so the frame is the scene's. Two things kept a
window from ever going idle, both fixed:
- Look-hover re-armed forever: resting on a word with nothing to open, the
hover wait ran its two frames, found nothing, cancelled, and the next
present's pointer refresh started it again: a frame every other frame
for as long as the pointer rested there, live too (CPU). The place last
refused is remembered (Pardes.look_hover_refused) until the pointer
moves off it.
- The virtual clock moved only on a wait that timed out, so a steady
trickle of events held it still; it now moves to the core's wake too
when an event cut the wait short and nothing owes a frame.
Captures go to a directory of their own, outside the session's (a file
written there every frame is a change in the directory its panes look
at). All 17 goldens match the file as recorded at oooopqyv; eight runs,
five alongside a ReleaseFast build and three with all 16 cores saturated
(load 13-19), were byte-identical, 14 s a run unloaded (was ~30).
Tests: the resting pointer lets the core go idle.
Diffstat (limited to 'test/gui_golden.py')
| -rw-r--r-- | test/gui_golden.py | 61 |
1 files changed, 39 insertions, 22 deletions
diff --git a/test/gui_golden.py b/test/gui_golden.py index 99e8c636..24fc5986 100644 --- a/test/gui_golden.py +++ b/test/gui_golden.py @@ -9,7 +9,8 @@ The window is hidden (PARDES_TEST renders into an offscreen capture texture), and the capture carries PARDES_TEST_PAD leftover pixels so edge bugs show. GPU rasterisation differs between machines and drivers: the goldens in test/gui-goldens.txt are a LOCAL gate for one machine, never a CI one. Each -scene waits until two captures 400 ms apart agree before it is hashed. +scene waits until the window says it is idle (its status file) before it is +hashed, so a loaded machine gives the same frames. """ import argparse import hashlib @@ -60,24 +61,34 @@ def panes(client): return {serial: client.read(f'/pane/{serial}/tag').decode(errors='replace') for serial in serials} +def status(root): + """The window's (step, frames captured, idle), or None before the first.""" + try: + step, frames, idle = (root / 'status').read_text().split()[:3] + return int(step), int(frames), idle == '1' + except (FileNotFoundError, ValueError): + return None + + def settled(root): - """The capture once it stops changing: two reads 400 ms apart agree.""" - latest = root / 'latest.ppm' - previous = None - deadline = time.monotonic() + 20 + """The capture once the window says it is idle: a loop step past the one + current when this was asked, with nothing owing a frame (the window's + status file, gui.zig writeTestStatus). Every command before this call has + been served by then (a 9P write returns once the core has it), so the + frame is the scene's however loaded the machine is; the digest of it.""" + deadline = time.monotonic() + 60 + start = None while time.monotonic() < deadline: - time.sleep(.4) - try: - data = latest.read_bytes() - except FileNotFoundError: - continue - if not data.startswith(b'P6'): - continue - digest = hashlib.sha256(data).hexdigest() - if digest == previous: - return digest - previous = digest - raise RuntimeError('capture never settled') + now = status(root) + if now is not None: + if start is None: + start = now[0] + elif now[0] >= start + 2 and now[2] and now[1] > 0: + data = (root / 'latest.ppm').read_bytes() + if data.startswith(b'P6'): + return hashlib.sha256(data).hexdigest() + time.sleep(.02) + raise RuntimeError('the window never went idle') def run(args): @@ -86,6 +97,12 @@ def run(args): root = Path(tempfile.gettempdir()) / 'pardes-gui-golden' shutil.rmtree(root, ignore_errors=True) root.mkdir(mode=0o700) + # Captures go outside the session's own directory: a file written there + # every frame is a change in the directory its panes look at, which + # would ask for another frame, and so on for ever. + captures = Path(tempfile.gettempdir()) / 'pardes-gui-golden-captures' + shutil.rmtree(captures, ignore_errors=True) + captures.mkdir(mode=0o700) name = 'gui-golden' config = root / 'config/pardes' config.mkdir(parents=True) @@ -109,7 +126,7 @@ def run(args): # The virtual clock: a transition's frames are the same frames on any # machine, so one partway through can be hashed too. inherited = {'PARDES_TEST': '1', 'PARDES_TEST_CLOCK': '1', 'PARDES_TEST_COLS': '120', 'PARDES_TEST_ROWS': '40', - 'PARDES_TEST_PAD': '7', 'PARDES_TEST_CAPTURE_DIR': str(root), + 'PARDES_TEST_PAD': '7', 'PARDES_TEST_CAPTURE_DIR': str(captures), 'SDL_VIDEODRIVER': 'wayland', 'WAYLAND_DISPLAY': str(display)} launch = ['-c', 'stty cols 120 rows 40; exec "$@"', name, str(args.binary.resolve()), '--9p=' + name, str(source)] @@ -120,11 +137,11 @@ def run(args): if 'shapes.zig' in tag), None)), 'source pane missing') def scene(label, frame='latest.ppm'): - settled(root) - results.append((label, hashlib.sha256((root / frame).read_bytes()).hexdigest())) + settled(captures) + results.append((label, hashlib.sha256((captures / frame).read_bytes()).hexdigest())) if args.keep: args.keep.mkdir(parents=True, exist_ok=True) - shutil.copyfile(root / frame, args.keep / (label + '.ppm')) + shutil.copyfile(captures / frame, args.keep / (label + '.ppm')) print(label, results[-1][1][:16], flush=True) def mouse(kind, button, x, y): @@ -201,7 +218,7 @@ def run(args): # chrome slides with each pane. client.write('/ctl', b'PanelSlide\n') client.write(f'/pane/{first}/exec', b'Newcol\n') - require(wait_for(lambda: (root / 'transition-6.ppm').exists()), 'transition frame 6 never drawn') + require(wait_for(lambda: (captures / 'transition-6.ppm').exists()), 'transition frame 6 never drawn') scene('18-mid-transition', 'transition-6.ppm') lines = ''.join(f'{label} {digest}\n' for label, digest in results) if args.update: |
