summaryrefslogtreecommitdiff
path: root/test/selfmount.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/selfmount.py')
-rw-r--r--test/selfmount.py126
1 files changed, 126 insertions, 0 deletions
diff --git a/test/selfmount.py b/test/selfmount.py
new file mode 100644
index 00000000..cc889a93
--- /dev/null
+++ b/test/selfmount.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+"""A session that opens, reads and saves its own 9P tree through a mount.
+
+The editor runs under `9ns --mntgen`, so /mnt/9p/pardes/<name> inside its
+namespace is its own tree, and is driven over its socket from outside. This
+used to hang the editor outright: the realpath, the stat and the read went
+out through the mount and came back as 9P requests only the editor's loop
+could answer, while that loop was blocked making them. Now the connection's
+task answers them while the editor waits (pardes.turn).
+
+Needs 9ns (cloud9) on PATH or in ~/.local/bin, and a runtime directory; it
+skips, loudly, without them.
+"""
+import fcntl
+import os
+from pathlib import Path
+import pty
+import shutil
+import signal
+import struct
+import sys
+import termios
+import time
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from ninep import Client
+
+
+def run(binary):
+ ns = shutil.which('9ns') or str(Path.home() / '.local/bin/9ns')
+ runtime = os.environ.get('XDG_RUNTIME_DIR')
+ if not os.path.exists(ns) or not runtime:
+ print('selfmount: skipped (needs 9ns and XDG_RUNTIME_DIR)')
+ return 0
+ name = 'selfmount%d' % os.getpid()
+ sock = f'{runtime}/pardes-9p-{name}.sock'
+ fixture = Path(f'/tmp/{name}.txt')
+ fixture.write_bytes(b'initial\n')
+ env = {k: v for k, v in os.environ.items() if not k.startswith('PARDES_')}
+ env.update(PARDES_NOTIME='1', TERM='xterm-256color', PARDES_DUMP=f'/tmp/{name}.dump.zon')
+ for language in ['RS', 'C', 'GO', 'TS', 'PY']:
+ env['PARDES_LSP_' + language] = ''
+ pid, master = pty.fork()
+ if pid == 0:
+ os.execve(ns, ['9ns', '--mntgen', '--', binary, '--tty', '--9p=' + name, str(fixture)], env)
+ fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack('HHHH', 30, 100, 0, 0))
+ os.set_blocking(master, False)
+ tail = bytearray()
+
+ def drain():
+ try:
+ while True:
+ chunk = os.read(master, 65536)
+ if not chunk:
+ return
+ tail.extend(chunk[-2000:])
+ except OSError:
+ pass
+
+ failures = []
+
+ def check(condition, what):
+ print(('ok ' if condition else 'FAIL ') + what)
+ if not condition:
+ failures.append(what)
+
+ try:
+ deadline = time.monotonic() + 15
+ while not os.path.exists(sock):
+ drain()
+ if time.monotonic() > deadline:
+ raise TimeoutError('socket never appeared: ' + tail.decode(errors='replace')[-400:])
+ time.sleep(.02)
+ time.sleep(.3)
+ mount = f'/mnt/9p/pardes/{name}'
+ with Client(sock) as client:
+ index = client.read('/index').decode()
+ rows = [row.split(maxsplit=3) for row in index.splitlines()]
+ first = rows[0][0]
+ # A Look at the tree's own index: realpath, stat and read all go
+ # out through 9ns and back in, and the write returns when the
+ # pane is open.
+ started = time.monotonic()
+ client.write('/look', (mount + '/index\n').encode())
+ took = time.monotonic() - started
+ check(took < 5, f'look at the mounted index returned ({took:.2f}s)')
+ rows = [row.split(maxsplit=3) for row in client.read('/index').decode().splitlines()]
+ opened = [row for row in rows if len(row) == 4 and row[3] == mount + '/index']
+ check(len(opened) == 1, 'a pane opened on the mounted index')
+ if opened:
+ body = client.read(f'/pane/{opened[0][0]}/body').decode()
+ check(body.startswith(index.splitlines()[0]), 'its body is the index as served')
+ # A Look at pane 1's body through the mount, an edit of the copy,
+ # and a Save: the save writes back through the mount into pane 1,
+ # and the exec returns once it has.
+ client.write('/look', f'{mount}/pane/{first}/body\n'.encode())
+ rows = [row.split(maxsplit=3) for row in client.read('/index').decode().splitlines()]
+ mirror = [row for row in rows if len(row) == 4 and row[3] == f'{mount}/pane/{first}/body']
+ check(len(mirror) == 1, 'a pane opened on the mounted body')
+ if mirror:
+ pane = mirror[0][0]
+ client.write(f'/pane/{pane}/addr', b'#0,#0')
+ client.write(f'/pane/{pane}/data', b'HELLO ')
+ client.write(f'/pane/{pane}/exec', b'Save\n')
+ check(client.read(f'/pane/{first}/body') == b'HELLO initial\n', f'Save wrote back through the mount into pane {first}')
+ text = ''.join(cell[0] for cell in client.screen()['cells'])
+ check('saved ' + mount in text, 'the editor reported the save')
+ client.write('/exec', b'Msg still here\n')
+ text = ''.join(cell[0] for cell in client.screen()['cells'])
+ check('still here' in text, 'the editor answers after all of that')
+ finally:
+ os.kill(pid, signal.SIGTERM)
+ time.sleep(.2)
+ try:
+ os.kill(pid, signal.SIGKILL)
+ except ProcessLookupError:
+ pass
+ os.waitpid(pid, 0)
+ for path in (fixture, Path(f'/tmp/{name}.dump.zon')):
+ path.unlink(missing_ok=True)
+ print('selfmount: ' + ('ok' if not failures else f'{len(failures)} failed'))
+ return 0 if not failures else 1
+
+
+if __name__ == '__main__':
+ sys.exit(run(str(Path(sys.argv[1]).resolve())))