summaryrefslogtreecommitdiff
path: root/test/ui_review.py
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-11 12:38:56 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commit682e237df7e8b22f820d14a4adee58e6e2f84268 (patch)
tree5d888cb46f1a49e89fc55e2a4c232d3af0265b1d /test/ui_review.py
parent6878e1c309172d624c0d7a3555f6f0217ae6770e (diff)
downloadpardes-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.py195
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())