From c1990b3e6e196ad41379aa432bf3ccca8a65d9f0 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 20 Sep 2026 00:36:50 -0300 Subject: 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//... /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 --- test/agent_session.py | 14 +- test/appearance_gallery.py | 26 ++-- test/collapse_layout.py | 8 +- test/column_tags.py | 32 ++-- test/font_size.py | 2 +- test/fs.py | 367 +++++++++++++++++++++++++++------------------ test/fs_bench.zig | 43 +++--- test/fs_namespace.zig | 12 +- test/fs_soak.py | 29 ++-- test/look_placement.py | 8 +- test/ninep.py | 39 ++++- test/output.zig | 12 +- test/panes.zig | 8 +- test/pdf_scroll_bench.zig | 18 +-- test/snapshot.zig | 2 +- test/tag_cleanup.py | 10 +- test/ui_review.py | 50 +++--- test/v9fs.py | 33 ++-- test/v9fs_terminal.py | 6 +- test/window_opacity.py | 12 +- 20 files changed, 430 insertions(+), 301 deletions(-) (limited to 'test') 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 diff --git a/test/fs.py b/test/fs.py index 2ffd5098..819a78d6 100644 --- a/test/fs.py +++ b/test/fs.py @@ -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(' 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(' [--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('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') -- cgit v1.3