diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-11 12:38:56 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 682e237df7e8b22f820d14a4adee58e6e2f84268 (patch) | |
| tree | 5d888cb46f1a49e89fc55e2a4c232d3af0265b1d /test/ui_review.py | |
| parent | 6878e1c309172d624c0d7a3555f6f0217ae6770e (diff) | |
| download | pardes-682e237df7e8b22f820d14a4adee58e6e2f84268.tar.gz pardes-682e237df7e8b22f820d14a4adee58e6e2f84268.zip | |
trunk: resume before the Reload experiment
Empty marker on the last pre-Reload change. Keep the Reload experiment on reload (3801914), its first change on reload-start (200a1fc), and the unfinished performance investigation on reload-perf-wip.
Diffstat (limited to 'test/ui_review.py')
| -rw-r--r-- | test/ui_review.py | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/test/ui_review.py b/test/ui_review.py new file mode 100644 index 00000000..df492eac --- /dev/null +++ b/test/ui_review.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""Capture a read-only Agave review in an isolated SDL or TTY session. + +python3 test/ui_review.py zig-out/bin/pardes-gui /tmp/pardes-review \\ + --project /path/to/agave --theme orchard + +Requires rust-analyzer for LSP, an X11 display and ImageMagick for SDL PNGs. +The project files are never written. Cargo build output stays in --cargo-target +(or OUTPUT/cargo-target). Use --skip-lsp when reviewing additional themes. +TTY captures are the actual core grid and styles, not emulator screenshots. +""" +import argparse +import json +import os +from pathlib import Path +import shutil +import subprocess +import time + +from fs import session + +def panes(client): + return {int(row.split(maxsplit=5)[0]): row.split(maxsplit=5)[5] + for row in client.read('/self/index').decode().splitlines()} + +def wait_for(fn, seconds=12): + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + result = fn() + if result: + return result + time.sleep(.1) + return None + +def command(client, serial, text): + """Execute a temporary tag command without changing source contents.""" + tag = f'/self/pane/{serial}/tag' + before = client.read(tag).rstrip(b'\n') + tail_start = before.index(b'Save Tty Del') + while tail_start and before[tail_start - 1:tail_start] == b' ': + tail_start -= 1 + client.write(tag, b' ' + text.encode()) + current = client.read(tag) + start = current.rfind(text.encode()) + client.write(f'/self/pane/{serial}/event', f'Mx{start} {start + len(text.encode())}\n'.encode()) + client.write(f'/self/pane/{serial}/ctl', b'cleartag\n') + client.write(tag, before[tail_start:]) + +def new_output(client, before): + return next((serial for serial in panes(client) if serial not in before and + client.read(f'/self/pane/{serial}/body').strip()), None) + +def require(value, description): + if not value: + raise AssertionError(description) + return value + +def capture(client, root, label, gui): + time.sleep(.4) + screen = client.screen() + (root / (label + '.json')).write_text(json.dumps(screen)) + cols = screen['cols'] + text = '\n'.join(''.join(cell[0] for cell in screen['cells'][at:at+cols]) + for at in range(0, len(screen['cells']), cols)) + (root / (label + '.txt')).write_text(text) + (root / (label + '-index.txt')).write_text(client.read('/self/index').decode()) + if gui: + require(wait_for(lambda: (root / 'latest.ppm').exists(), 8), 'GPU capture missing') + def convert_frame(): + shutil.copyfile(root / 'latest.ppm', root / (label + '.ppm')) + return subprocess.run(['magick', str(root / (label + '.ppm')), str(root / (label + '.png'))], + stderr=subprocess.DEVNULL).returncode == 0 + if not wait_for(convert_frame, 8): + raise RuntimeError('GPU capture did not contain a complete image') + print(label, flush=True) + +def run(args): + project = args.project.resolve() + source_path = project / 'gossip/src/crds_value.rs' + original = source_path.read_bytes() + root = args.output.resolve() + root.mkdir(parents=True, exist_ok=True, mode=0o700) + root.chmod(0o700) + report = {'find': False, 'grep': False, 'terminal': False, 'source_unchanged': False, + 'lsp': 'skipped' if args.skip_lsp else 'requested'} + config = root / 'config/pardes' + config.mkdir(parents=True, exist_ok=True) + (config / 'init').write_text('Shell /bin/sh\n' + (f'Theme {args.theme}\n' if args.theme else '')) + name = 'ui-review-' + str(os.getpid()) + rust = (str(args.rust_analyzer.resolve()) if args.rust_analyzer else + subprocess.check_output(['rustup', 'which', 'rust-analyzer'], text=True).strip()) if not args.skip_lsp else '' + launch = ['PARDES_LSP_RS=' + rust, 'CARGO_HOME=' + os.path.expanduser('~/.cargo'), + 'RUSTUP_HOME=' + os.path.expanduser('~/.rustup'), + 'CARGO_TARGET_DIR=' + str((args.cargo_target or root / 'cargo-target').resolve()), 'CARGO_NET_OFFLINE=true', + str(args.binary.resolve()), '--9p=' + name, str(source_path)] + inherited = {'PARDES_TEST': '1', 'PARDES_TEST_CAPTURE_DIR': str(root), + 'PARDES_TEST_COLS': '150', 'PARDES_TEST_ROWS': '48', + 'SDL_VIDEODRIVER': 'x11'} if not args.tty else {} + launch = ['-c', 'stty cols 150 rows 48; exec "$@"', 'ui-review', '/usr/bin/env', *launch] + terminal_handle = {} + with session('/bin/sh', root, name, socket_name=name, tty=True, + launch=launch, inherited=inherited, terminal=terminal_handle) as (client, address): + print('socket', address, flush=True) + capture(client, root, '01-source', not args.tty) + source = next(serial for serial, tag in panes(client).items() if 'crds_value.rs' in tag) + before = panes(client) + command(client, source, 'Find crds_value') + find_pane = require(wait_for(lambda: new_output(client, before)), 'Find did not return results') + require(b'crds_value.rs' in client.read(f'/self/pane/{find_pane}/body'), 'Find result missing source') + report['find'] = True + capture(client, root, '02-find', not args.tty) + if find_pane: + (root / 'find-results.txt').write_bytes(client.read(f'/self/pane/{find_pane}/body')) + client.write(f'/self/pane/{find_pane}/ctl', b'del\n') + before = panes(client) + command(client, source, 'Grep CrdsValueLabel') + grep_pane = require(wait_for(lambda: new_output(client, before)), 'Grep did not return results') + require(b'CrdsValueLabel' in client.read(f'/self/pane/{grep_pane}/body'), 'Grep result missing match') + report['grep'] = True + if grep_pane: + (root / 'grep-results.txt').write_bytes(client.read(f'/self/pane/{grep_pane}/body')) + capture(client, root, '03-grep', not args.tty) + command(client, source, 'Tty') + terminal = require(wait_for(lambda: next((serial for serial in panes(client) + if 'pty' in client.list(f'/self/pane/{serial}')), None)), + 'Tty did not create a terminal') + if terminal: + client.write(f'/self/pane/{terminal}/pty/data', b"printf '\\033[1;36mAgave workspace\\033[0m\\n'; pwd; printf '\\033[32mRead-only UI review ready\\033[0m\\n'\r") + require(wait_for(lambda: b'\nRead-only UI review ready\n' in + client.read(f'/self/pane/{terminal}/body').replace(b'\r', b'')), + 'Terminal command output was not observed') + report['terminal'] = True + capture(client, root, '04-terminal', not args.tty) + if not args.skip_lsp: + before = panes(client) + command(client, source, 'Symbols') + symbols = wait_for(lambda: new_output(client, before), args.lsp_wait) + report['symbols'] = 'returned results' if symbols else 'no output before deadline' + if symbols: + (root / 'symbols-results.txt').write_bytes(client.read(f'/self/pane/{symbols}/body')) + capture(client, root, '05-symbols', not args.tty) + if symbols: + client.write(f'/self/pane/{symbols}/ctl', b'del\n') + offset = original.index(b'CrdsValue {') + client.write(f'/self/pane/{source}/addr', f'#{offset},#{offset}'.encode()) + client.write(f'/self/pane/{source}/ctl', b'dot=addr\nshow\n') + before = panes(client) + command(client, source, 'Hover') + hover = wait_for(lambda: new_output(client, before), args.lsp_wait) + report['hover'] = 'returned results' if hover else 'no output before deadline' + if hover: + (root / 'hover-results.txt').write_bytes(client.read(f'/self/pane/{hover}/body')) + capture(client, root, '06-hover', not args.tty) + # Look focuses the source without changing it; gd then travels through + # the host's real input parser, unlike tag builtin dispatch above. + line = original[:original.index(b'impl Sanitize for CrdsValue')].count(b'\n') + 1 + client.write(f'/self/pane/{source}/ctl', f'look {source_path}:{line}:19\n'.encode()) + os.write(terminal_handle['input_fd'], b'gd') + time.sleep(1) + capture(client, root, '07-definition', not args.tty) + command(client, source, 'Lspinfo') + lsp = wait_for(lambda: next((serial for serial, tag in panes(client).items() if '+Lsp' in tag), None)) + if lsp: + info = client.read(f'/self/pane/{lsp}/body') + (root / 'lsp-info.txt').write_bytes(info) + report['lsp'] = info.decode() + capture(client, root, '08-lsp-info', not args.tty) + if lsp: + client.write(f'/self/pane/{lsp}/ctl', b'del\n') + for theme in args.themes: + command(client, source, 'Theme ' + theme) + capture(client, root, 'theme-' + theme, not args.tty) + for serial, tag in panes(client).items(): + if any(word in tag for word in ['+Hover', '+Search']): + (root / f'pane-{serial}.txt').write_bytes(client.read(f'/self/pane/{serial}/body')) + assert client.read(f'/self/pane/{source}/body') == original == source_path.read_bytes() + report['source_unchanged'] = True + if terminal: + client.write(f'/self/pane/{terminal}/pty/data', b'exit\r') + (root / 'report.json').write_text(json.dumps(report, indent=2)) + print('artifacts', root, flush=True) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('binary', type=Path) + parser.add_argument('output', type=Path) + parser.add_argument('--project', required=True, type=Path, help='Agave checkout (read only)') + parser.add_argument('--cargo-target', type=Path, help='reuse an isolated cargo target directory') + parser.add_argument('--rust-analyzer', type=Path, help='server executable; defaults to rustup which') + parser.add_argument('--tty', action='store_true') + parser.add_argument('--theme') + parser.add_argument('--themes', nargs='*', default=[], help='capture each theme after exercising the workspace') + parser.add_argument('--skip-lsp', action='store_true') + parser.add_argument('--lsp-wait', type=float, default=25) + run(parser.parse_args()) |
