diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-20 00:36:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | c1990b3e6e196ad41379aa432bf3ccca8a65d9f0 (patch) | |
| tree | d7ad5afe559f7f56464b14c3122242fa3f16399b /test | |
| parent | 717afaf3177a2e0925b18ae3445119efe808b129 (diff) | |
| download | pardes-c1990b3e6e196ad41379aa432bf3ccca8a65d9f0.tar.gz pardes-c1990b3e6e196ad41379aa432bf3ccca8a65d9f0.zip | |
Flatten the 9P control tree and move it out of fs.zig
The served tree loses the self/ level: /index /ctl /new /log /screen
/listeners /pane/<n>/... /os, with /src only in -Dembed-sources=true builds
(default off, on for esp32p4). ctl speaks the editor's own language with two
lowercase verbs, look TEXT and exec TEXT, plus acme's addr verbs; the new/
factory directory becomes one clone file; cons is gone (exec Msg); name and
sel are files; stats report real lengths, modes and mtimes; /log streams
pane new/del/rename/save events. The tree code lives in src/ninep/
(tree, pane, ctl, addr, pty, events, screen, sources); fs.zig keeps host
access, mounts, resolution and find/grep. Same engine and transports.
README (fs-help.txt) and docs rewritten; tests updated and extended.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'test')
| -rw-r--r-- | test/agent_session.py | 14 | ||||
| -rw-r--r-- | test/appearance_gallery.py | 26 | ||||
| -rw-r--r-- | test/collapse_layout.py | 8 | ||||
| -rw-r--r-- | test/column_tags.py | 32 | ||||
| -rw-r--r-- | test/font_size.py | 2 | ||||
| -rw-r--r-- | test/fs.py | 367 | ||||
| -rw-r--r-- | test/fs_bench.zig | 43 | ||||
| -rw-r--r-- | test/fs_namespace.zig | 12 | ||||
| -rw-r--r-- | test/fs_soak.py | 29 | ||||
| -rw-r--r-- | test/look_placement.py | 8 | ||||
| -rw-r--r-- | test/ninep.py | 39 | ||||
| -rw-r--r-- | test/output.zig | 12 | ||||
| -rw-r--r-- | test/panes.zig | 8 | ||||
| -rw-r--r-- | test/pdf_scroll_bench.zig | 18 | ||||
| -rw-r--r-- | test/snapshot.zig | 2 | ||||
| -rw-r--r-- | test/tag_cleanup.py | 10 | ||||
| -rw-r--r-- | test/ui_review.py | 50 | ||||
| -rw-r--r-- | test/v9fs.py | 33 | ||||
| -rw-r--r-- | test/v9fs_terminal.py | 6 | ||||
| -rw-r--r-- | test/window_opacity.py | 12 |
20 files changed, 430 insertions, 301 deletions
diff --git a/test/agent_session.py b/test/agent_session.py index b920b006..aa87c845 100644 --- a/test/agent_session.py +++ b/test/agent_session.py @@ -52,21 +52,21 @@ def run(args, command, status): client, _ = context.__enter__() try: try: - while not client.read('/self/pane/1/body').strip(): + while not client.read('/pane/1/body').strip(): time.sleep(.01) started = time.monotonic() - client.write('/self/pane/1/pty/data', shlex.join(['/bin/sh', str(script)]).encode() + b'\r') + client.write('/pane/1/pty/data', shlex.join(['/bin/sh', str(script)]).encode() + b'\r') status['phase'] = 'readiness' wait_ready(client, args.ready) if args.prepare_key is not None: status['phase'] = 'preparation' - client.write('/self/pane/1/pty/data', args.prepare_key) + client.write('/pane/1/pty/data', args.prepare_key) wait_ready(client, args.ready) ready_ms = (time.monotonic() - started) * 1000 status.update(phase='history', required_body_rows=args.min_rows) while True: started = time.monotonic() - body = client.read('/self/pane/1/body') + body = client.read('/pane/1/body') body_read_ms = (time.monotonic() - started) * 1000 rows = body.count(b'\n') + int(bool(body) and not body.endswith(b'\n')) status.update(body_bytes=len(body), body_rows=rows) @@ -81,13 +81,13 @@ def run(args, command, status): raise AssertionError('probe is already visible') status['phase'] = 'probe-visible' started = time.monotonic() - client.write('/self/pane/1/pty/data', probe.encode()) + client.write('/pane/1/pty/data', probe.encode()) while probe not in screen_text(client).replace('\n', ''): time.sleep(.01) write_ms = (time.monotonic() - started) * 1000 status['phase'] = 'probe-cleared' started = time.monotonic() - client.write('/self/pane/1/pty/data', b'\x15') + client.write('/pane/1/pty/data', b'\x15') while True: observed = screen_text(client) status['observed_visible_text_sha256'] = hashlib.sha256(observed.encode()).hexdigest() @@ -109,7 +109,7 @@ def run(args, command, status): client.socket.settimeout(.25) for _ in range(2): try: - client.write('/self/pane/1/pty/data', b'\x03') + client.write('/pane/1/pty/data', b'\x03') except (OSError, EOFError): pass time.sleep(.05) diff --git a/test/appearance_gallery.py b/test/appearance_gallery.py index 7779efa9..0fa12136 100644 --- a/test/appearance_gallery.py +++ b/test/appearance_gallery.py @@ -80,21 +80,21 @@ def run(args): before = set(panes(client)) command(client, source, 'Newcol') right = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)), 'right column missing') - client.write(f'/self/pane/{right}/ctl', f'name {root / "Review"}\n'.encode()) - client.write(f'/self/pane/{right}/body', PREFERENCE_NOTES if args.theme_selector else NOTES, truncate=True) - client.write(f'/self/pane/{right}/ctl', b'clean\n') + client.write(f'/pane/{right}/name', f'{root / "Review"}\n'.encode()) + client.write(f'/pane/{right}/body', PREFERENCE_NOTES if args.theme_selector else NOTES, truncate=True) + client.write(f'/pane/{right}/ctl', b'clean\n') before = set(panes(client)) command(client, right, 'Tty') terminal = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)), 'terminal missing') - client.write(f'/self/pane/{terminal}/pty/data', b"PS1='$ '; printf '\\033[2J\\033[H\\033[1;36mPardes workspace\\033[0m\\n\\n'; printf 'Agave source: read-only\\nTerminal: ready\\n\\n'; printf '\\033[32mSmall touches. Same workflow.\\033[0m\\n'\r") - require(wait_for(lambda: b'Small touches. Same workflow.' in client.read(f'/self/pane/{terminal}/body')), 'terminal output missing') + client.write(f'/pane/{terminal}/pty/data', b"PS1='$ '; printf '\\033[2J\\033[H\\033[1;36mPardes workspace\\033[0m\\n\\n'; printf 'Agave source: read-only\\nTerminal: ready\\n\\n'; printf '\\033[32mSmall touches. Same workflow.\\033[0m\\n'\r") + require(wait_for(lambda: b'Small touches. Same workflow.' in client.read(f'/pane/{terminal}/body')), 'terminal output missing') def config_report(): before = set(panes(client)) command(client, right, 'Config') pane = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)), 'Config output missing') - text = client.read(f'/self/pane/{pane}/body').decode() - client.write(f'/self/pane/{pane}/ctl', b'del\n') + text = client.read(f'/pane/{pane}/body').decode() + client.write(f'/pane/{pane}/ctl', b'exec Del\n') return text settings = config_report() @@ -135,7 +135,7 @@ def run(args): command(client, right, 'Theme ' + theme) if args.theme_selector: # Keep both quiet active gutter ink and vivid comments in view. - client.write(f'/self/pane/{source}/ctl', f'look {source_path}:117\n'.encode()) + client.write(f'/pane/{source}/ctl', f'look {source_path}:117\n'.encode()) take_frame('classic-' + theme) if args.theme_selector: frame = client.screen() @@ -162,7 +162,7 @@ def run(args): command(client, right, 'ThemeSel') selector = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)), 'ThemeSel missing') - body = client.read(f'/self/pane/{selector}/body').decode() + body = client.read(f'/pane/{selector}/body').decode() native, legacy = body.split('# Legacy and imported themes', 1) for theme in ('forge_black', 'forge_soft', 'orchard_black'): require('Theme ' + theme + '\n' in native, 'native contrast variant below legacy heading') @@ -171,14 +171,14 @@ def run(args): 'ThemeSel did not open at native first entry') take_frame('theme-selector') report['native_themes_first'] = True - client.write(f'/self/pane/{selector}/ctl', b'del\n') + client.write(f'/pane/{selector}/ctl', b'exec Del\n') command(client, terminal, 'Collapse') for pet in (() if args.skip_pets else ('cat', 'frog', 'off')): command(client, right, 'Pet ' + pet) settings = config_report() require('Pet: ' + pet + '\n' in settings, 'pet setting not applied') # Returning focus to the source closes all transient report state. - client.write(f'/self/pane/{source}/ctl', f'look {source_path}:1\n'.encode()) + client.write(f'/pane/{source}/ctl', f'look {source_path}:1\n'.encode()) take_frame('pet-' + pet) if pet != 'off': frame = client.screen() @@ -194,9 +194,9 @@ def run(args): require(band_digest('pet-' + pet) != band_digest('pet-' + pet + '-later'), 'pet did not animate in workspace tag') report['pets'].append(pet) - require(client.read(f'/self/pane/{source}/body') == original == source_path.read_bytes(), 'Agave source changed') + require(client.read(f'/pane/{source}/body') == original == source_path.read_bytes(), 'Agave source changed') report['source_unchanged'] = True - client.write(f'/self/pane/{terminal}/pty/data', b'exit\r') + client.write(f'/pane/{terminal}/pty/data', b'exit\r') (root / 'report.json').write_text(json.dumps(report, indent=2)) print(json.dumps(report, indent=2)) diff --git a/test/collapse_layout.py b/test/collapse_layout.py index e549bd37..244ee57a 100644 --- a/test/collapse_layout.py +++ b/test/collapse_layout.py @@ -29,7 +29,7 @@ def geometry(client, names): def toggle(client, serial): """Execute the default tag's Collapse word without changing any body or tag.""" - pane = f'/self/pane/{serial}' + pane = f'/pane/{serial}' tag = client.read(pane + '/tag') at = tag.rfind(b' Collapse') require(at >= 0, 'default pane tag lacks Collapse') @@ -57,8 +57,8 @@ def run(args): for index, label in enumerate(names): if index: serials.append(new_pane(client, b'')) - pane = f'/self/pane/{serials[index]}' - client.write(pane + '/ctl', ('name ' + label + '\n').encode()) + pane = f'/pane/{serials[index]}' + client.write(pane + '/name', (label + '\n').encode()) client.write(pane + '/body', contents, truncate=True) original = require(wait_for(lambda: geometry(client, names)), 'four pane tags not visible') require(all(height > 1 for _, height in original), f'fixture has no body room: {original}') @@ -82,7 +82,7 @@ def run(args): require(wait_for(lambda: geometry(client, names) == original), f'expansion did not restore original geometry: {geometry(client, names)}') for serial in serials: - require(client.read(f'/self/pane/{serial}/body') == contents, 'Collapse changed pane contents') + require(client.read(f'/pane/{serial}/body') == contents, 'Collapse changed pane contents') require((root / (name + '.txt')).read_bytes() == b'initial\n', 'fixture source file changed') print(f'{"TTY" if args.tty else "SDL"}: 7 local collapse/expand pairs, exact unrelated pane geometry, and content preservation passed') diff --git a/test/column_tags.py b/test/column_tags.py index 6173d38a..80b623bd 100644 --- a/test/column_tags.py +++ b/test/column_tags.py @@ -70,7 +70,7 @@ def await_text(client, row, text): def tag(client, serial): - return client.read(f'/self/pane/{serial}/tag').decode().rstrip('\n') + return client.read(f'/pane/{serial}/tag').decode().rstrip('\n') def new_serial(client, previous): @@ -79,10 +79,10 @@ def new_serial(client, previous): def exec_tag_without_cleanup(client, serial, text): """Restore can replace the whole core, so never touch the old pane after it.""" - client.write(f'/self/pane/{serial}/tag', (' ' + text).encode()) - whole = client.read(f'/self/pane/{serial}/tag') + client.write(f'/pane/{serial}/tag', (' ' + text).encode()) + whole = client.read(f'/pane/{serial}/tag') start = whole.rfind(text.encode()) - client.write(f'/self/pane/{serial}/event', f'Mx{start} {start + len(text.encode())}\n'.encode()) + client.write(f'/pane/{serial}/event', f'Mx{start} {start + len(text.encode())}\n'.encode()) def reconnect(address): @@ -140,10 +140,10 @@ def run(args): keys.click(*require(locate(client, 'Newcol', row=0), 'Newcol missing'), button=1) right = require(wait_for(lambda: new_serial(client, before)), 'Newcol did not create its pane') right_name = root / 'right.rs' - client.write(f'/self/pane/{right}/ctl', f'name {right_name}\n'.encode()) + client.write(f'/pane/{right}/name', f'{right_name}\n'.encode()) right_body = b'// Column-local scratch\nfn main() {\n let ready = true;\n}\n' - client.write(f'/self/pane/{right}/body', right_body) - client.write(f'/self/pane/{right}/ctl', b'clean\n') + client.write(f'/pane/{right}/body', right_body) + client.write(f'/pane/{right}/ctl', b'clean\n') require(wait_for(lambda: locate(client, 'New Tty', row=1, minimum_col=75)), 'right column tag missing') require(locate(client, 'New Tty', row=1, minimum_col=75) == (75, 1), 'right column tag retains an inset') @@ -171,9 +171,9 @@ def run(args): before = panes(client) keys.click(col_start, 1, button=1) extra = require(wait_for(lambda: new_serial(client, before)), 'right-column New did not create a pane') - client.write(f'/self/pane/{extra}/body', b'RIGHT-COLUMN-NEW\n') + client.write(f'/pane/{extra}/body', b'RIGHT-COLUMN-NEW\n') require(wait_for(lambda: locate(client, 'RIGHT-COLUMN-NEW', minimum_col=75)), 'right-column command targeted another column') - client.write(f'/self/pane/{extra}/ctl', b'delete\n') + client.write(f'/pane/{extra}/ctl', b'exec Del\n') checks.append('middle-click New targets the inactive right column') # An owned short-path file makes its complete tag visible. Pane-tag @@ -197,7 +197,7 @@ def run(args): keys.escape() require(wait_for(lambda: tag(client, right).startswith(str(right_name) + ' ')), 'Escape did not cancel filename draft') require(not (root / 'Xright.rs').exists(), 'editing a filename wrote a file') - require(client.read(f'/self/pane/{right}/body') == right_body, 'tag editing changed file body') + require(client.read(f'/pane/{right}/body') == right_body, 'tag editing changed file body') checks.append('filename draft cancels without disk writes or body changes') # Keep a real embedded shell in the right column for useful captures. @@ -205,9 +205,9 @@ def run(args): before = panes(client) keys.click(*require(locate(client, 'Tty', row=1, minimum_col=75), 'Tty missing'), button=1) terminal = require(wait_for(lambda: new_serial(client, before)), 'column Tty did not create a pane') - require('pty' in client.list(f'/self/pane/{terminal}'), 'Tty created a non-terminal pane') - client.write(f'/self/pane/{terminal}/pty/data', b"printf 'Column-local terminal ready\\n'; pwd\r") - require(wait_for(lambda: b'\nColumn-local terminal ready\n' in client.read(f'/self/pane/{terminal}/body').replace(b'\r', b'')), 'terminal command output missing') + require('pty' in client.list(f'/pane/{terminal}'), 'Tty created a non-terminal pane') + client.write(f'/pane/{terminal}/pty/data', b"printf 'Column-local terminal ready\\n'; pwd\r") + require(wait_for(lambda: b'\nColumn-local terminal ready\n' in client.read(f'/pane/{terminal}/body').replace(b'\r', b'')), 'terminal command output missing') checks.append('right-column embedded terminal executes harmless commands') # Persistence is checked through real Dump/Restore dispatch and the @@ -285,11 +285,11 @@ def run(args): capture(client, root, 'columns-tag-reveal', not args.tty) keys.escape() checks.append('long-path pane tag reveals its command tail when the caret moves to End') - require(client.read(f'/self/pane/{source_serial}/body') == original == source.read_bytes(), 'Agave source changed') + require(client.read(f'/pane/{source_serial}/body') == original == source.read_bytes(), 'Agave source changed') checks.append('Agave source bytes unchanged') for serial in panes(client): - if 'pty' in client.list(f'/self/pane/{serial}'): - client.write(f'/self/pane/{serial}/pty/data', b'exit\r') + if 'pty' in client.list(f'/pane/{serial}'): + client.write(f'/pane/{serial}/pty/data', b'exit\r') report = {'checks': checks, 'cols': 150, 'rows': 48, 'host': 'tty' if args.tty else 'sdl', 'input': 'keyboard and SGR mouse through the host PTY parser', 'source': str(source), 'artifacts': str(root)} diff --git a/test/font_size.py b/test/font_size.py index e40b7b3a..3a0a9a28 100644 --- a/test/font_size.py +++ b/test/font_size.py @@ -29,7 +29,7 @@ def run(args): def ready(): for serial, tag in panes(client).items(): if '/+Config ' in tag: - body = client.read(f'/self/pane/{serial}/body').decode() + body = client.read(f'/pane/{serial}/body').decode() if f'Font effective size: {size:.2f} pixels' in body: return body return None @@ -72,7 +72,7 @@ def session(binary, root, name, *options, socket_name=None, file=None, tty=False time.sleep(.005) with Client(address) as client: if network is not None: - for dial in client.read('/self/listeners').decode().splitlines(): + for dial in client.read('/listeners').decode().splitlines(): parts = dial.split('!') if parts[0] in ['tcp', 'quic']: assert len(parts) == 3 and 0 < int(parts[2]) < 65536, dial @@ -101,60 +101,136 @@ def session(binary, root, name, *options, socket_name=None, file=None, tty=False def new_pane(client, contents): - serial = int(client.read('/self/new/ctl').split()[0]) - client.write(f'/self/pane/{serial}/body', contents) + serial = int(client.read('/new').split()[0]) + if contents: + client.write(f'/pane/{serial}/body', contents) return serial def look(client, path, source=None): + """A right click on `path` from a pane (a fresh scratch by default).""" serial = source if source is not None else new_pane(client, b'') - client.write(f'/self/pane/{serial}/body', path.encode(), truncate=True) - client.write(f'/self/pane/{serial}/event', f'ML0 {len(path.encode())}\n'.encode()) - rows = client.read('/self/index').splitlines() - opened = int(rows[-1].split()[0]) - assert opened != serial, f'Look did not open {path}' - return opened + client.write(f'/pane/{serial}/ctl', f'look {path}\n'.encode()) + results = [int(row) for row in client.read('/ctl').split()] + assert len(results) == 1 and results[0] != serial, (path, results) + return results[0] def execute(client, serial, command): - text = command.encode() - client.write(f'/self/pane/{serial}/body', text, truncate=True) - client.write(f'/self/pane/{serial}/event', f'MX0 {len(text)}\n'.encode()) + """A middle click on `command` from a pane.""" + client.write(f'/pane/{serial}/ctl', f'exec {command}\n'.encode()) -def discovery(binary): +def walk_tree(client, path='/', skip=('/new', '/log', '/os', '/screen')): + """Stat every entry below `path`, as `find` does, without opening anything.""" + seen = [] + for name in client.list(path): + child = (path.rstrip('/') + '/' + name) + info = client.stat(child) + seen.append((child, info)) + if info['dir'] and child not in skip: + seen.extend(walk_tree(client, child, skip)) + return seen + + +def discovery(binary, embedded=False): with tempfile.TemporaryDirectory(prefix='pardes-discovery-') as directory: - with session(binary, Path(directory), 'discovery') as (client, _): - before = client.read('/self/index') - entries = client.list('/self/new') - assert {'README', 'ctl', 'body', 'addr', 'data', 'event'} <= set(entries) - guide = client.read('/self/README') - assert guide == client.read('/self/new/README') - assert b'Each open creates a separate pane' in guide - # Walk and stat each entry, as a filesystem browser does, without open. - from ninep import string - for name in entries: - client.fid += 1 - fid = client.fid - client.rpc(110, struct.pack('<IIH', 1, fid, 3) + - string('self') + string('new') + string(name)) + root = Path(directory) + with session(binary, root, 'discovery') as (client, _): + top = client.list('/') + assert top[:9] == ['README', 'index', 'ctl', 'new', 'log', 'screen', 'listeners', 'pane', 'os'], top + assert 'self' not in top and 'cons' not in top, top + assert ('src' in top) == embedded, (top, embedded) + before = client.read('/index') + guide = client.read('/README') + assert guide.count(b'\n') <= 45 and b'exec Msg' in guide and b'/new' in guide, guide + assert client.stat('/README')['length'] == len(guide) + # ls/stat/find over the whole tree, without opening, creates nothing. + seen = walk_tree(client) + names = {path for path, _ in seen} + assert {'/index', '/ctl', '/new', '/log', '/pane/1', '/pane/1/body', '/pane/1/name', + '/pane/1/sel', '/pane/1/ctl', '/pane/1/tag', '/pane/1/event'} <= names, sorted(names) + assert '/pane/1/pty' not in names, 'the fixture file pane grew a pty/' + assert not any(name.startswith('/self') or name.endswith('/rdsel') or name.endswith('/wrsel') + for name in names), sorted(names) + for path, info in seen: + assert info['mode'] in (0o755, 0o644, 0o666, 0o444, 0o222, 0o555), (path, info) + assert info['mtime'] > 0, (path, info) + assert client.read('/index') == before, 'browsing created a pane' + assert client.stat('/index')['length'] == len(before) > 0 + index = client.stat('/index') + assert index['mode'] == 0o444 and client.stat('/ctl')['mode'] == 0o666 + assert client.stat('/pane/1/body')['mode'] == 0o666 and client.stat('/pane')['mode'] == 0o755 + fixture = int(before.split()[0]) + assert before.split()[1:3] == [b'text', b'0'] and before.endswith(b'/discovery.txt\n'), before + # Before any command, /ctl reports the editor. + status = dict(line.split(maxsplit=1) for line in client.read('/ctl').decode().splitlines()) + assert int(status['pid']) > 0 and status['version'] and int(status['panes']) == 1, status + # /new: one pane per open, its serial is the file's content. + log = client.open('/log') + first = int(client.read('/new').split()[0]) + second = int(client.read('/new').split()[0]) + assert first != second and first != fixture, (first, second) + assert client.read_fid(log) == f'new {first} {root}/+New\n'.encode() + assert client.read_fid(log) == f'new {second} {root}/+New\n'.encode() + assert set(client.list('/pane')) == {str(fixture), str(first), str(second)} + client.write(f'/pane/{first}/body', b'first pane', truncate=True) + assert client.read(f'/pane/{first}/body') == b'first pane' + assert client.read(f'/pane/{second}/body') == b'' + assert client.stat(f'/pane/{first}/body')['length'] == len(b'first pane') + rows = {int(row.split()[0]): row.split() for row in client.read('/index').splitlines()} + assert rows[first][1:3] == [b'text', b'1'] and rows[second][1:3] == [b'text', b'0'], rows + # name: read the file name, write to rename; sel: the selection. + assert client.read(f'/pane/{first}/name') == f'{root}/+New\n'.encode() + client.write(f'/pane/{first}/name', b'notes.txt\n') + assert client.read(f'/pane/{first}/name') == f'{root}/notes.txt\n'.encode() + assert client.read_fid(log) == f'rename {first} {root}/notes.txt\n'.encode() + assert client.stat(f'/pane/{first}/name')['length'] == len(f'{root}/notes.txt\n') + client.write(f'/pane/{first}/addr', b'#0,#5') + client.write(f'/pane/{first}/ctl', b'dot=addr\n') + assert client.read(f'/pane/{first}/sel') == b'first' + assert client.stat(f'/pane/{first}/sel')['length'] == 5 + client.write(f'/pane/{first}/sel', b'FIRST') + assert client.read(f'/pane/{first}/body') == b'FIRST pane' + assert client.read(f'/pane/{first}/sel') == b'' + # exec Msg shows text; exec Save writes the named file; exec Del closes. + client.write(f'/pane/{first}/ctl', b'exec Msg hello from 9P\n') + assert 'hello from 9P' in ''.join(cell[0] for cell in client.screen()['cells']) + client.write(f'/pane/{first}/ctl', b'exec Save\n') + assert (root / 'notes.txt').read_bytes() == b'FIRST pane' + assert client.read_fid(log) == f'save {first} {root}/notes.txt\n'.encode() + assert client.read('/ctl') == f'{first}\n'.encode() + client.write('/ctl', b'exec Del\n') + assert client.read_fid(log) == f'del {second} {root}/+New\n'.encode() + assert str(second) not in client.list('/pane') + # look through /ctl opens a file; the serial reads back. + (root / 'opened.txt').write_bytes(b'one\ntwo\nthree\n') + client.write('/ctl', f'look {root}/opened.txt:2\n'.encode()) + opened = int(client.read('/ctl')) + assert opened not in (fixture, first, second), opened + assert client.read(f'/pane/{opened}/body') == b'one\ntwo\nthree\n' + assert client.read(f'/pane/{opened}/name') == f'{root}/opened.txt\n'.encode() + assert client.read_fid(log) == f'new {opened} {root}/opened.txt\n'.encode() + client.write(f'/pane/{first}/ctl', f'look {root}/opened.txt:3\n'.encode()) + assert client.read('/ctl') == f'{opened}\n'.encode(), 'a repeated look targets the open pane' + assert set(client.list('/pane')) == {str(fixture), str(first), str(opened)} + # Writing an event record back still performs the action it names. + client.write(f'/pane/{first}/body', b'Msg via event', truncate=True) + client.write(f'/pane/{first}/event', b'FX0 13\n') + assert 'via event' in ''.join(cell[0] for cell in client.screen()['cells']) + for bad in [b'Look x\n', b'Exec Msg x\n', b'put\n', b'name x\n', b'del\n']: try: - client.rpc(124, struct.pack('<I', fid)) - finally: - client.close(fid) - assert client.read('/self/index') == before, 'browsing created a pane' - first = int(client.read('/self/new/ctl').split()[0]) - second = int(client.read('/self/new/ctl').split()[0]) - assert first != second - client.write(f'/self/pane/{first}/body', b'first pane', truncate=True) - assert client.read(f'/self/pane/{first}/body') == b'first pane' - assert client.read(f'/self/pane/{second}/body') == b'' - before_ids = set(client.list('/self/pane')) - client.write('/self/new/body', b'created with text', truncate=True) - made = set(client.list('/self/pane')) - before_ids - assert len(made) == 1 - assert client.read('/self/pane/' + made.pop() + '/body') == b'created with text' - print('9P discovery: listing/stat/README are inert; opens create independent panes') + client.write('/ctl', bad) + except OSError: + pass + else: + raise AssertionError(f'{bad!r} was accepted') + client.close(log) + if embedded: + assert client.read('/src/pardes.zig').startswith(b'const std') + assert client.stat('/src/pardes.zig')['mode'] == 0o444 + assert b'pub const Pardes' in client.read(f'/pane/{look(client, "/virtual/src/pardes.zig")}/body') + print('9P discovery: listing/stat/find are inert; new, ctl, name, sel and log behave') def test(binary, quic=False): @@ -188,14 +264,14 @@ def test(binary, quic=False): with session(binary, root, 'renamed', '--9p=explicit', socket_name='explicit') as (named, named_address): assert named_address.name == 'pardes-9p-explicit.sock' assert not (root / 'pardes-9p-renamed.sock').exists() - assert named.read('/self/pane/1/body') == b'initial\n' + assert named.read('/pane/1/body') == b'initial\n' with session(binary, root, 'first') as (client, address): - assert set(client.list('/')) == {'os', 'self'} - assert {'pane', 'index', 'new'} <= set(client.list('/self')) - assert client.read('/self/pane/1/body') == b'initial\n' + assert {'pane', 'index', 'new', 'ctl', 'log', 'os'} <= set(client.list('/')) + assert 'self' not in client.list('/') + assert client.read('/pane/1/body') == b'initial\n' assert client.read('/os' + str(root / 'first.txt')) == b'initial\n' assert 'first.txt' in client.list('/os' + str(root)) - assert client.read('/self/pane/1/../1/body') == b'initial\n' + assert client.read('/pane/1/../1/body') == b'initial\n' assert client.read('/os' + str(root) + '/../' + root.name + '/first.txt') == b'initial\n' listing = root / 'listing' @@ -207,9 +283,9 @@ def test(binary, quic=False): assert set(small.list('/os' + str(listing))) == { f'entry-{number:02}' for number in range(24)} - frozen = client.open('/self/screen') + frozen = client.open('/screen') before = bytearray(client.read_fid(frozen, count=31)) - client.write('/self/pane/1/body', b'screen changed\n') + client.write('/pane/1/body', b'screen changed\n') while chunk := client.read_fid(frozen, len(before)): before.extend(chunk) client.close(frozen) @@ -220,12 +296,12 @@ def test(binary, quic=False): payload = ('const value = "λ";\n' * 3000).encode() serial = new_pane(client, payload) - body = f'/self/pane/{serial}/body' + body = f'/pane/{serial}/body' assert client.read(body) == payload client.write(body, b'replaced\n', truncate=True) assert client.read(body) == b'replaced\n' - event = client.open(f'/self/pane/{serial}/event') + event = client.open(f'/pane/{serial}/event') client.write(body, b'event\n') record = client.read_fid(event) assert record.startswith(b'EI') and b'event\n' in record, record @@ -242,82 +318,83 @@ def test(binary, quic=False): missing = False try: - client.read('/self/pane/4294967295/body') + client.read('/pane/4294967295/body') except OSError: missing = True assert missing assert client.read(body) == b'replaced\nevent\n' - source = look(client, '/virtual/src/pardes.zig') - assert b'pub const Pardes' in client.read(f'/self/pane/{source}/body') + if 'src' in client.list('/'): + source = look(client, '/virtual/src/pardes.zig') + assert b'pub const Pardes' in client.read(f'/pane/{source}/body') (root / 'index').write_bytes(b'OS takes precedence\n') opened = look(client, 'index') - assert client.read(f'/self/pane/{opened}/body') == b'OS takes precedence\n' + assert client.read(f'/pane/{opened}/body') == b'OS takes precedence\n' opened = look(client, '/n/self/index') - index = client.read(f'/self/pane/{opened}/body') + index = client.read(f'/pane/{opened}/body') assert index.split()[0] == b'1' and b'first.txt' in index, index (root / 'explicit.txt').write_bytes(b'explicit OS mount\n') opened = look(client, '/n/os' + str(root / 'explicit.txt')) - assert client.read(f'/self/pane/{opened}/body') == b'explicit OS mount\n' + assert client.read(f'/pane/{opened}/body') == b'explicit OS mount\n' own = root / 'pardes-9p-second.sock' with session(binary, root, 'second', '--mount=peer=' + str(address), '--mount=own=' + str(own)) as (remote, _): - opened = look(remote, '/n/own/self/pane/1/body') - assert remote.read(f'/self/pane/{opened}/body') == b'initial\n' - opened = look(remote, '/n/peer/self/pane/' + str(serial) + '/body') - assert remote.read(f'/self/pane/{opened}/body') == b'replaced\nevent\n' - remote.write(f'/self/pane/{opened}/body', b'saved to peer\n', truncate=True) - remote.write(f'/self/pane/{opened}/ctl', b'put\n') + opened = look(remote, '/n/own/pane/1/body') + assert remote.read(f'/pane/{opened}/body') == b'initial\n' + opened = look(remote, '/n/peer/pane/' + str(serial) + '/body') + assert remote.read(f'/pane/{opened}/body') == b'replaced\nevent\n' + remote.write(f'/pane/{opened}/body', b'saved to peer\n', truncate=True) + remote.write(f'/pane/{opened}/ctl', b'exec Save\n') assert client.read(body) == b'saved to peer\n' large = b'mounted file contents\n' * 60000 large_path = root / 'large.txt' large_path.write_bytes(large) opened = look(remote, '/n/peer/os' + str(large_path)) - assert remote.read(f'/self/pane/{opened}/body') == large + assert remote.read(f'/pane/{opened}/body') == large edited = large[:-3] + b'edited\n' - remote.write(f'/self/pane/{opened}/body', edited, truncate=True) - remote.write(f'/self/pane/{opened}/ctl', b'put\n') + remote.write(f'/pane/{opened}/body', edited, truncate=True) + remote.write(f'/pane/{opened}/ctl', b'exec Save\n') assert large_path.read_bytes() == edited assert client.read('/os' + str(large_path)) == edited with session(binary, root, 'runtime') as (remote, own_address): control = new_pane(remote, b'') execute(remote, control, 'Mount peer ' + str(address)) - opened = look(remote, '/n/peer/self/pane/1/body', source=control) - assert remote.read(f'/self/pane/{opened}/body') == client.read('/self/pane/1/body') + opened = look(remote, '/n/peer/pane/1/body', source=control) + assert remote.read(f'/pane/{opened}/body') == client.read('/pane/1/body') execute(remote, control, 'Mount peer ' + str(own_address)) execute(remote, control, 'Unmount peer') - remote.write(f'/self/pane/{opened}/body', b'runtime mounted Save\n', truncate=True) - remote.write(f'/self/pane/{opened}/ctl', b'put\n') - assert client.read('/self/pane/1/body') == b'runtime mounted Save\n' - assert remote.read('/self/pane/1/body') == b'initial\n' + remote.write(f'/pane/{opened}/body', b'runtime mounted Save\n', truncate=True) + remote.write(f'/pane/{opened}/ctl', b'exec Save\n') + assert client.read('/pane/1/body') == b'runtime mounted Save\n' + assert remote.read('/pane/1/body') == b'initial\n' roots = look(remote, '/n', source=control) - assert b'/n/peer/\n' in remote.read(f'/self/pane/{roots}/body') - remote.write(f'/self/pane/{opened}/ctl', b'delete\n') + assert b'/n/peer/\n' in remote.read(f'/pane/{roots}/body') + remote.write(f'/pane/{opened}/ctl', b'exec Del\n') execute(remote, control, 'Unmount peer') - remote.write(f'/self/pane/{roots}/ctl', b'get\n') - assert remote.read(f'/self/pane/{roots}/body') == b'/n/os/\n/n/self/\n' + remote.write(f'/pane/{roots}/ctl', b'get\n') + assert remote.read(f'/pane/{roots}/body') == b'/n/os/\n/n/self/\n' execute(remote, control, 'Mount peer ' + str(own_address)) - opened = look(remote, '/n/peer/self/pane/1/body', source=control) - assert remote.read(f'/self/pane/{opened}/body') == b'initial\n' + opened = look(remote, '/n/peer/pane/1/body', source=control) + assert remote.read(f'/pane/{opened}/body') == b'initial\n' with session(binary, root, 'mounted-startup', '--mount=peer=' + str(address), - file='/n/peer/self/pane/1/body:1') as (remote, _): - assert remote.read('/self/pane/1/body') == b'runtime mounted Save\n' + file='/n/peer/pane/1/body:1') as (remote, _): + assert remote.read('/pane/1/body') == b'runtime mounted Save\n' - client.write(f'/self/pane/{serial}/ctl', b'delete\n') - assert str(serial) not in client.list('/self/pane') + client.write(f'/pane/{serial}/ctl', b'exec Del\n') + assert str(serial) not in client.list('/pane') for tty in [False, True]: name = 'restore tty space' if tty else 'restore detached space' with session(binary, root, name, tty=tty) as (old, address): - old.write('/self/pane/1/body', b'dumped state\n', truncate=True) + old.write('/pane/1/body', b'dumped state\n', truncate=True) control = new_pane(old, b'') execute(old, control, 'Restore ' + str(root / 'missing dump.zon')) - assert old.read('/self/pane/1/body') == b'dumped state\n' + assert old.read('/pane/1/body') == b'dumped state\n' execute(old, control, 'Mount own ' + str(address)) execute(old, control, 'Dump') saved = root / (name + '.dump.zon') @@ -325,12 +402,12 @@ def test(binary, quic=False): while not saved.exists(): assert time.monotonic() < deadline, 'Dump did not write its state' time.sleep(.005) - held_body = old.open('/self/pane/1/body') - held_screen = old.open('/self/screen') - old_ids = {int(row.split()[0]) for row in old.read('/self/index').splitlines()} + held_body = old.open('/pane/1/body') + held_screen = old.open('/screen') + old_ids = {int(row.split()[0]) for row in old.read('/index').splitlines()} assert old.read_fid(held_body) == b'dumped state\n' assert old.read_fid(held_screen, count=1) == b'{' - old.write('/self/pane/1/body', b'changed after dump\n', truncate=True) + old.write('/pane/1/body', b'changed after dump\n', truncate=True) try: execute(old, control, 'Restore ' + str(saved)) except (EOFError, ConnectionResetError, BrokenPipeError): @@ -342,15 +419,15 @@ def test(binary, quic=False): else: raise AssertionError('Restore left an old body fid usable') with Client(address) as restored: - restored_ids = [int(row.split()[0]) for row in restored.read('/self/index').splitlines()] + restored_ids = [int(row.split()[0]) for row in restored.read('/index').splitlines()] assert len(restored_ids) == 2 and old_ids.isdisjoint(restored_ids), restored_ids file_id, control_id = restored_ids - contents = restored.read(f'/self/pane/{file_id}/body') - assert contents == b'dumped state\n', (contents, saved.read_text(), restored.read('/self/index')) - assert restored.read(f'/self/pane/{control_id}/body') == b'Dump' + contents = restored.read(f'/pane/{file_id}/body') + assert contents == b'dumped state\n', (contents, saved.read_text(), restored.read('/index')) + assert restored.read(f'/pane/{control_id}/body') == b'', 'commands ran through ctl, not the body' assert restored.screen()['cols'] == 80 opened = look(restored, '/n/own/os' + str(root / (name + '.txt')), source=control_id) - assert restored.read(f'/self/pane/{opened}/body') == b'initial\n' + assert restored.read(f'/pane/{opened}/body') == b'initial\n' with session(binary, root, 'forwarding') as (client, address): child_dir = root / 'child-working-directory' @@ -362,25 +439,25 @@ def test(binary, quic=False): for word, expected, selected, reuse in [('space name.txt:2:4', spaced.read_bytes(), [14, 14], False), ('/n/self/pane/1/body', b'initial\n', None, False), ('/virtual/pane/1/body:1:2-4', b'initial\n', [1, 4], True)]: - before = {int(row.split()[0]) for row in client.read('/self/index').splitlines()} + before = {int(row.split()[0]) for row in client.read('/index').splitlines()} result = subprocess.run([binary, '--tty', word], cwd=child_dir, env=env, capture_output=True, timeout=3) assert result.returncode == 0, (word, result.stderr) - after = {int(row.split()[0]) for row in client.read('/self/index').splitlines()} + after = {int(row.split()[0]) for row in client.read('/index').splitlines()} if reuse: assert after == before else: opened, = after - before - assert client.read(f'/self/pane/{opened}/body') == expected - assert client.read('/self/pane/1/body') == b'initial\n' + assert client.read(f'/pane/{opened}/body') == expected + assert client.read('/pane/1/body') == b'initial\n' if selected is not None: - addr = client.open(f'/self/pane/{opened}/addr') - client.write(f'/self/pane/{opened}/ctl', b'addr=dot\n') + addr = client.open(f'/pane/{opened}/addr') + client.write(f'/pane/{opened}/ctl', b'addr=dot\n') actual = list(map(int, client.read_fid(addr).split())) client.close(addr) assert actual == selected, (word, actual, selected) - addr = client.open('/self/pane/1/addr', 2) - ctl = client.open('/self/pane/1/ctl', 1) + addr = client.open('/pane/1/addr', 2) + ctl = client.open('/pane/1/ctl', 1) for fid, text in [(addr, b'#0,#3'), (ctl, b'limit=addr\n'), (addr, b'#0')]: client.rpc(118, struct.pack('<IQI', fid, 0, len(text)) + text) try: @@ -398,7 +475,7 @@ def test(binary, quic=False): finally: client.close(addr) client.close(ctl) - client.read('/self/pane/1/ctl') + client.read('/pane/1/ctl') control = new_pane(client, b'') execute(client, control, 'Dump') @@ -407,10 +484,10 @@ def test(binary, quic=False): while not saved.exists(): assert time.monotonic() < deadline, 'forwarding fixture Dump did not finish' time.sleep(.005) - before = client.read('/self/index') + before = client.read('/index') inherited = {key: env[key] for key in ['PARDES_9P', 'PARDES_PANE', 'PARDES_FORWARD_LOOK']} cases = [ - ('mount', ['--mount=peer=' + str(address), '/n/peer/self/pane/1/body'], None), + ('mount', ['--mount=peer=' + str(address), '/n/peer/pane/1/body'], None), ('name', ['--9p=forwarded-name'], 'forwarded-name'), ('tcp', ['--9p-tcp=tcp!127.0.0.1!0'], None), ('load', ['-l', str(saved)], None), @@ -425,19 +502,19 @@ def test(binary, quic=False): launch=[*([] if name == 'shells' else ['--tty']), *options], inherited=inherited) as (local, local_address): assert local_address != address - index = local.read('/self/index').splitlines() + index = local.read('/index').splitlines() if name == 'mount': assert len(index) == 1 and b'/n/peer/' in index[0], index - assert local.read('/self/pane/1/body') == b'initial\n' + assert local.read('/pane/1/body') == b'initial\n' elif name == 'load': - assert local.read('/self/index') == before - assert local.read('/self/pane/1/body') == b'initial\n' + assert local.read('/index') == before + assert local.read('/pane/1/body') == b'initial\n' else: assert len(index) == (3 if name == 'shells' else 1), index if name in ['tcp', 'quic']: - assert (name + '!127.0.0.1!').encode() in local.read('/self/listeners') - assert client.read('/self/index') == before - assert client.read('/self/pane/1/body') == b'initial\n' + assert (name + '!127.0.0.1!').encode() in local.read('/listeners') + assert client.read('/index') == before + assert client.read('/pane/1/body') == b'initial\n' for name, identity, word in [ ('stale-file', dict(inherited, PARDES_9P=str(root / 'absent.sock')), str(spaced)), @@ -450,11 +527,11 @@ def test(binary, quic=False): launch=['--tty', *([word] if word else [])]) as (local, local_address): assert local_address != address if word == str(spaced): - assert local.read('/self/pane/1/body') == spaced.read_bytes() + assert local.read('/pane/1/body') == spaced.read_bytes() elif word: - assert word.encode() in local.read('/self/index') or word in ''.join( + assert word.encode() in local.read('/index') or word in ''.join( cell[0] for cell in local.screen()['cells']) - assert client.read('/self/index') == before + assert client.read('/index') == before for tty in [False, True]: name = 'lsp-tty' if tty else 'lsp-detached' @@ -463,23 +540,23 @@ def test(binary, quic=False): execute(client, control, 'Lspinfo') deadline = time.monotonic() + 3 while True: - matches = [int(row.split()[0]) for row in client.read('/self/index').splitlines() + matches = [int(row.split()[0]) for row in client.read('/index').splitlines() if b'+Lsp' in row] if matches: break assert time.monotonic() < deadline, (name, 'Lspinfo did not complete') time.sleep(.005) assert len(matches) == 1, matches - result = client.read(f'/self/pane/{matches[0]}/body') + result = client.read(f'/pane/{matches[0]}/body') assert result.startswith(b'backend: zls-inproc+lsp-client\n'), result assert b'protocol servers (lsp-client):' in result, result - assert client.read('/self/pane/1/body') == b'initial\n' + assert client.read('/pane/1/body') == b'initial\n' assert '+Lsp' in ''.join(cell[0] for cell in client.screen()['cells']) with session(binary, root, 'highlight') as (client, _): - client.write('/self/pane/1/ctl', b'name keywords.zig\n') + client.write('/pane/1/name', b'keywords.zig\n') source = b'// padding\n' * 9999 + b'fn check() void {\n if (true) return;\n}\n' - client.write('/self/pane/1/body', source, truncate=True) + client.write('/pane/1/body', source, truncate=True) screen = client.screen() assert len(screen['cells']) == screen['cols'] * screen['rows'] found = set() @@ -508,9 +585,9 @@ def test(binary, quic=False): client.write(wire_path, b'written through long filename\n', truncate=True) assert path.read_bytes() == b'written through long filename\n' opened = look(client, '/n/os' + str(path)) - assert client.read(f'/self/pane/{opened}/body') == path.read_bytes() + assert client.read(f'/pane/{opened}/body') == path.read_bytes() opened = look(client, '/n/os' + str(directory)) - listing = client.read(f'/self/pane/{opened}/body') + listing = client.read(f'/pane/{opened}/body') for path in directory.iterdir(): assert ('/n/os' + str(path)).encode() in listing if quic: @@ -526,66 +603,66 @@ def quic_test(binary): ports = {} with session(binary, root, 'server', '--9p-tcp=tcp!' + host + '!0', '--9p-quic=quic!' + host + '!0', tty=tty, network=ports) as (unix, address): - assert set(unix.read('/self/listeners').decode().splitlines()) == { + assert set(unix.read('/listeners').decode().splitlines()) == { 'unix!' + str(address), 'tcp!' + host + '!' + str(ports['TCP']), 'quic!' + host + '!' + str(ports['QUIC'])} with Client((host, ports['TCP'])) as tcp: - unix_body = unix.open('/self/pane/1/body') - tcp_body = tcp.open('/self/pane/1/body') + unix_body = unix.open('/pane/1/body') + tcp_body = tcp.open('/pane/1/body') with session(binary, root, 'client') as (remote, _): control = new_pane(remote, b'') dial = 'quic!' + host + '!' + str(ports['QUIC']) execute(remote, control, 'Mount peer ' + dial) - opened = look(remote, '/n/peer/self/pane/1/body', source=control) - body = f'/self/pane/{opened}/body' + opened = look(remote, '/n/peer/pane/1/body', source=control) + body = f'/pane/{opened}/body' assert remote.read(body) == b'initial\n' payload = 'saved café λ\n'.encode() + b'x' * (3 * 8192 + 27) + b'\n' remote.write(body, payload, truncate=True) - remote.write(f'/self/pane/{opened}/ctl', b'put\n') + remote.write(f'/pane/{opened}/ctl', b'exec Save\n') assert unix.read_fid(unix_body) == payload[:unix.msize - 11] - assert tcp.read('/self/pane/1/body') == payload - tcp.write('/self/pane/1/body', b'TCP to QUIC Get\n', truncate=True) - remote.write(f'/self/pane/{opened}/ctl', b'get\n') + assert tcp.read('/pane/1/body') == payload + tcp.write('/pane/1/body', b'TCP to QUIC Get\n', truncate=True) + remote.write(f'/pane/{opened}/ctl', b'get\n') assert remote.read(body) == b'TCP to QUIC Get\n' assert unix.read_fid(unix_body) == b'TCP to QUIC Get\n' assert tcp.read_fid(tcp_body) == b'TCP to QUIC Get\n' - screen_pane = look(remote, '/n/peer/self/screen', source=control) - screen = json.loads(remote.read(f'/self/pane/{screen_pane}/body')) + screen_pane = look(remote, '/n/peer/screen', source=control) + screen = json.loads(remote.read(f'/pane/{screen_pane}/body')) assert len(screen['cells']) == screen['cols'] * screen['rows'] assert 'TCP to QUIC Get' in ''.join(cell[0] for cell in screen['cells']) large = ('normal λ line\n' * 20000).encode() fixture = root / 'large.txt' fixture.write_bytes(large) large_pane = look(remote, '/n/peer/os' + str(fixture), source=control) - assert remote.read(f'/self/pane/{large_pane}/body') == large + assert remote.read(f'/pane/{large_pane}/body') == large edited = b'QUIC file saved\n' - remote.write(f'/self/pane/{large_pane}/body', edited, truncate=True) - remote.write(f'/self/pane/{large_pane}/ctl', b'put\n') + remote.write(f'/pane/{large_pane}/body', edited, truncate=True) + remote.write(f'/pane/{large_pane}/ctl', b'exec Save\n') assert fixture.read_bytes() == edited for pane in [opened, screen_pane, large_pane]: - remote.write(f'/self/pane/{pane}/ctl', b'delete\n') + remote.write(f'/pane/{pane}/ctl', b'exec Del\n') execute(remote, control, 'Unmount peer') roots = look(remote, '/n', source=control) - assert remote.read(f'/self/pane/{roots}/body') == b'/n/os/\n/n/self/\n' + assert remote.read(f'/pane/{roots}/body') == b'/n/os/\n/n/self/\n' assert unix.read_fid(unix_body) == b'TCP to QUIC Get\n' assert tcp.read_fid(tcp_body) == b'TCP to QUIC Get\n' control = new_pane(unix, b'') execute(unix, control, 'Mount own ' + dial) - own = look(unix, '/n/own/self/pane/1/body', source=control) - assert unix.read(f'/self/pane/{own}/body') == b'TCP to QUIC Get\n' - unix.write(f'/self/pane/{own}/body', b'same-session QUIC Save\n', truncate=True) - unix.write(f'/self/pane/{own}/ctl', b'put\n') + own = look(unix, '/n/own/pane/1/body', source=control) + assert unix.read(f'/pane/{own}/body') == b'TCP to QUIC Get\n' + unix.write(f'/pane/{own}/body', b'same-session QUIC Save\n', truncate=True) + unix.write(f'/pane/{own}/ctl', b'exec Save\n') assert unix.read_fid(unix_body) == b'same-session QUIC Save\n' assert tcp.read_fid(tcp_body) == b'same-session QUIC Save\n' - unix.write(f'/self/pane/{own}/ctl', b'delete\n') + unix.write(f'/pane/{own}/ctl', b'exec Del\n') execute(unix, control, 'Unmount own') unix.close(unix_body) tcp.close(tcp_body) if __name__ == '__main__': - if len(sys.argv) == 3 and sys.argv[2] == '--discovery': - discovery(str(Path(sys.argv[1]).resolve())) + if len(sys.argv) in [3, 4] and sys.argv[2] == '--discovery': + discovery(str(Path(sys.argv[1]).resolve()), embedded=sys.argv[3:] == ['--embedded']) raise SystemExit(0) if len(sys.argv) not in [2, 3] or (len(sys.argv) == 3 and sys.argv[2] != '--quic'): raise SystemExit('usage: fs.py <pardes> [--quic | --discovery]') diff --git a/test/fs_bench.zig b/test/fs_bench.zig index 7fe49734..f9735ee2 100644 --- a/test/fs_bench.zig +++ b/test/fs_bench.zig @@ -1,6 +1,7 @@ const std = @import("std"); const pardes = @import("pardes"); const filesystem = pardes.filesystem; +const ctlfs = pardes.ctlfs; pub const std_options: std.Options = .{ .log_level = .err }; @@ -73,8 +74,8 @@ const Session = struct { s.core.deinit(); } - fn node(s: *const Session, file: filesystem.PaneFile) u64 { - return filesystem.Node.of(s.file_serial, file); + fn node(s: *const Session, file: ctlfs.PaneFile) u64 { + return ctlfs.Node.of(s.file_serial, file); } }; @@ -90,9 +91,9 @@ fn checkNumbers(bytes: []const u8, expected: []const usize) ![]const u8 { return std.mem.trimStart(u8, rest, " "); } -fn checkReply(s: *Session, req: filesystem.Req, reply: filesystem.Reply) !void { +fn checkReply(s: *Session, req: ctlfs.Req, reply: ctlfs.Reply) !void { const event_read = req.op == .read and req.node == s.node(.event); - const expected_status: filesystem.Status = if (event_read) .again else .ok; + const expected_status: ctlfs.Status = if (event_read) .again else .ok; if (reply.tag != req.tag or reply.errno != 0 or reply.status != expected_status) return error.IncorrectReply; const body = s.core.panes[s.file_id].?.file.?.content; @@ -112,16 +113,18 @@ fn checkReply(s: *Session, req: filesystem.Req, reply: filesystem.Reply) !void { if (reply.payload != .staged or reply.payload.staged != bytes.len or bytes.len > req.size) return error.IncorrectReply; const pane = s.core.panes[s.file_id].?; + if (req.node == @intFromEnum(ctlfs.TopFile.index)) { + var expected_buf: [4200]u8 = undefined; + const expected = try std.fmt.bufPrint(&expected_buf, "{d} text {d} {s}\n", .{ s.file_serial, @intFromBool(pane.file.?.revision != pane.file.?.saved_revision), pane.file.?.path }); + if (!std.mem.eql(u8, expected, bytes)) return error.IncorrectReply; + return; + } const rest = try checkNumbers(bytes, &.{ s.file_serial, s.tag.len, body.len, 0, @intFromBool(pane.file.?.revision != pane.file.?.saved_revision) }); if (req.node == s.node(.ctl)) { const tail = try checkNumbers(rest, &.{pane.cols}); if (!std.mem.startsWith(u8, tail, "default ")) return error.IncorrectReply; if ((try checkNumbers(tail[8..], &.{pardes.config.tab_width})).len != 0) return error.IncorrectReply; - } else if (req.node == @intFromEnum(filesystem.SelfFile.index)) { - const tag = s.tag[0 .. std.mem.indexOfScalar(u8, s.tag, '\n') orelse s.tag.len]; - if (rest.len != tag.len + 1 or rest[rest.len - 1] != '\n' or !std.mem.eql(u8, rest[0..tag.len], tag)) - return error.IncorrectReply; } else return error.UnmeasuredOperation; } }, @@ -129,8 +132,8 @@ fn checkReply(s: *Session, req: filesystem.Req, reply: filesystem.Reply) !void { const bytes = s.core.fs.out.items; if (reply.payload != .staged or reply.payload.staged != bytes.len or bytes.len > req.size) return error.IncorrectReply; - const names = [_][]const u8{ "index", "cons", "new", "pane", "screen", "listeners" }; - const nodes = [_]u64{ @intFromEnum(filesystem.SelfFile.index), @intFromEnum(filesystem.SelfFile.cons), @intFromEnum(filesystem.SelfFile.new), filesystem.namespace_panes, @intFromEnum(filesystem.SelfFile.screen), @intFromEnum(filesystem.SelfFile.listeners) }; + const names = [_][]const u8{ "README", "index", "ctl", "new", "log", "screen", "listeners", "pane", "os" }; + const nodes = [_]u64{ @intFromEnum(ctlfs.TopFile.README), @intFromEnum(ctlfs.TopFile.index), @intFromEnum(ctlfs.TopFile.ctl), @intFromEnum(ctlfs.TopFile.new), @intFromEnum(ctlfs.TopFile.log), @intFromEnum(ctlfs.TopFile.screen), @intFromEnum(ctlfs.TopFile.listeners), @intFromEnum(ctlfs.TopFile.pane), filesystem.os_root }; var off: usize = 0; var entry: usize = 0; while (off < bytes.len) : (entry += 1) { @@ -140,7 +143,7 @@ fn checkReply(s: *Session, req: filesystem.Req, reply: filesystem.Reply) !void { const size: usize = bytes[off + 9]; off += 10; if (dir > 1 or size == 0 or size > bytes.len - off) return error.IncorrectReply; - if (entry < names.len and (node != nodes[entry] or dir != @intFromBool(entry == 2 or entry == 3) or + if (entry < names.len and (node != nodes[entry] or dir != @intFromBool(entry >= 7) or !std.mem.eql(u8, names[entry], bytes[off..][0..size]))) return error.IncorrectReply; off += size; } @@ -151,21 +154,21 @@ fn checkReply(s: *Session, req: filesystem.Req, reply: filesystem.Reply) !void { } } -fn bench(s: *Session, name: []const u8, reps: usize, req: filesystem.Req, note: []const u8) !void { +fn bench(s: *Session, name: []const u8, reps: usize, req: ctlfs.Req, note: []const u8) !void { std.debug.assert(reps > 0); const initial_bytes = s.core.panes[s.file_id].?.file.?.content.len; const initial_hash = if (req.op == .write) std.hash.Wyhash.hash(0, s.core.panes[s.file_id].?.file.?.content) else 0; for (0..64) |_| { - try checkReply(s, req, filesystem.handle(s.core, req)); + try checkReply(s, req, ctlfs.handle(s.core, req)); _ = s.core.scratch.reset(.retain_capacity); } s.counting.allocations = 0; - const expected_status: filesystem.Status = if (req.op == .read and req.node == s.node(.event)) .again else .ok; + const expected_status: ctlfs.Status = if (req.op == .read and req.node == s.node(.event)) .again else .ok; var valid = true; - var reply: filesystem.Reply = undefined; + var reply: ctlfs.Reply = undefined; const start = nowNs(); for (0..reps) |_| { - reply = filesystem.handle(s.core, req); + reply = ctlfs.handle(s.core, req); valid = valid and reply.tag == req.tag and reply.status == expected_status and reply.errno == 0; std.mem.doNotOptimizeAway(reply); _ = s.core.scratch.reset(.retain_capacity); @@ -223,7 +226,7 @@ pub fn main(init: std.process.Init) !void { try bench(&s, "lookup ctl", reps, .{ .tag = 2, .op = .lookup, - .node = filesystem.Node.of(s.file_serial, .dir), + .node = ctlfs.Node.of(s.file_serial, .dir), .data = "ctl", }, "name -> node"); try bench(&s, "read body 4K", reps, .{ @@ -249,13 +252,13 @@ pub fn main(init: std.process.Init) !void { try bench(&s, "read index", reps, .{ .tag = 6, .op = .read, - .node = @intFromEnum(filesystem.SelfFile.index), + .node = @intFromEnum(ctlfs.TopFile.index), .size = 4096, }, "one line per pane"); - try bench(&s, "readdir self", reps, .{ + try bench(&s, "readdir root", reps, .{ .tag = 7, .op = .readdir, - .node = @intFromEnum(filesystem.SelfFile.root), + .node = ctlfs.root, .size = 4096, }, "staged dirents"); try bench(&s, "read event (empty)", reps, .{ diff --git a/test/fs_namespace.zig b/test/fs_namespace.zig index c856148d..8b6f245e 100644 --- a/test/fs_namespace.zig +++ b/test/fs_namespace.zig @@ -13,13 +13,13 @@ test "runtime Mount and Unmount copy command arguments and change Look resolutio try std.testing.expectEqualStrings("peer", p.fs.mounts.items[0].name); try std.testing.expectEqualStrings("/tmp/a socket.sock", p.fs.mounts.items[0].dial); var path: [4096]u8 = undefined; - try std.testing.expectEqualStrings("/n/peer/self/index", filesystem.resolve(p, "/n/peer/self/index", "/", &path).?.path); + try std.testing.expectEqualStrings("/n/peer/index", filesystem.resolve(p, "/n/peer/index", "/", &path).?.path); try std.testing.expect(p.executeBuiltinLine(0, "Mount peer /tmp/other.sock")); try std.testing.expectEqual(@as(usize, 1), p.fs.mounts.items.len); try std.testing.expectEqualStrings("/tmp/a socket.sock", p.fs.mounts.items[0].dial); try std.testing.expect(p.executeBuiltinLine(0, "Unmount peer")); try std.testing.expectEqual(@as(usize, 0), p.fs.mounts.items.len); - try std.testing.expect(filesystem.resolve(p, "/n/peer/self/index", "/", &path) == null); + try std.testing.expect(filesystem.resolve(p, "/n/peer/index", "/", &path) == null); for ([_][]const u8{ "Mount", "Mount name", "Mount os /tmp/peer.sock", "Mount peer tcp!127.0.0.1!0", "Unmount", "Unmount name extra" }) |invalid| { try std.testing.expect(p.executeBuiltinLine(0, invalid)); try std.testing.expectEqual(@as(usize, 0), p.fs.mounts.items.len); @@ -185,6 +185,7 @@ test "filesystem resolves OS first then virtual with explicit mounts and parent const fallback = filesystem.resolve(p, "index", directory, &resolved_buffer) orelse return error.MissingVirtualFallback; try std.testing.expectEqualStrings("/virtual/index", fallback.path); + if (!pardes.memory.limits.embedded_sources) return; for ([_][]const u8{ "src/pardes.zig", "/virtual/src/pardes.zig", "/virtual/src/../src/pardes.zig" }) |word| { const found = filesystem.resolve(p, word, directory, &resolved_buffer) orelse return error.MissingEmbeddedSource; const bytes = try filesystem.read(p, found.path); @@ -235,6 +236,7 @@ test "explicit OS file opens and restores keep their names and request watches" } test "virtual file opens do not request OS watches" { + if (!pardes.memory.limits.embedded_sources) return error.SkipZigTest; const p = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); while (p.nextEffect()) |_| {} @@ -243,16 +245,16 @@ test "virtual file opens do not request OS watches" { while (p.nextEffect()) |effect| if (effect == .watch) return error.VirtualFileWatch; } -test "Look at the self factory reserves its own pane and failed opens release it" { +test "Look at the clone file reserves its own pane and failed opens release it" { const p = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); - p.lookAt(0, "/virtual/new/ctl"); + p.lookAt(0, "/virtual/new"); var control_count: usize = 0; var scratch_count: usize = 0; for (p.panes) |slot| { const pane = slot orelse continue; const file = pane.file orelse continue; - if (std.mem.eql(u8, file.path, "/virtual/new/ctl")) control_count += 1; + if (std.mem.eql(u8, file.path, "/virtual/new")) control_count += 1; if (file.output) |output| if (output.from == .cmd and output.from.cmd == .New) { scratch_count += 1; }; diff --git a/test/fs_soak.py b/test/fs_soak.py index 1201fda2..5569a7c7 100644 --- a/test/fs_soak.py +++ b/test/fs_soak.py @@ -56,7 +56,7 @@ def read_fid(client, fid, limit, count): def check_body(client, pane, count=4096): - fid = client.open(f'/self/pane/{pane["serial"]}/body') + fid = client.open(f'/pane/{pane["serial"]}/body') try: actual = read_fid(client, fid, BODY_LIMIT, count) finally: @@ -127,7 +127,7 @@ def run(args): address = unix_address if args.transport == 'tcp': listeners = [line.split('!') for line in - control.read('/self/listeners').decode().splitlines() + control.read('/listeners').decode().splitlines() if line.startswith('tcp!')] assert len(listeners) == 1, listeners transport, host, port = listeners[0] @@ -140,11 +140,12 @@ def run(args): for slot in range(4): body = payload(rng) serial = 1 if slot == 0 else new_pane(client, body) - base = f'/self/pane/{serial}' + base = f'/pane/{serial}' if slot == 0: client.write(base + '/body', body, truncate=True) path = root / f'pane-{slot}-0.zig' - client.write(base + '/ctl', f'name {path}\nput\n'.encode()) + client.write(base + '/name', f'{path}\n'.encode()) + client.write(base + '/ctl', b'exec Save\n') panes.append({'serial': serial, 'body': body, 'path': path, 'variant': 0}) emit(kind='start', seed=args.seed, session_pid=pid, transport=args.transport, host='tty' if args.tty else 'detached', @@ -161,7 +162,7 @@ def run(args): for operation in operations: slot = rng.randrange(len(panes)) pane = panes[slot] - base = f'/self/pane/{pane["serial"]}' + base = f'/pane/{pane["serial"]}' if operation == 'append': addition = payload(rng, min(256, BODY_LIMIT - len(pane['body']))) client.write(base + '/body', addition) @@ -185,7 +186,8 @@ def run(args): elif operation == 'save': pane['variant'] ^= 1 pane['path'] = root / f'pane-{slot}-{pane["variant"]}.zig' - client.write(base + '/ctl', f'name {pane["path"]}\nput\n'.encode()) + client.write(base + '/name', f'{pane["path"]}\n'.encode()) + client.write(base + '/ctl', b'exec Save\n') check_bytes(pane['path'].read_bytes(), pane['body'], 'saved file') check_bytes(client.read('/os' + str(pane['path'])), pane['body'], 'OS mount') elif operation == 'reload': @@ -194,19 +196,20 @@ def run(args): client.write(base + '/ctl', b'get\n') elif operation == 'cycle': old_serial = pane['serial'] - client.write(base + '/ctl', b'delete\n') + client.write(base + '/ctl', b'exec Del\n') pane['body'] = payload(rng) pane['serial'] = new_pane(client, pane['body']) assert pane['serial'] != old_serial - base = f'/self/pane/{pane["serial"]}' - client.write(base + '/ctl', f'name {pane["path"]}\nput\n'.encode()) + base = f'/pane/{pane["serial"]}' + client.write(base + '/name', f'{pane["path"]}\n'.encode()) + client.write(base + '/ctl', b'exec Save\n') elif operation == 'reconnect': client.socket.close() client = Client(address) check_session(client, control, pid) check_body(control, pane) elif operation == 'screen': - frozen = client.open('/self/screen') + frozen = client.open('/screen') try: before = read_fid(client, frozen, SCREEN_LIMIT, 4096) check_screen(before) @@ -217,7 +220,7 @@ def run(args): check_bytes(after, before, 'frozen screen') finally: client.close(frozen) - fresh = client.open('/self/screen') + fresh = client.open('/screen') try: check_screen(read_fid(client, fresh, SCREEN_LIMIT, 4096)) finally: @@ -225,8 +228,8 @@ def run(args): operation_count += 1 check_body(client, pane) expected = {str(pane['serial']) for pane in panes} - assert set(client.list('/self/pane')) == expected - assert set(control.list('/self/pane')) == expected + assert set(client.list('/pane')) == expected + assert set(control.list('/pane')) == expected for pane in panes: check_body(client, pane) check_body(control, pane) diff --git a/test/look_placement.py b/test/look_placement.py index e9918c5d..1341e776 100644 --- a/test/look_placement.py +++ b/test/look_placement.py @@ -70,7 +70,7 @@ def run_case(args, root, source_index): require(len(serials) == 3, 'classic fixture did not boot three terminals') markers = {} for index, serial in enumerate(serials): - pane = f'/self/pane/{serial}' + pane = f'/pane/{serial}' require('pty' in client.list(pane), 'fixture contains an existing document') marker = f'LP_{index}' markers[serial] = marker @@ -89,7 +89,7 @@ def run_case(args, root, source_index): require(before[serials[0]][:2] == (0, 2) and before[serials[1]][0] == 0 and before[serials[1]][1] > 2 and before[serials[2]][:2] == (75, 2), f'unexpected initial classic layout: {before}') - client.write(f'/self/pane/{source}/ctl', f'look {target}\n'.encode()) + client.write(f'/pane/{source}/ctl', f'look {target}\n'.encode()) opened = require(wait_for(lambda: next((serial for serial in panes(client) if serial not in serials), None)), 'Look did not open the first document') @@ -111,9 +111,9 @@ def run_case(args, root, source_index): require(rectangle_text(before_frame, before[serial]) == rectangle_text(after_frame, after[serial]), f'Look changed unrelated terminal {serial} rendered text') - require(client.read(f'/self/pane/{opened}/body') == contents, + require(client.read(f'/pane/{opened}/body') == contents, 'opened document contents differ') - client.write(f'/self/pane/{source}/ctl', f'look {target}:2\n'.encode()) + client.write(f'/pane/{source}/ctl', f'look {target}:2\n'.encode()) require(list(panes(client)) == serials + [opened], 'repeated Look created a duplicate pane') require(wait_for(lambda: geometry(client, markers)[1] == after), 'repeated Look changed layout') diff --git a/test/ninep.py b/test/ninep.py index b128cc8a..6c625951 100644 --- a/test/ninep.py +++ b/test/ninep.py @@ -116,7 +116,44 @@ class Client: self.close(fid) def screen(self): - return json.loads(self.read('/self/screen')) + return json.loads(self.read('/screen')) + + def walk(self, path): + names = [part for part in os.fspath(path).split('/') if part] + self.fid += 1 + fid = self.fid + oldfid = 1 + try: + for start in range(0, max(1, len(names)), 16): + part = names[start:start + 16] + reply = self.rpc(110, struct.pack('<IIH', oldfid, fid, len(part)) + + b''.join(string(name) for name in part)) + if struct.unpack_from('<H', reply)[0] != len(part): + raise FileNotFoundError(path) + oldfid = fid + return fid + except BaseException: + try: + self.close(fid) + except OSError: + pass + raise + + def stat(self, path): + """Tstat without an open: {'name', 'mode', 'length', 'mtime', 'qid'}.""" + fid = self.walk(path) + try: + reply = self.rpc(124, struct.pack('<I', fid)) + finally: + self.close(fid) + size = struct.unpack_from('<H', reply, 2)[0] + record = reply[4:4 + size] + qid = record[6:19] + mode, _, mtime, length = struct.unpack_from('<IIIQ', record, 19) + name_len = struct.unpack_from('<H', record, 39)[0] + name = record[41:41 + name_len].decode() + return {'name': name, 'mode': mode & 0o777, 'dir': bool(mode & 0x80000000), + 'length': length, 'mtime': mtime, 'qid': qid} def write(self, path, data, truncate=False): fid = self.open(path, 1 | (16 if truncate else 0)) diff --git a/test/output.zig b/test/output.zig index 21e9f04c..07a1ce23 100644 --- a/test/output.zig +++ b/test/output.zig @@ -575,6 +575,12 @@ test "EffectCode lists readable virtual sources used by this backend" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); + if (!pardes.memory.limits.embedded_sources) { + const before = p.active; + try std.testing.expect(p.executeBuiltinLine(0, "EffectCode PanelSlide")); + try std.testing.expectEqual(before, p.active); + return error.SkipZigTest; + } try std.testing.expect(p.executeBuiltinLine(0, "EffectCode PanelSlide")); const out = p.panes[p.active].?.file.?; try std.testing.expectEqualStrings(config.effect_code_buffer, std.fs.path.basename(out.path)); @@ -1035,10 +1041,10 @@ test "identical result refresh keeps warm content caches without allocation" { const style_ptr = file.highlights.ptr; const highlight_start = file.highlight_start; const selected = result.vsel; - const event_node = pardes.filesystem.Node.of(result.serial, .event); - const opened = pardes.filesystem.handle(p, .{ .tag = 0, .op = .open, .node = event_node }); + const event_node = pardes.ctlfs.Node.of(result.serial, .event); + const opened = pardes.ctlfs.handle(p, .{ .tag = 0, .op = .open, .node = event_node }); try std.testing.expectEqual(.ok, opened.status); - defer _ = pardes.filesystem.handle(p, .{ .tag = 0, .op = .release, .node = event_node, .handle = opened.handle }); + defer _ = pardes.ctlfs.handle(p, .{ .tag = 0, .op = .release, .node = event_node, .handle = opened.handle }); for (0..32) |_| { const incoming = try p.gpa.dupe(u8, text); diff --git a/test/panes.zig b/test/panes.zig index 81015356..d3c29856 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -834,10 +834,10 @@ const ClickCursorTests = struct { for (0..2) |_| p.update(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = x + 2, .row = y + 4 } }); try std.testing.expectEqual(@as(usize, 2), pane.file.?.scroll); _ = try p.render(frame.allocator()); - const event_node = filesystem.Node.of(pane.serial, .event); - const opened = filesystem.handle(p, .{ .tag = 0, .op = .open, .node = event_node }); + const event_node = pardes.ctlfs.Node.of(pane.serial, .event); + const opened = pardes.ctlfs.handle(p, .{ .tag = 0, .op = .open, .node = event_node }); try std.testing.expectEqual(.ok, opened.status); - defer _ = filesystem.handle(p, .{ .tag = 0, .op = .release, .node = event_node, .handle = opened.handle }); + defer _ = pardes.ctlfs.handle(p, .{ .tag = 0, .op = .release, .node = event_node, .handle = opened.handle }); for ([_]pardes.Mouse.Button{ .middle, .right }) |button| { const events = &p.fs.panes[0].events; @@ -1907,7 +1907,7 @@ test "file history allocation precedes a filesystem read with side effects" { const original = p.panes[0].?; const serial = p.next_serial; failing.fail_index = failing.alloc_index + 2; - try std.testing.expectError(error.OutOfMemory, panes.File.open(p, 1, "/n/self/new/ctl", 0)); + try std.testing.expectError(error.OutOfMemory, panes.File.open(p, 1, "/n/self/new", 0)); try std.testing.expect(failing.has_induced_failure); try std.testing.expectEqual(original, p.panes[0].?); try std.testing.expectEqual(serial, p.next_serial); diff --git a/test/pdf_scroll_bench.zig b/test/pdf_scroll_bench.zig index 31aab270..33dfe70a 100644 --- a/test/pdf_scroll_bench.zig +++ b/test/pdf_scroll_bench.zig @@ -547,9 +547,9 @@ fn verifyIdentity( " expected frames={d} images={d} visual={x:0>16} transport={x:0>16}\n" ++ " got frames={d} images={d} visual={x:0>16} transport={x:0>16}\n", .{ - name, round, want.frames, want.images, - want.visual, want.transport, got.frames, got.images, - got.visual, got.transport, + name, round, want.frames, want.images, + want.visual, want.transport, got.frames, got.images, + got.visual, got.transport, }, ); return error.UnstableIdentity; @@ -569,8 +569,8 @@ fn reportText(config: Config, travel: Travel, results: []const Result) void { std.debug.print( "{s}: {d} pages, {d}px strip at {d}x{d} cells of {d}x{d}px\n", .{ - config.path, travel.pages, travel.strip_px, bench_cols, - bench_rows, bench_cell_pixels.w, bench_cell_pixels.h, + config.path, travel.pages, travel.strip_px, bench_cols, + bench_rows, bench_cell_pixels.w, bench_cell_pixels.h, }, ); std.debug.print( @@ -578,9 +578,9 @@ fn reportText(config: Config, travel: Travel, results: []const Result) void { .{ config.warmup, config.reps }, ); std.debug.print("{s:<16} {s:>6} {s:>7} {s:>10} {s:>10} {s:>10} {s:>10} {s:>10} {s:>11} {s:>7} {s:>5} {s:<16} {s}\n", .{ - "scenario", "frames", "notches", "min", - "median", "p90", "p99", "max", - "total_us", "rasters", "kept", "visual", + "scenario", "frames", "notches", "min", + "median", "p90", "p99", "max", + "total_us", "rasters", "kept", "visual", "transport", }); std.debug.print("{s}\n", .{"-" ** 160}); @@ -618,7 +618,7 @@ fn reportJson(io: std.Io, config: Config, travel: Travel, results: []const Resul "{{\"benchmark\":\"pardes-pdf-scroll\",\"build\":\"{s}\",\"path\":\"{s}\"," ++ "\"pages\":{d},\"strip_px\":{d},\"warmup\":{d},\"reps\":{d},\"results\":[", .{ - bench_config.core_optimize, config.path, travel.pages, + bench_config.core_optimize, config.path, travel.pages, travel.strip_px, config.warmup, config.reps, }, ) catch return; diff --git a/test/snapshot.zig b/test/snapshot.zig index a7a1ba48..7fa84a86 100644 --- a/test/snapshot.zig +++ b/test/snapshot.zig @@ -1151,7 +1151,7 @@ const Screen9p = struct { fn snap9p(arena: std.mem.Allocator, h: *eh.Harness, label: []const u8, prev: *Prev) ![]u8 { var dial_buf: [24]u8 = undefined; const dial = try std.fmt.bufPrint(&dial_buf, "{d}", .{h.pid}); - const bytes = try Client.read(gpa, dial, "/self/screen", "/self/screen"); + const bytes = try Client.read(gpa, dial, "/screen", "/screen"); defer gpa.free(bytes); const parsed = try std.json.parseFromSlice(Screen9p, gpa, bytes, .{}); defer parsed.deinit(); diff --git a/test/tag_cleanup.py b/test/tag_cleanup.py index 52ee8e9d..7b7d8bbf 100644 --- a/test/tag_cleanup.py +++ b/test/tag_cleanup.py @@ -28,7 +28,7 @@ def run(args): launch=launch, inherited=inherited, terminal=terminal) as (client, _): keys = HostInput(terminal['input_fd']) pdf = next(serial for serial, tag in panes(client).items() if str(source) in tag) - pdf_tag = client.read(f'/self/pane/{pdf}/tag').decode().strip() + pdf_tag = client.read(f'/pane/{pdf}/tag').decode().strip() require(pdf_tag.startswith(str(source) + ' [1/'), 'PDF name/page prefix missing') require(pdf_tag.endswith('] Tty Del PdfSections PdfTint Collapse'), 'PDF commands missing') require(all(word not in pdf_tag for word in ['PdfFit', 'filtered', 'full', 'disabled']), 'PDF tag contains fit or tint-state clutter') @@ -36,7 +36,7 @@ def run(args): before = set(panes(client)) execute(client, control, 'Tty') tty = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)), 'terminal missing') - client.write(f'/self/pane/{control}/ctl', b'delete\n') # owned command-only scratch + client.write(f'/pane/{control}/ctl', b'exec Del\n') # owned command-only scratch def tag_position(): frame = client.screen() @@ -64,11 +64,11 @@ def run(args): keys.send(b'\t') require(wait_for(lambda: (p := tag_position()) and p[2] == ' '), 'Mode from edited tag ignored parked raw mode') keys.send(b'\x02') - client.write(f'/self/pane/{tty}/pty/data', b"printf 'Mode toggle ready\\n'\r") - require(wait_for(lambda: b'Mode toggle ready' in client.read(f'/self/pane/{tty}/body')), 'terminal input failed') + client.write(f'/pane/{tty}/pty/data', b"printf 'Mode toggle ready\\n'\r") + require(wait_for(lambda: b'Mode toggle ready' in client.read(f'/pane/{tty}/body')), 'terminal input failed') capture(client, root, 'pdf-and-tty-tags', not args.tty) require(source.read_bytes() == original, 'PDF file changed') - client.write(f'/self/pane/{tty}/pty/data', b'exit\r') + client.write(f'/pane/{tty}/pty/data', b'exit\r') print('PDF tag, Mode click, Ctrl-B, edited-tag execution and source preservation passed') diff --git a/test/ui_review.py b/test/ui_review.py index df492eac..2b6faa29 100644 --- a/test/ui_review.py +++ b/test/ui_review.py @@ -21,7 +21,7 @@ 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()} + for row in client.read('/index').decode().splitlines()} def wait_for(fn, seconds=12): deadline = time.monotonic() + seconds @@ -34,7 +34,7 @@ def wait_for(fn, seconds=12): def command(client, serial, text): """Execute a temporary tag command without changing source contents.""" - tag = f'/self/pane/{serial}/tag' + tag = f'/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' ': @@ -42,13 +42,13 @@ def command(client, serial, text): 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(f'/pane/{serial}/event', f'Mx{start} {start + len(text.encode())}\n'.encode()) + client.write(f'/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) + client.read(f'/pane/{serial}/body').strip()), None) def require(value, description): if not value: @@ -63,7 +63,7 @@ def capture(client, root, label, gui): 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()) + (root / (label + '-index.txt')).write_text(client.read('/index').decode()) if gui: require(wait_for(lambda: (root / 'latest.ppm').exists(), 8), 'GPU capture missing') def convert_frame(): @@ -106,28 +106,28 @@ def run(args): 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') + require(b'crds_value.rs' in client.read(f'/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') + (root / 'find-results.txt').write_bytes(client.read(f'/pane/{find_pane}/body')) + client.write(f'/pane/{find_pane}/ctl', b'exec 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') + require(b'CrdsValueLabel' in client.read(f'/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')) + (root / 'grep-results.txt').write_bytes(client.read(f'/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)), + if 'pty' in client.list(f'/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") + client.write(f'/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'')), + client.read(f'/pane/{terminal}/body').replace(b'\r', b'')), 'Terminal command output was not observed') report['terminal'] = True capture(client, root, '04-terminal', not args.tty) @@ -137,46 +137,46 @@ def run(args): 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')) + (root / 'symbols-results.txt').write_bytes(client.read(f'/pane/{symbols}/body')) capture(client, root, '05-symbols', not args.tty) if symbols: - client.write(f'/self/pane/{symbols}/ctl', b'del\n') + client.write(f'/pane/{symbols}/ctl', b'exec 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') + client.write(f'/pane/{source}/addr', f'#{offset},#{offset}'.encode()) + client.write(f'/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')) + (root / 'hover-results.txt').write_bytes(client.read(f'/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()) + client.write(f'/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') + info = client.read(f'/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') + client.write(f'/pane/{lsp}/ctl', b'exec 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() + (root / f'pane-{serial}.txt').write_bytes(client.read(f'/pane/{serial}/body')) + assert client.read(f'/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') + client.write(f'/pane/{terminal}/pty/data', b'exit\r') (root / 'report.json').write_text(json.dumps(report, indent=2)) print('artifacts', root, flush=True) diff --git a/test/v9fs.py b/test/v9fs.py index 7dc88195..16916d0b 100644 --- a/test/v9fs.py +++ b/test/v9fs.py @@ -42,25 +42,26 @@ def worker(mountpoint, socket, uid, gid, original_namespace): assert len(mounted) == 1 and ' - 9p ' in mounted[0], mounted assert not any(field.startswith(('shared:', 'master:')) for field in mounted[0].split()[6:]) - assert set(os.listdir(mountpoint)) == {'os', 'self'} - tree = mountpoint / 'self' - assert {'index', 'pane', 'new', 'screen'} <= set(os.listdir(tree)) + tree = mountpoint + assert {'os', 'index', 'pane', 'new', 'ctl', 'log', 'screen', 'README'} <= set(os.listdir(tree)) + assert 'self' not in os.listdir(tree) # A direct connection provides independent evidence for VFS reads/writes. with Client(socket) as client: - assert (tree / 'index').read_bytes() == client.read('/self/index') + assert (tree / 'index').read_bytes() == client.read('/index') assert (tree / 'pane/1/body').read_bytes() == b'initial\n' - before = client.read('/self/index') - subprocess.run(['ls', '-l', str(tree / 'new')], check=True, capture_output=True, timeout=5) - assert {'README', 'ctl', 'body'} <= set(os.listdir(tree / 'new')) - assert (tree / 'new/README').read_bytes() == (tree / 'README').read_bytes() - assert client.read('/self/index') == before, 'browsing created panes' - serial = int((tree / 'new/ctl').read_bytes().split()[0]) - another = int((tree / 'new/ctl').read_bytes().split()[0]) + before = client.read('/index') + subprocess.run(['ls', '-l', str(tree), str(tree / 'pane' / '1')], check=True, capture_output=True, timeout=5) + subprocess.run(['find', str(tree / 'pane'), str(tree / 'new'), '-ls'], check=True, capture_output=True, timeout=5) + assert (tree / 'README').read_bytes() == client.read('/README') + assert (tree / 'new').stat().st_size == 0 + assert client.read('/index') == before, 'browsing created panes' + serial = int((tree / 'new').read_bytes().split()[0]) + another = int((tree / 'new').read_bytes().split()[0]) assert serial != another, 'cached factory reused a pane' - client.write(f'/self/pane/{another}/ctl', b'delete\n') + client.write(f'/pane/{another}/ctl', b'exec Del\n') pane = tree / 'pane' / str(serial) - wire = f'/self/pane/{serial}' + wire = f'/pane/{serial}' assert str(serial) in os.listdir(tree / 'pane') assert serial in [int(row.split()[0]) for row in (tree / 'index').read_bytes().splitlines()] @@ -76,7 +77,7 @@ def worker(mountpoint, socket, uid, gid, original_namespace): assert client.read(wire + '/body') == b'v9fs update\nappended\n' # Exercise an actual shell redirection, including its O_TRUNC open. - subprocess.run(['/bin/sh', '-c', 'printf "name kernel-probe\\n" > "$1/ctl"', + subprocess.run(['/bin/sh', '-c', 'printf "kernel-probe\\n" > "$1/name"', 'v9fs-probe', str(pane)], check=True, timeout=5) tag = client.read(wire + '/tag') assert tag.split(maxsplit=1)[0] == str(socket.parent / 'kernel-probe').encode(), tag @@ -100,7 +101,7 @@ def worker(mountpoint, socket, uid, gid, original_namespace): # Reading OS files through the exported tree does not recurse through # the mount: the core still lives in the supervisor's namespace. assert (mountpoint / 'os' / str(socket.parent).lstrip('/') / 'kernel.txt').read_bytes() == b'initial\n' - write_existing(pane / 'ctl', b'delete\n') + write_existing(pane / 'ctl', b'exec Del\n') assert serial not in [int(row.split()[0]) for row in (tree / 'index').read_bytes().splitlines()] print('v9fs: namespace isolation, privilege drop, inherited mount, directory refresh, ' @@ -154,7 +155,7 @@ def run(binary, helper): print(run_helper(command), end='') assert os.readlink('/proc/self/ns/mnt') == namespace assert list(target.iterdir()) == [], 'mount escaped its private namespace' - assert client.read('/self/pane/1/body') == b'initial\n', 'core stopped serving after probe exit' + assert client.read('/pane/1/body') == b'initial\n', 'core stopped serving after probe exit' print('v9fs: supervisor namespace unchanged and session cleanup passed') diff --git a/test/v9fs_terminal.py b/test/v9fs_terminal.py index cc273a40..b49246d1 100644 --- a/test/v9fs_terminal.py +++ b/test/v9fs_terminal.py @@ -74,7 +74,7 @@ sys.exit(1) execute(client, control, 'Tty9p') until(report_path.exists, 'Tty9p did not reach the sudo stand-in') report = json.loads(report_path.read_text()) - pane = f"/self/pane/{report['pane']}" + pane = f"/pane/{report['pane']}" until(lambda: b'V9FS_AUTH_READY' in client.read(pane + '/body'), 'prompt not visible') assert report['socket'] == str(address) assert report['argv'][:2] == ['-E', '--'], report @@ -85,7 +85,7 @@ sys.exit(1) assert report['path'] == path assert report['shell'] == str(Path(shell).resolve()), report # The detached core remains responsive during authentication. - assert client.read('/self/pane/1/body') == b'initial\n' + assert client.read('/pane/1/body') == b'initial\n' target = Path(report['mount']) assert target.is_dir() and list(target.iterdir()) == [] if interrupted: @@ -100,7 +100,7 @@ sys.exit(1) client.write(pane + '/pty/data', b'printf "OUTER_READY:%s\\n" "$PARDES_PANE"\r') expected = ('OUTER_READY:' + report['pane']).encode() until(lambda: expected in client.read(pane + '/body'), 'original shell did not remain usable') - assert client.read('/self/pane/1/body') == b'initial\n' + assert client.read('/pane/1/body') == b'initial\n' print('Tty9p: shell-first startup, quoted paths, hidden password input, failure recovery and cleanup passed') diff --git a/test/window_opacity.py b/test/window_opacity.py index 8abe8677..b9529373 100644 --- a/test/window_opacity.py +++ b/test/window_opacity.py @@ -49,9 +49,9 @@ def run(args): command(client, source, 'Tty') terminal = require(wait_for(lambda: next((serial for serial in panes(client) if serial not in before), None)), 'terminal missing') - client.write(f'/self/pane/{terminal}/pty/data', + client.write(f'/pane/{terminal}/pty/data', b"PS1='$ '; printf '\\033[2J\\033[H\\033[36mTerminal opacity sample\\033[0m\\n'\r") - require(wait_for(lambda: b'Terminal opacity sample' in client.read(f'/self/pane/{terminal}/body')), + require(wait_for(lambda: b'Terminal opacity sample' in client.read(f'/pane/{terminal}/body')), 'terminal output missing') def output(builtin): @@ -59,8 +59,8 @@ def run(args): command(client, source, builtin) pane = require(wait_for(lambda: next((serial for serial in panes(client) if serial not in before), None)), builtin + ' output missing') - text = client.read(f'/self/pane/{pane}/body').decode() - client.write(f'/self/pane/{pane}/ctl', b'del\n') + text = client.read(f'/pane/{pane}/body').decode() + client.write(f'/pane/{pane}/ctl', b'exec Del\n') return text help_text = output('Help') @@ -208,9 +208,9 @@ def run(args): require(all(int(value) == 0xffffffff for value in multipliers), 'whole-window alpha modifier would fade foreground text') require('WindowOpacity: 100%\n' in output('Config'), '100% opacity not restored') - require(source_path.read_bytes() == original == client.read(f'/self/pane/{source}/body'), + require(source_path.read_bytes() == original == client.read(f'/pane/{source}/body'), 'source contents changed') - client.write(f'/self/pane/{terminal}/pty/data', b'exit\r') + client.write(f'/pane/{terminal}/pty/data', b'exit\r') report.update(source_unchanged=True, core_rgb_and_text_unchanged=True, invalid_value_ignored=True, help_updated=True, restored_opaque=True) (root / 'report.json').write_text(json.dumps(report, indent=2) + '\n') |
