summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 08:17:38 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit6f7c79650857da578b9e6f8c49c772eb7c40bcae (patch)
treed5e9dcfe134f67d497dfbb4b31a64811b1f6d345 /test
parentab668719385c94df3e274f71cb744f5917c1da7e (diff)
downloadpardes-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')
-rw-r--r--test/gui_golden.py61
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: