summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-21 20:07:43 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit9070942b29bd10dddcdecdb0e88ba0fb40608467 (patch)
treefa1fc5984c7847c52bf4e78d277586edce2d8325 /test
parent0122e94fb37422085325f2dc78ca015051ce5f91 (diff)
downloadpardes-9070942b29bd10dddcdecdb0e88ba0fb40608467.tar.gz
pardes-9070942b29bd10dddcdecdb0e88ba0fb40608467.zip
Plan 9 idiom for the control filesystem, and the regressions a624a56 left
The 9P tree stops being a command language wearing a filesystem. /new created a pane as a side effect of a *read*; it is now Tcreate in /pane, with Tremove to close, which cloud9's engine has always supported and the editor never declared: tree.zig now says `features = .{ .create = true, .remove = true }`. Eleven pane ctl verbs become files that can be read as well as written -- dot, limit, dirty, mark, scroll, look, exec -- leaving ctl with `get`, the one verb no file would say better. Root /ctl splits into a read-only /status and the /look and /exec files whose write IS the click. stat carries real sizes where it used to answer 0, and qid versions track a pane's revision, so a client can poll for change without re-reading the body. Commit a624a56 moved raw-tty keys to an early-return branch that knew only Ctrl-B and bare Escape, and in the same edit deleted the paste branch below it. That cost Shift-Escape (the unconditional way out of tty mode) and both paste chords: Ctrl-V and Ctrl-Shift-V reached the child as keystrokes, so an agent CLI running in a pane took Ctrl-V for its image-paste binding and answered "No image found in clipboard". Both are restored, with tests. Nested detection was not subtly broken but deleted: 60367d8 removed nested.zig's process-ancestry walk and left "am I inside pardes" derived from PARDES_FORWARD_LOOK, which read "0" both for --nested and for "the listener did not come up". PARDES_PID now answers that question on its own, checked with kill(pid, 0); PARDES_9P and PARDES_PANE answer how to reach it; the flag is gone. The posted-9P registry also self-heals now -- a session that aborts cannot unlink its own socket, so posting sweeps entries whose target refuses a connection, symlinks only and on a definite ECONNREFUSED only. Elsewhere: tty scrolling is sticky-bottom, following new output only from the last row, with typing and entering raw mode snapping back to live; the boot layouts are a Boot enum instead of a chain of ifs, and the bare tty startup (Boot.tty, which main.zig names) opens an empty text pane under the shell while tests keep Boot.tty_shell; builtins announce themselves on the message row under a Verbose setting that is on by default; Config prints each setting the way you would type it back, so WindowOpacity 70 rather than "WindowOpacity: 70%"; LocationsConfig opens its window only when called bare; every tagline puts the word that closes the thing last, and a column now outlives its panes -- closing the last one leaves an empty pane, and only Delcol, newly on the column tagline, takes the column away. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'test')
-rw-r--r--test/appearance_gallery.py14
-rw-r--r--test/column_tags.py4
-rw-r--r--test/e2e_harness.zig2
-rw-r--r--test/font_size.py6
-rw-r--r--test/fs.py134
-rw-r--r--test/fs_bench.zig4
-rw-r--r--test/fs_namespace.zig15
-rw-r--r--test/fs_soak.py8
-rw-r--r--test/look_placement.py6
-rw-r--r--test/ninep.py19
-rw-r--r--test/output.zig32
-rw-r--r--test/panes.zig76
-rw-r--r--test/pdf.zig2
-rw-r--r--test/tag_cleanup.py2
-rw-r--r--test/ui_review.py12
-rw-r--r--test/v9fs.py35
-rw-r--r--test/window_opacity.py2
17 files changed, 250 insertions, 123 deletions
diff --git a/test/appearance_gallery.py b/test/appearance_gallery.py
index 0fa12136..48c0ea1b 100644
--- a/test/appearance_gallery.py
+++ b/test/appearance_gallery.py
@@ -82,7 +82,7 @@ def run(args):
right = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)), 'right column missing')
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')
+ client.write(f'/pane/{right}/dirty', b'0\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')
@@ -94,12 +94,12 @@ def run(args):
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'/pane/{pane}/body').decode()
- client.write(f'/pane/{pane}/ctl', b'exec Del\n')
+ client.write(f'/pane/{pane}/exec', b'Del\n')
return text
settings = config_report()
- require('Font effective size: 18.00 pixels' in settings, 'configured font size not applied: ' + settings)
- require('TaglineSize: 80' in settings, 'configured tag size missing')
+ require('Font effective size 18.00 pixels' in settings, 'configured font size not applied: ' + settings)
+ require('TaglineSize 80' in settings, 'configured tag size missing')
(root / 'effective-config.txt').write_text(settings)
report['font_effective_pixels'] = 18
report['tagline_percent'] = 80
@@ -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'/pane/{source}/ctl', f'look {source_path}:117\n'.encode())
+ client.write(f'/pane/{source}/look', f'{source_path}:117\n'.encode())
take_frame('classic-' + theme)
if args.theme_selector:
frame = client.screen()
@@ -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'/pane/{selector}/ctl', b'exec Del\n')
+ client.write(f'/pane/{selector}/exec', b'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'/pane/{source}/ctl', f'look {source_path}:1\n'.encode())
+ client.write(f'/pane/{source}/look', f'{source_path}:1\n'.encode())
take_frame('pet-' + pet)
if pet != 'off':
frame = client.screen()
diff --git a/test/column_tags.py b/test/column_tags.py
index 80b623bd..fe3a8699 100644
--- a/test/column_tags.py
+++ b/test/column_tags.py
@@ -143,7 +143,7 @@ def run(args):
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'/pane/{right}/body', right_body)
- client.write(f'/pane/{right}/ctl', b'clean\n')
+ client.write(f'/pane/{right}/dirty', b'0\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')
@@ -173,7 +173,7 @@ def run(args):
extra = require(wait_for(lambda: new_serial(client, before)), 'right-column New did not create a pane')
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'/pane/{extra}/ctl', b'exec Del\n')
+ client.write(f'/pane/{extra}/exec', b'Del\n')
checks.append('middle-click New targets the inactive right column')
# An owned short-path file makes its complete tag visible. Pane-tag
diff --git a/test/e2e_harness.zig b/test/e2e_harness.zig
index f7f380df..5a181b78 100644
--- a/test/e2e_harness.zig
+++ b/test/e2e_harness.zig
@@ -92,7 +92,7 @@ pub const Harness = struct {
const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
const pid = forkpty(&master, null, null, &ws);
if (pid == 0) {
- for ([_][*:0]const u8{ "PARDES_9P", "PARDES_PANE", "PARDES_FORWARD_LOOK" }) |name|
+ for ([_][*:0]const u8{ "PARDES_PID", "PARDES_9P", "PARDES_PANE" }) |name|
if (unsetenv(name) != 0) _exit(126);
_ = execvp(exe, argv.ptr);
_exit(127);
diff --git a/test/font_size.py b/test/font_size.py
index 3a0a9a28..b7104099 100644
--- a/test/font_size.py
+++ b/test/font_size.py
@@ -30,16 +30,16 @@ def run(args):
for serial, tag in panes(client).items():
if '/+Config ' in tag:
body = client.read(f'/pane/{serial}/body').decode()
- if f'Font effective size: {size:.2f} pixels' in body:
+ if f'Font effective size {size:.2f} pixels' in body:
return body
return None
return require(wait_for(ready), f'Font did not become {size} pixels')
initial = report(18)
- require('Font requested size: 18.00' in initial, 'startup size suffix ignored')
+ require('Font requested size 18.00' in initial, 'startup size suffix ignored')
require(args.font in initial, 'requested font missing')
execute(client, control, f'Font {args.font}:20.5')
- require('Font requested size: 20.50' in report(20.5), 'fractional size ignored')
+ require('Font requested size 20.50' in report(20.5), 'fractional size ignored')
execute(client, control, f'Font {args.font}')
report(20.5) # no suffix preserves current size
execute(client, control, f'Font {args.font}:900')
diff --git a/test/fs.py b/test/fs.py
index 819a78d6..c3ffa440 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -100,8 +100,15 @@ def session(binary, root, name, *options, socket_name=None, file=None, tty=False
assert not reader.is_alive(), 'terminal reader did not stop'
+def newest(client):
+ """The newest pane's serial: /index is ordered by serial."""
+ return int(client.read('/index').splitlines()[-1].split()[0])
+
+
def new_pane(client, contents):
- serial = int(client.read('/new').split()[0])
+ """mkdir in /pane; the editor names the directory after the new serial."""
+ client.mkdir('/pane', 'scratch')
+ serial = newest(client)
if contents:
client.write(f'/pane/{serial}/body', contents)
return serial
@@ -110,18 +117,18 @@ def new_pane(client, contents):
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'/pane/{serial}/ctl', f'look {path}\n'.encode())
- results = [int(row) for row in client.read('/ctl').split()]
+ client.write(f'/pane/{serial}/look', f'{path}\n'.encode())
+ results = [int(row) for row in client.read('/look').split()]
assert len(results) == 1 and results[0] != serial, (path, results)
return results[0]
def execute(client, serial, command):
"""A middle click on `command` from a pane."""
- client.write(f'/pane/{serial}/ctl', f'exec {command}\n'.encode())
+ client.write(f'/pane/{serial}/exec', f'{command}\n'.encode())
-def walk_tree(client, path='/', skip=('/new', '/log', '/os', '/screen')):
+def walk_tree(client, path='/', skip=('/log', '/os', '/screen')):
"""Stat every entry below `path`, as `find` does, without opening anything."""
seen = []
for name in client.list(path):
@@ -138,18 +145,21 @@ def discovery(binary, embedded=False):
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 top[:10] == ['README', 'index', 'status', 'look', 'exec', 'log', 'screen',
+ 'listeners', 'pane', 'os'], top
+ assert 'self' not in top and 'cons' not in top and 'new' 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 guide.count(b'\n') <= 45 and b'Msg hello' in guide and b'mkdir' 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 {'/index', '/status', '/look', '/exec', '/log', '/pane/1', '/pane/1/body',
+ '/pane/1/name', '/pane/1/sel', '/pane/1/ctl', '/pane/1/tag', '/pane/1/event',
+ '/pane/1/dot', '/pane/1/limit', '/pane/1/dirty', '/pane/1/mark',
+ '/pane/1/scroll', '/pane/1/look', '/pane/1/exec'} <= 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)
@@ -159,18 +169,28 @@ def discovery(binary, embedded=False):
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 index['mode'] == 0o444 and client.stat('/status')['mode'] == 0o444
+ assert client.stat('/exec')['mode'] == 0o666
assert client.stat('/pane/1/body')['mode'] == 0o666 and client.stat('/pane')['mode'] == 0o755
+ # A range file reads the pair it takes; a flag file reads 0 or 1.
+ assert client.stat('/pane/1/addr')['length'] == 24
+ assert client.stat('/pane/1/limit')['length'] == 0
+ assert client.read('/pane/1/dirty') == b'0\n'
+ assert client.read('/pane/1/scroll') == b'1\n'
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.
+ # /status reports the editor; /exec has nothing to report yet.
+ status = dict(line.split(maxsplit=1) for line in client.read('/status').decode().splitlines())
+ assert int(status['pid']) > 0 and status['version'] and int(status['panes']) >= 1, status
+ assert client.read('/exec') == b''
+ # mkdir makes a pane, named by the serial the editor gives it.
log = client.open('/log')
- first = int(client.read('/new').split()[0])
- second = int(client.read('/new').split()[0])
+ client.mkdir('/pane', 'one')
+ first = newest(client)
+ client.mkdir('/pane', 'two')
+ second = newest(client)
assert first != second and first != fixture, (first, second)
+ assert 'one' not in client.list('/pane') and 'two' not in client.list('/pane')
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)}
@@ -186,51 +206,63 @@ def discovery(binary, embedded=False):
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')
+ # Copying addr onto dot selects that range; sel reads and replaces it.
client.write(f'/pane/{first}/addr', b'#0,#5')
- client.write(f'/pane/{first}/ctl', b'dot=addr\n')
+ client.write(f'/pane/{first}/dot', client.read(f'/pane/{first}/addr'))
+ assert client.read(f'/pane/{first}/dot') == client.read(f'/pane/{first}/addr')
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')
+ # exec Msg shows text; exec Save writes the named file; rmdir closes.
+ client.write(f'/pane/{first}/exec', b'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 client.read(f'/pane/{first}/dirty') == b'1\n'
+ client.write(f'/pane/{first}/exec', b'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('/exec') == f'{first}\n'.encode()
+ assert client.read(f'/pane/{first}/dirty') == b'0\n'
+ client.remove(f'/pane/{second}')
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.
+ # look 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'))
+ client.write('/look', f'{root}/opened.txt:2\n'.encode())
+ opened = int(client.read('/look'))
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)}
+ client.write(f'/pane/{first}/look', f'{root}/opened.txt:3\n'.encode())
+ assert client.read('/look') == f'{opened}\n'.encode(), 'a repeated look targets the open pane'
+ assert {str(fixture), str(first), str(opened)} <= set(client.list('/pane'))
# 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']:
+ # A command line may not carry control characters, and nothing else
+ # in the tree may be created or removed.
+ refused = [lambda: client.write('/exec', b'Msg a\x00b'),
+ lambda: client.write('/status', b'anything\n'),
+ lambda: client.mkdir('/', 'x'),
+ lambda: client.create('/pane', 'plain-file'),
+ lambda: client.remove('/pane/1/body'),
+ lambda: client.remove('/index')]
+ for index, attempt in enumerate(refused):
try:
- client.write('/ctl', bad)
+ attempt()
except OSError:
pass
else:
- raise AssertionError(f'{bad!r} was accepted')
+ raise AssertionError(f'refusal {index} 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')
+ print('9P discovery: listing/stat/find are inert; create, remove, look, exec, name, sel and log behave')
def test(binary, quic=False):
@@ -266,7 +298,7 @@ def test(binary, quic=False):
assert not (root / 'pardes-9p-renamed.sock').exists()
assert named.read('/pane/1/body') == b'initial\n'
with session(binary, root, 'first') as (client, address):
- assert {'pane', 'index', 'new', 'ctl', 'log', 'os'} <= set(client.list('/'))
+ assert {'pane', 'index', 'status', 'look', 'exec', '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'
@@ -346,7 +378,7 @@ def test(binary, quic=False):
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')
+ remote.write(f'/pane/{opened}/exec', b'Save\n')
assert client.read(body) == b'saved to peer\n'
large = b'mounted file contents\n' * 60000
@@ -356,7 +388,7 @@ def test(binary, quic=False):
assert remote.read(f'/pane/{opened}/body') == large
edited = large[:-3] + b'edited\n'
remote.write(f'/pane/{opened}/body', edited, truncate=True)
- remote.write(f'/pane/{opened}/ctl', b'exec Save\n')
+ remote.write(f'/pane/{opened}/exec', b'Save\n')
assert large_path.read_bytes() == edited
assert client.read('/os' + str(large_path)) == edited
@@ -368,12 +400,12 @@ def test(binary, quic=False):
execute(remote, control, 'Mount peer ' + str(own_address))
execute(remote, control, 'Unmount peer')
remote.write(f'/pane/{opened}/body', b'runtime mounted Save\n', truncate=True)
- remote.write(f'/pane/{opened}/ctl', b'exec Save\n')
+ remote.write(f'/pane/{opened}/exec', b'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'/pane/{roots}/body')
- remote.write(f'/pane/{opened}/ctl', b'exec Del\n')
+ remote.write(f'/pane/{opened}/exec', b'Del\n')
execute(remote, control, 'Unmount peer')
remote.write(f'/pane/{roots}/ctl', b'get\n')
assert remote.read(f'/pane/{roots}/body') == b'/n/os/\n/n/self/\n'
@@ -385,7 +417,7 @@ def test(binary, quic=False):
file='/n/peer/pane/1/body:1') as (remote, _):
assert remote.read('/pane/1/body') == b'runtime mounted Save\n'
- client.write(f'/pane/{serial}/ctl', b'exec Del\n')
+ client.write(f'/pane/{serial}/exec', b'Del\n')
assert str(serial) not in client.list('/pane')
for tty in [False, True]:
@@ -424,7 +456,7 @@ def test(binary, quic=False):
file_id, control_id = restored_ids
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.read(f'/pane/{control_id}/body') == b'', 'commands ran through exec, 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'/pane/{opened}/body') == b'initial\n'
@@ -451,14 +483,12 @@ def test(binary, quic=False):
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'/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)
+ # The selection is dot; reading it takes no command at all.
+ actual = list(map(int, client.read(f'/pane/{opened}/dot').split()))
assert actual == selected, (word, actual, selected)
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')]:
+ limit = client.open('/pane/1/limit', 2)
+ for fid, text in [(addr, b'#0,#3'), (limit, b'#0,#3'), (addr, b'#0')]:
client.rpc(118, struct.pack('<IQI', fid, 0, len(text)) + text)
try:
for _ in range(8):
@@ -474,7 +504,7 @@ def test(binary, quic=False):
raise AssertionError('no-argument child cleared the parent address limit')
finally:
client.close(addr)
- client.close(ctl)
+ client.close(limit)
client.read('/pane/1/ctl')
control = new_pane(client, b'')
@@ -618,7 +648,7 @@ def quic_test(binary):
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'/pane/{opened}/ctl', b'exec Save\n')
+ remote.write(f'/pane/{opened}/exec', b'Save\n')
assert unix.read_fid(unix_body) == payload[:unix.msize - 11]
assert tcp.read('/pane/1/body') == payload
tcp.write('/pane/1/body', b'TCP to QUIC Get\n', truncate=True)
@@ -637,10 +667,10 @@ def quic_test(binary):
assert remote.read(f'/pane/{large_pane}/body') == large
edited = b'QUIC file saved\n'
remote.write(f'/pane/{large_pane}/body', edited, truncate=True)
- remote.write(f'/pane/{large_pane}/ctl', b'exec Save\n')
+ remote.write(f'/pane/{large_pane}/exec', b'Save\n')
assert fixture.read_bytes() == edited
for pane in [opened, screen_pane, large_pane]:
- remote.write(f'/pane/{pane}/ctl', b'exec Del\n')
+ remote.write(f'/pane/{pane}/exec', b'Del\n')
execute(remote, control, 'Unmount peer')
roots = look(remote, '/n', source=control)
assert remote.read(f'/pane/{roots}/body') == b'/n/os/\n/n/self/\n'
@@ -651,10 +681,10 @@ def quic_test(binary):
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')
+ unix.write(f'/pane/{own}/exec', b'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'/pane/{own}/ctl', b'exec Del\n')
+ unix.write(f'/pane/{own}/exec', b'Del\n')
execute(unix, control, 'Unmount own')
unix.close(unix_body)
tcp.close(tcp_body)
diff --git a/test/fs_bench.zig b/test/fs_bench.zig
index f9735ee2..74b7b5da 100644
--- a/test/fs_bench.zig
+++ b/test/fs_bench.zig
@@ -132,8 +132,8 @@ fn checkReply(s: *Session, req: ctlfs.Req, reply: ctlfs.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{ "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 };
+ const names = [_][]const u8{ "README", "index", "status", "look", "exec", "log", "screen", "listeners", "pane", "os" };
+ const nodes = [_]u64{ @intFromEnum(ctlfs.TopFile.README), @intFromEnum(ctlfs.TopFile.index), @intFromEnum(ctlfs.TopFile.status), @intFromEnum(ctlfs.TopFile.look), @intFromEnum(ctlfs.TopFile.exec), @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) {
diff --git a/test/fs_namespace.zig b/test/fs_namespace.zig
index 8b6f245e..3eba6639 100644
--- a/test/fs_namespace.zig
+++ b/test/fs_namespace.zig
@@ -245,22 +245,17 @@ test "virtual file opens do not request OS watches" {
while (p.nextEffect()) |effect| if (effect == .watch) return error.VirtualFileWatch;
}
-test "Look at the clone file reserves its own pane and failed opens release it" {
+test "Look at a virtual file opens one pane and failed opens release their slot" {
const p = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
- p.lookAt(0, "/virtual/new");
- var control_count: usize = 0;
- var scratch_count: usize = 0;
+ p.lookAt(0, "/virtual/index");
+ var opened: 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")) control_count += 1;
- if (file.output) |output| if (output.from == .cmd and output.from.cmd == .New) {
- scratch_count += 1;
- };
+ if (std.mem.eql(u8, file.path, "/virtual/index")) opened += 1;
}
- try std.testing.expectEqual(@as(usize, 1), control_count);
- try std.testing.expectEqual(@as(usize, 1), scratch_count);
+ try std.testing.expectEqual(@as(usize, 1), opened);
const free = p.freeSlot().?;
try std.testing.expectError(error.FileNotFound, pardes.panes.File.open(p, free, "/n/self/does-not-exist", 0));
try std.testing.expect(p.panes[free] == null);
diff --git a/test/fs_soak.py b/test/fs_soak.py
index 5569a7c7..a62f03e6 100644
--- a/test/fs_soak.py
+++ b/test/fs_soak.py
@@ -145,7 +145,7 @@ def run(args):
client.write(base + '/body', body, truncate=True)
path = root / f'pane-{slot}-0.zig'
client.write(base + '/name', f'{path}\n'.encode())
- client.write(base + '/ctl', b'exec Save\n')
+ client.write(base + '/exec', b'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',
@@ -187,7 +187,7 @@ def run(args):
pane['variant'] ^= 1
pane['path'] = root / f'pane-{slot}-{pane["variant"]}.zig'
client.write(base + '/name', f'{pane["path"]}\n'.encode())
- client.write(base + '/ctl', b'exec Save\n')
+ client.write(base + '/exec', b'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':
@@ -196,13 +196,13 @@ def run(args):
client.write(base + '/ctl', b'get\n')
elif operation == 'cycle':
old_serial = pane['serial']
- client.write(base + '/ctl', b'exec Del\n')
+ client.remove(base)
pane['body'] = payload(rng)
pane['serial'] = new_pane(client, pane['body'])
assert pane['serial'] != old_serial
base = f'/pane/{pane["serial"]}'
client.write(base + '/name', f'{pane["path"]}\n'.encode())
- client.write(base + '/ctl', b'exec Save\n')
+ client.write(base + '/exec', b'Save\n')
elif operation == 'reconnect':
client.socket.close()
client = Client(address)
diff --git a/test/look_placement.py b/test/look_placement.py
index 1341e776..8a31157f 100644
--- a/test/look_placement.py
+++ b/test/look_placement.py
@@ -74,7 +74,7 @@ def run_case(args, root, source_index):
require('pty' in client.list(pane), 'fixture contains an existing document')
marker = f'LP_{index}'
markers[serial] = marker
- client.write(pane + '/ctl', b'cleartag\n')
+ client.write(pane + '/tag', b'', truncate=True)
client.write(pane + '/tag', f' {marker} Tty Del Collapse'.encode())
# Establish non-pristine shells and a stable prompt before measuring.
client.write(pane + '/pty/data', f"printf 'ready-{index}\\n'\r".encode())
@@ -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'/pane/{source}/ctl', f'look {target}\n'.encode())
+ client.write(f'/pane/{source}/look', f'{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')
@@ -113,7 +113,7 @@ def run_case(args, root, source_index):
f'Look changed unrelated terminal {serial} rendered text')
require(client.read(f'/pane/{opened}/body') == contents,
'opened document contents differ')
- client.write(f'/pane/{source}/ctl', f'look {target}:2\n'.encode())
+ client.write(f'/pane/{source}/look', f'{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 6c625951..ac4e146b 100644
--- a/test/ninep.py
+++ b/test/ninep.py
@@ -97,6 +97,25 @@ class Client:
def close(self, fid):
self.rpc(120, struct.pack('<I', fid))
+ def create(self, path, name, perm=0o777, mode=0):
+ """Tcreate `name` in the directory `path`; returns the new qid."""
+ fid = self.walk(path)
+ try:
+ reply = self.rpc(114, struct.pack('<I', fid) + string(name) +
+ struct.pack('<IB', perm, mode))
+ if len(reply) != 17:
+ raise OSError('invalid create reply')
+ return reply[:13]
+ finally:
+ self.close(fid)
+
+ def mkdir(self, path, name):
+ return self.create(path, name, 0x80000000 | 0o777)
+
+ def remove(self, path):
+ """Tremove, which clunks the fid whether or not it succeeds."""
+ self.rpc(122, struct.pack('<I', self.walk(path)))
+
def read_fid(self, fid, offset=0, count=None):
count = min(count if count is not None else self.msize - 11, self.msize - 11)
reply = self.rpc(116, struct.pack('<IQI', fid, offset, count))
diff --git a/test/output.zig b/test/output.zig
index 07a1ce23..1fcb6104 100644
--- a/test/output.zig
+++ b/test/output.zig
@@ -543,10 +543,10 @@ test "Config prints the startup path and refreshes its one output" {
const out = p.panes[opened].?.file.?;
for ([_][]const u8{
"Startup config: " ++ path ++ "\n",
- "Theme: orchard\n",
- "Shell requested (new panes): " ++ config.default_shell ++ " (default)\n",
- "Shell effective (last spawn): (none)\n",
- "Shell pending: on\n",
+ "Theme orchard\n",
+ "Shell requested (new panes) " ++ config.default_shell ++ " (default)\n",
+ "Shell effective (last spawn) (none)\n",
+ "Shell pending on\n",
}) |line| try std.testing.expect(std.mem.indexOf(u8, out.content, line) != null);
try std.testing.expectEqualStrings(config.config_buffer, std.fs.path.basename(out.path));
try std.testing.expectEqual(panes.Output.Origin{ .cmd = .Config }, out.output.?.from);
@@ -653,36 +653,36 @@ test "every enabled setting builtin mutates the State Config reports" {
try std.testing.expect(p.executeBuiltinLine(p.active, "Config"));
const report = p.panes[p.active].?.file.?.content;
for ([_][]const u8{
- "Colors: off\n",
- "Wrap: off\n",
- "Tagbottom: on\n",
- "Debug: on\n",
- "Theme: acme\n",
- "Shell requested (new panes): fish\n",
+ "Colors off\n",
+ "Wrap off\n",
+ "Tagbottom on\n",
+ "Debug on\n",
+ "Theme acme\n",
+ "Shell requested (new panes) fish\n",
}) |line| try std.testing.expect(std.mem.indexOf(u8, report, line) != null);
const transition = if (builtins.capabilities.panel_transitions)
- try std.fmt.bufPrint(&buf, "Panel transition: {s}\n", .{
+ try std.fmt.bufPrint(&buf, "Panel transition {s}\n", .{
config.Runtime.findAction(.{ .transition = p.settings.panel_transition }).?.word,
})
else
- "Panel transition: unsupported\n";
+ "Panel transition unsupported\n";
try std.testing.expect(std.mem.indexOf(u8, report, transition) != null);
const scene_status = if (builtins.capabilities.scene_shaders) "on" else "unsupported";
for ([_][]const u8{ "Crt", "Ripple", "Glitch" }) |name| {
- const line = try std.fmt.bufPrint(&buf, "{s}: {s}\n", .{ name, scene_status });
+ const line = try std.fmt.bufPrint(&buf, "{s} {s}\n", .{ name, scene_status });
try std.testing.expect(std.mem.indexOf(u8, report, line) != null);
}
const tagline = if (!builtins.capabilities.tagline_font_size)
- "TaglineSize: unsupported\n"
+ "TaglineSize unsupported\n"
else
- try std.fmt.bufPrint(&buf, "TaglineSize: {d}%{s}\n", .{
+ try std.fmt.bufPrint(&buf, "TaglineSize {d}{s}\n", .{
p.settings.font.tagline_percent,
if (builtins.capabilities.font_picker) "" else " (build-time only)",
});
try std.testing.expect(std.mem.indexOf(u8, report, tagline) != null);
if (chosen_font) |name| {
try std.testing.expect(std.mem.indexOf(u8, report, name) != null);
- try std.testing.expect(std.mem.indexOf(u8, report, "Font pending: on\n") != null);
+ try std.testing.expect(std.mem.indexOf(u8, report, "Font pending on\n") != null);
}
}
diff --git a/test/panes.zig b/test/panes.zig
index d3c29856..e4e677e4 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -731,6 +731,72 @@ const TtySelectionTests = struct {
}
}
}
+
+ /// Every byte the pane's program would have read off its pty since the
+ /// last call, joined.
+ fn childInput(p: *Pardes, buf: []u8) []const u8 {
+ var n: usize = 0;
+ while (p.nextEffect()) |effect| switch (effect) {
+ .write => |w| {
+ const bytes = w.bytes.slice();
+ if (n + bytes.len > buf.len) break;
+ @memcpy(buf[n..][0..bytes.len], bytes);
+ n += bytes.len;
+ },
+ else => {},
+ };
+ return buf[0..n];
+ }
+
+ // Raw tty mode forwards nearly every key to the child; the two paste
+ // chords are the exception, and forwarding them instead is what broke
+ // pasting into a terminal pane. An application that gets Ctrl-V rather
+ // than the text goes and reads the desktop clipboard on its own account,
+ // and a coding agent reading it is looking for an image, not for words.
+ test "raw tty paste chords type at the program instead of reaching it as keys" {
+ if (comptime !panes.Terminal.enabled) return error.SkipZigTest;
+ for ([_]bool{ false, true }) |bracketed| {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 12 });
+ defer p.deinit();
+ const pane = p.panes[0].?;
+ try std.testing.expectEqual(Pane.Mode.tty, pane.mode);
+ if (bracketed) p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?2004h" } });
+ try std.testing.expectEqual(bracketed, panes.Terminal.bracketedPaste(pane));
+ p.yank = try p.gpa.dupe(u8, "one\ntwo"); // what a `y` anywhere left behind
+ var buf: [256]u8 = undefined;
+ _ = childInput(p, &buf);
+
+ // Ctrl-V types the register straight at the program, asking the
+ // desktop for nothing. Under DECSET 2004 the program's own fences
+ // carry it, which is what stops readline RUNNING a multi-line paste.
+ p.update(.{ .key = .{ .cp = 'v', .ctrl = true } });
+ try std.testing.expectEqualStrings(
+ if (bracketed) "\x1b[200~one\ntwo\x1b[201~" else "one\rtwo",
+ childInput(p, &buf),
+ );
+ try std.testing.expect(p.clip_pending == null);
+
+ // Ctrl-Shift-V is the other store: it ASKS, types nothing yet, and
+ // the answer lands at the program rather than in an edit buffer. A
+ // host that folds the Shift into the letter says the same thing.
+ for ([_]Key{
+ .{ .cp = 'v', .ctrl = true, .shift = true },
+ .{ .cp = 'V', .ctrl = true },
+ }) |chord| {
+ p.update(.{ .key = chord });
+ try std.testing.expect(p.clip_pending != null);
+ try std.testing.expectEqualStrings("", childInput(p, &buf));
+ p.update(.{ .paste = "from-desktop" });
+ try std.testing.expect(p.clip_pending == null);
+ try std.testing.expectEqualStrings(
+ if (bracketed) "\x1b[200~from-desktop\x1b[201~" else "from-desktop",
+ childInput(p, &buf),
+ );
+ }
+ // ...and neither chord put the clipboard in the register on its way.
+ try std.testing.expectEqualStrings("one\ntwo", p.yank orelse "");
+ }
+ }
};
const ClickCursorTests = struct {
@@ -1616,7 +1682,7 @@ test "generated output refuses every pipe prompt before seeding the tag" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
const content = try p.gpa.dupe(u8, "generated\n");
- const pane = try panes.Output.open(p, 1, "/", .{ .cmd = .Help }, "", content);
+ const pane = try panes.Output.open(p, p.freeSlot().?, "/", .{ .cmd = .Help }, "", content);
p.active = 1;
for ([_]Key{ .{ .cp = '|' }, .{ .cp = '|', .alt = true }, .{ .cp = '!' }, .{ .cp = '!', .alt = true } }) |key| {
p.update(.{ .key = key });
@@ -1825,14 +1891,14 @@ test "file history construction failures preserve existing panes" {
defer if (incoming) |bytes| p.gpa.free(bytes);
failing.fail_index = failing.alloc_index + failure;
const result = switch (case) {
- .open => panes.File.open(p, 1, path, 0),
+ .open => panes.File.open(p, p.freeSlot().?, path, 0),
.restore => panes.File.restore(p, 1, .{
.kind = .file,
.tag = "",
.body = "",
.file = .{ .path = "/restored.zig", .content = "restored\n" },
}),
- .output => panes.Output.open(p, 1, "/", .{ .cmd = .Help }, "", incoming.?),
+ .output => panes.Output.open(p, p.freeSlot().?, "/", .{ .cmd = .Help }, "", incoming.?),
.tutor => p.openTutorView(1),
.replace => p.setTestFile("replacement\n"),
};
@@ -1900,14 +1966,14 @@ test "file history owns bounded snapshot storage without per-edit bookkeeping al
try std.testing.expectEqual(@as(usize, 0), history.redo_len);
}
-test "file history allocation precedes a filesystem read with side effects" {
+test "file history allocation precedes reading the served tree" {
var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
const p = try Pardes.init(failing.allocator(), .{ .tty_only = true, .cols = 40, .rows = 12 });
defer p.deinit();
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", 0));
+ try std.testing.expectError(error.OutOfMemory, panes.File.open(p, p.freeSlot().?, "/n/self/index", 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.zig b/test/pdf.zig
index beeb0b40..930cc5c0 100644
--- a/test/pdf.zig
+++ b/test/pdf.zig
@@ -1253,7 +1253,7 @@ test "PdfTint cycles pane-local state without adding tag clutter" {
try std.testing.expectEqual(PdfTintMode.filtered, pv.tint);
const initial_tag = try pardes.test_api.tagText(p, p.scratch.allocator(), pane);
try std.testing.expect(std.mem.startsWith(u8, initial_tag, pv.path));
- try std.testing.expect(std.mem.endsWith(u8, initial_tag, " PdfSections PdfTint Collapse"));
+ try std.testing.expect(std.mem.endsWith(u8, initial_tag, " PdfSections PdfTint Collapse Del"));
try std.testing.expect(std.mem.indexOf(u8, initial_tag, "filtered") == null);
try std.testing.expect(pardes.test_api.runBuiltin(
diff --git a/test/tag_cleanup.py b/test/tag_cleanup.py
index 7b7d8bbf..ae0a0e66 100644
--- a/test/tag_cleanup.py
+++ b/test/tag_cleanup.py
@@ -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'/pane/{control}/ctl', b'exec Del\n') # owned command-only scratch
+ client.write(f'/pane/{control}/exec', b'Del\n') # owned command-only scratch
def tag_position():
frame = client.screen()
diff --git a/test/ui_review.py b/test/ui_review.py
index 2b6faa29..a1cdcdeb 100644
--- a/test/ui_review.py
+++ b/test/ui_review.py
@@ -43,7 +43,7 @@ def command(client, serial, text):
current = client.read(tag)
start = current.rfind(text.encode())
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(f'/pane/{serial}/tag', b'', truncate=True)
client.write(tag, before[tail_start:])
def new_output(client, before):
@@ -111,7 +111,7 @@ def run(args):
capture(client, root, '02-find', not args.tty)
if find_pane:
(root / 'find-results.txt').write_bytes(client.read(f'/pane/{find_pane}/body'))
- client.write(f'/pane/{find_pane}/ctl', b'exec Del\n')
+ client.write(f'/pane/{find_pane}/exec', b'Del\n')
before = panes(client)
command(client, source, 'Grep CrdsValueLabel')
grep_pane = require(wait_for(lambda: new_output(client, before)), 'Grep did not return results')
@@ -140,10 +140,10 @@ def run(args):
(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'/pane/{symbols}/ctl', b'exec Del\n')
+ client.write(f'/pane/{symbols}/exec', b'Del\n')
offset = original.index(b'CrdsValue {')
client.write(f'/pane/{source}/addr', f'#{offset},#{offset}'.encode())
- client.write(f'/pane/{source}/ctl', b'dot=addr\nshow\n')
+ client.write(f'/pane/{source}/dot', client.read(f'/pane/{source}/addr'))
before = panes(client)
command(client, source, 'Hover')
hover = wait_for(lambda: new_output(client, before), args.lsp_wait)
@@ -154,7 +154,7 @@ def run(args):
# 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'/pane/{source}/ctl', f'look {source_path}:{line}:19\n'.encode())
+ client.write(f'/pane/{source}/look', f'{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)
@@ -166,7 +166,7 @@ def run(args):
report['lsp'] = info.decode()
capture(client, root, '08-lsp-info', not args.tty)
if lsp:
- client.write(f'/pane/{lsp}/ctl', b'exec Del\n')
+ client.write(f'/pane/{lsp}/exec', b'Del\n')
for theme in args.themes:
command(client, source, 'Theme ' + theme)
capture(client, root, 'theme-' + theme, not args.tty)
diff --git a/test/v9fs.py b/test/v9fs.py
index 16916d0b..fa35388f 100644
--- a/test/v9fs.py
+++ b/test/v9fs.py
@@ -43,8 +43,8 @@ def worker(mountpoint, socket, uid, gid, original_namespace):
assert not any(field.startswith(('shared:', 'master:')) for field in mounted[0].split()[6:])
tree = mountpoint
- assert {'os', 'index', 'pane', 'new', 'ctl', 'log', 'screen', 'README'} <= set(os.listdir(tree))
- assert 'self' not in os.listdir(tree)
+ assert {'os', 'index', 'pane', 'status', 'look', 'exec', 'log', 'screen', 'README'} <= set(os.listdir(tree))
+ assert 'self' not in os.listdir(tree) and 'new' 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('/index')
@@ -52,14 +52,31 @@ def worker(mountpoint, socket, uid, gid, original_namespace):
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)
+ subprocess.run(['find', str(tree / 'pane'), '-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'/pane/{another}/ctl', b'exec Del\n')
+ # mkdir through the kernel mount opens a pane. The editor names it
+ # after its serial, not after the name asked for, so the kernel's own
+ # revalidation of that name may fail; the index is the answer.
+ def serials():
+ return {int(row.split()[0]) for row in (tree / 'index').read_bytes().splitlines()}
+
+ def mkpane(name):
+ known = serials()
+ try:
+ (tree / 'pane' / name).mkdir()
+ except FileNotFoundError:
+ pass
+ made = serials() - known
+ assert len(made) == 1, (name, made)
+ return made.pop()
+
+ serial = mkpane('kernel-made')
+ another = mkpane('kernel-made-again')
+ assert serial != another, 'a second mkdir reused a pane'
+ assert not (tree / 'pane' / 'kernel-made').exists()
+ (tree / 'pane' / str(another)).rmdir()
+ assert another not in serials()
pane = tree / 'pane' / str(serial)
wire = f'/pane/{serial}'
assert str(serial) in os.listdir(tree / 'pane')
@@ -101,7 +118,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'exec Del\n')
+ (tree / 'pane' / str(serial)).rmdir()
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, '
diff --git a/test/window_opacity.py b/test/window_opacity.py
index b9529373..a576b1bd 100644
--- a/test/window_opacity.py
+++ b/test/window_opacity.py
@@ -60,7 +60,7 @@ def run(args):
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'/pane/{pane}/body').decode()
- client.write(f'/pane/{pane}/ctl', b'exec Del\n')
+ client.write(f'/pane/{pane}/exec', b'Del\n')
return text
help_text = output('Help')