diff options
Diffstat (limited to 'test/selfmount.py')
| -rw-r--r-- | test/selfmount.py | 126 |
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()))) |
