#!/usr/bin/env python3 import contextlib import fcntl import json import os import shutil import signal from pathlib import Path import subprocess import struct import sys import tempfile import termios import threading import time from ninep import Client @contextlib.contextmanager def session(binary, root, name, *options, socket_name=None, file=None, tty=False, network=None, launch=None, inherited=None, terminal=None): env = {key: value for key, value in os.environ.items() if not key.startswith('PARDES_')} env.update(HOME=str(root), XDG_RUNTIME_DIR=str(root), XDG_CONFIG_HOME=str(root / 'config'), PARDES_NOTIME='1', PARDES_DUMP=str(root / (name + '.dump.zon'))) env.update(inherited or {}) for language in ['RS', 'C', 'GO', 'TS', 'PY']: env['PARDES_LSP_' + language] = '' fixture = root / (name + '.txt') fixture.write_bytes(b'initial\n') address = root / ('pardes-9p-' + (socket_name or name) + '.sock') child = None master = slave = None reader = None try: args = [binary, '--detach=' + name, *options, str(file or fixture)] preexec = None if tty: master, slave = os.openpty() fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 24, 80, 0, 0)) env['TERM'] = 'xterm-256color' args = [binary, '--tty', '--9p=' + (socket_name or name), *options, str(file or fixture)] preexec = lambda: fcntl.ioctl(0, termios.TIOCSCTTY, 0) if launch is not None: args = [binary, *launch] child = subprocess.Popen(args, cwd=root, env=env, stdin=slave, stdout=slave if tty else subprocess.DEVNULL, stderr=subprocess.PIPE, start_new_session=True, preexec_fn=preexec) if terminal is not None: # Borrowed handles for input-driving probes, valid inside this context. terminal.update(pid=child.pid, input_fd=master) if launch is not None and socket_name is None: address = root / f'pardes-9p-{child.pid}.sock' if slave is not None: os.close(slave) slave = None def drain(): try: while os.read(master, 65536): pass except OSError: pass reader = threading.Thread(target=drain, daemon=True) reader.start() deadline = time.monotonic() + 10 while not address.exists(): if child.poll() is not None: raise AssertionError((args, child.returncode, child.stderr.read().decode())) if time.monotonic() > deadline: raise TimeoutError('default 9P socket did not appear') time.sleep(.005) with Client(address) as client: if network is not None: 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 network[parts[0].upper()] = int(parts[2]) assert set(network) == {'TCP', 'QUIC'}, network yield client, address finally: if terminal is not None: terminal.clear() if child is not None: if child.poll() is None: child.terminate() try: child.wait(timeout=5) except subprocess.TimeoutExpired: child.kill() child.wait() child.stderr.close() if slave is not None: os.close(slave) if master is not None: os.close(master) if reader is not None: reader.join(timeout=1) 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): """An open of /pane/new makes a pane; the read names it.""" serial = int(client.read('/pane/new')) 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'/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) # The scratch it looked from goes: a column holds only so many panes, # each its tag and two rows. if source is None: client.remove(f'/pane/{serial}') return results[0] def execute(client, serial, command): """A middle click on `command` from a pane.""" client.write(f'/pane/{serial}/exec', f'{command}\n'.encode()) def execute_refused(client, serial, command, words): """A middle click on a builtin that fails: the write fails, saying why.""" try: execute(client, serial, command) except OSError as why: assert words in str(why), (command, why) return raise AssertionError(f'{command!r} was taken') 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): 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: root = Path(directory) with session(binary, root, 'discovery') as (client, address): top = client.list('/') # A set: which files the root holds, not the order a listing gives. assert set(top) - {'src', 'shaders'} == {'README', 'index', 'status', 'focus', 'ctl', 'commands', 'recent', 'look', 'exec', 'log', 'screen', 'listeners', 'layout', 'tag', 'tagexec', 'pane', 'col', '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'Msg hello' in guide and b'pane/new' in guide, guide assert client.stat('/README')['length'] == len(guide) # ls/stat/find over the whole tree, without opening, creates nothing. seen = walk_tree(client) names = {path for path, _ in seen} assert {'/index', '/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) for path, info in seen: assert info['mode'] in (0o755, 0o644, 0o666, 0o444, 0o222, 0o555), (path, info) assert info['mtime'] > 0, (path, info) assert client.read('/index') == before, 'browsing created a pane' assert client.stat('/index')['length'] == len(before) > 0 index = client.stat('/index') assert index['mode'] == 0o444 and client.stat('/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.rsplit(b' ', 1)[0].endswith(b'/discovery.txt'), before # /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'' # An open of /pane/new makes a pane and the read names it; each # open makes another. A stat makes none, which is why new can be # listed at all: ls -l stats every name a listing gave it. # /log answers the recent past and ends; `follow` written to the # same open makes its reads past that wait for new records. log = client.open('/log', 2) frozen = len(client.read_fid(log)) client.rpc(118, struct.pack(') deletes the addressed # range only, so the write after it replaces that range. scratch = new_pane(client, b'hello world\nsecond line\n') client.write(f'/pane/{scratch}/addr', b'#0,#5') client.write(f'/pane/{scratch}/data', b'HOWDY', truncate=True) assert client.read(f'/pane/{scratch}/body') == b'HOWDY world\nsecond line\n' # sam's regexps: ^ at any line; a miss says so and leaves no # address, so the data write after it refuses. client.write(f'/pane/{scratch}/addr', b'/^second/') assert client.read(f'/pane/{scratch}/addr') == b' 12 18 ' for path, data, why in ((f'/pane/{scratch}/addr', b'/nowhere/', 'no match for regexp'), (f'/pane/{scratch}/data', b'LOST', 'no address')): try: client.write(path, data) raise AssertionError(f'{path} took {data!r}') except OSError as refused: assert why in str(refused), refused assert client.read(f'/pane/{scratch}/body') == b'HOWDY world\nsecond line\n' # Through a mount both read as EINVAL; the log says which and why. assert f'err {scratch} addr: no match for regexp\n'.encode() in client.read('/log') client.remove(f'/pane/{scratch}') # A save the host cannot do fails the write that asked for it, # and the log's err record names the path and why. unsaved = new_pane(client, b'unsaved\n') try: client.write(f'/pane/{unsaved}/ctl', b'Save /nonexistent-pardes-dir/x.txt\n') raise AssertionError('a Save into a missing directory succeeded') except OSError as refused: assert 'Save /nonexistent-pardes-dir/x.txt: no such directory' in str(refused), refused assert f'err {unsaved} ctl: Save /nonexistent-pardes-dir/x.txt: '.encode() in client.read('/log'), client.read('/log') client.remove(f'/pane/{unsaved}') # Sixteen connections fill the slots; the next is told why. extra = [Client(address) for _ in range(15)] try: try: Client(address).__exit__() raise AssertionError('a seventeenth connection was served') except OSError as refused: assert 'too many connections' in str(refused), refused finally: for other in extra: other.__exit__() deadline = time.monotonic() + 5 while b'err - 9p: too many connections' not in client.read('/log'): assert time.monotonic() < deadline, client.read('/log') time.sleep(.01) print('9P discovery: listing/stat/find are inert; new, remove, look, exec, name, sel, log, ctl lock, focus, the ctl split and commands behave') def run_file(binary): """pty/run: a line in, how it ended out, on the same open.""" def run(c, serial, line): fid = c.open(f'/pane/{serial}/pty/run', 2) try: c.rpc(118, struct.pack('= 2, 'the REPL did not get it') client.write(f'/pane/{term}/ctl', b'Repl -\n') client.write(f'/pane/{term}/pty/data', b'\x04') wait(lambda: run(b'true\n') == b'exit 0\n', 'the terminal did not come back to its shell') # The layout and a column's files: Newcol, its tag, rmdir. assert len(client.read('/layout').splitlines()) >= 2 # a column, then `active` client.write('/ctl', b'Newcol\n') columns = [row for row in client.read('/layout').splitlines() if not row.startswith(b'active')] assert len(columns) == 2, columns empty = columns[-1].split()[0].decode() assert b'New' in client.read(f'/col/{empty}/tag') client.remove(f'/col/{empty}') assert len([row for row in client.read('/layout').splitlines() if not row.startswith(b'active')]) == 1 # ask/answer: Save on a terminal asks for a path over 9P. client.write(f'/pane/{term}/exec', b'Save\n') wait(lambda: f'ask {term} save path\n' in log(), 'no ask logged') client.write(f'/pane/{term}/ctl', f'answer {root}/scroll.txt\n'.encode()) wait(lambda: (root / 'scroll.txt').exists(), 'the answered Save wrote nothing') assert f'answer {term} {root}/scroll.txt\n' in log() # Dump, then Restore it: the new session names its panes. client.write('/ctl', b'Dump\n') saved = root / 'flow.dump.zon' wait(saved.exists, 'Dump wrote nothing') assert f'dump {saved}\n' in log() try: client.write('/ctl', f'Restore {saved}\n'.encode()) except (EOFError, OSError): pass time.sleep(.5) with Client(address) as restored: wait(lambda: f'restore {saved}' in restored.read('/log').decode(), 'Restore not logged') names = restored.read('/index').decode() assert f'{root}/a.txt' in names and f'{root}/b.txt' in names, names # Recent goes on across the Restore, and is kept after it. assert f'{root}/a.txt\n' in restored.read('/recent').decode(), restored.read('/recent') (root / 'after.txt').write_bytes(b'after\n') look(restored, str(root / 'after.txt')) assert f'open {root}/after.txt\n' in restored.read('/recent').decode() state = root / '.local' / 'state' / 'pardes' / 'recent' wait(lambda: f'{root}/after.txt'.encode() in state.read_bytes(), 'Recent not kept after Restore') assert time.monotonic() - started < 30, 'the canary took too long' def python_repl_runs_one_line(binary): """A one-line exec sent to Python's own REPL (3.13's pyrepl, which asks for bracketed paste) runs at once, with no second Enter: the Enter reaches it after the paste, not inside it. A real pty, a real python3.""" python = shutil.which('python3') if python is None: return version = subprocess.run([python, '-c', 'import sys; print(sys.version_info >= (3, 13))'], capture_output=True, text=True).stdout.strip() if version != 'True': return with tempfile.TemporaryDirectory(prefix='pardes-repl-') as directory: with session(binary, Path(directory), 'repl') as (client, _): def panes(client): return {int(row.split()[0]) for row in client.read('/index').splitlines()} before = panes(client) client.write('/pane/1/ctl', b'Tty\n') deadline = time.monotonic() + 10 while time.monotonic() < deadline and panes(client) == before: time.sleep(.05) tty = max(panes(client) - before) def body(): return client.read(f'/pane/{tty}/body').decode('utf-8', 'replace') def wait_for(text, seconds=15): deadline = time.monotonic() + seconds while time.monotonic() < deadline: if text in body(): return True time.sleep(.1) return False assert wait_for('$') or wait_for('>'), body() client.write(f'/pane/{tty}/pty/data', python.encode() + b' -q\r') assert wait_for('>>>'), body() client.write(f'/pane/{tty}/ctl', b'Repl python\n') # An exec on the REPL's own body goes to the REPL, as its event # record written back does: an empty range acts on its text. fid = client.open(f'/pane/{tty}/event', 2) try: record = b'MX0 0 0 10 print(6*7)\n' client.rpc(118, struct.pack('>>'), (code, body()[-200:]) finally: client.close(fid) def signal_cleans_up(binary): """A detached session told to stop by a signal unlinks its sockets.""" for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]: with tempfile.TemporaryDirectory(prefix='pardes-sig-') as directory: root = Path(directory) seen = {} with session(binary, root, 'sig', terminal=seen) as (client, address): detached = root / 'pardes-detached-sig.sock' assert address.exists() and detached.exists() os.kill(seen['pid'], sig) deadline = time.monotonic() + 5 while (address.exists() or detached.exists()) and time.monotonic() < deadline: time.sleep(.01) assert not address.exists() and not detached.exists(), (sig, list(root.iterdir())) def odd_paths(binary): """Long and odd paths written where a path goes -- name, look, DumpDir, Dump, Save -- are refused or said, never a crash: a part over 255 bytes (ENAMETOOLONG), a loop of links (ELOOP), a file taken for a directory (ENOTDIR), one not ours (EACCES).""" with tempfile.TemporaryDirectory(prefix='pardes-odd-') as directory: root = Path(directory) (root / 'loop').symlink_to(root / 'loop') (root / 'plain').write_bytes(b'x\n') odd = ['/tmp/' + 'x' * 300 + '/f.txt', str(root / ('y' * 300)), str(root / 'loop' / 'f'), str(root / 'plain' / 'f'), '/root/nope/f', str(root / 'a' / '..' / 'b.txt')] with session(binary, root, 'odd') as (client, _): serial = new_pane(client, b'text\n') for path in odd: for where, line in ((f'/pane/{serial}/name', path), ('/look', path), ('/ctl', f'DumpDir {path}'), ('/ctl', 'Dump'), (f'/pane/{serial}/ctl', 'Save')): try: client.write(where, (line + '\n').encode()) except OSError: pass assert client.read('/index'), (where, line) client.write('/ctl', b'DumpDir\n') # A theme file that is not there, or no theme, fails the write. (root / 'bad.zon').write_bytes(b'.{ .nonsense = 1 }') for line, why in ((f'ThemeFile {root}/missing.zon', 'theme file'), (f'ThemeFile {root}/bad.zon', 'theme file'), ('ThemeFile notes.txt', 'a theme file is a .zon')): try: client.write('/ctl', (line + '\n').encode()) raise AssertionError(f'{line} was taken') except OSError as refused: assert why in str(refused), (line, refused) def delcol_last_keeps_window(binary): """Delcol on the last column leaves the window, empty but for the workspace tag, as acme does; Newcol and New start again from there.""" with tempfile.TemporaryDirectory(prefix='pardes-delcol-') as directory: with session(binary, Path(directory), 'delcol') as (client, _): def columns(): return [l for l in client.read('/layout').decode().splitlines() if l and not l.startswith('active')] while columns(): client.write(f'/col/{columns()[0].split()[0]}/ctl', b'Delcol\n') # Still here, empty. assert client.read('/index') == b'', client.read('/index') client.write('/ctl', b'Newcol\n') assert len(columns()) == 1, columns() serial = new_pane(client, b'back\n') assert str(serial).encode() in client.read('/index'), client.read('/index') def cut_writes(binary): """A write bigger than a 9P message comes as several, cut anywhere: each command line runs once, whole, an Edit block once it ends, and a last line with no newline when the open closes. Never a fragment.""" with tempfile.TemporaryDirectory(prefix='pardes-cut-') as directory: with session(binary, Path(directory), 'cut') as (client, _): burst = b''.join(b'Msg burst-%04d\n' % i for i in range(1000)) assert len(burst) > client.msize client.write('/exec', burst) log = client.read('/log').decode() said = [line.split(' ', 2)[2] for line in log.splitlines() if line.startswith('msg ') and 'burst-' in line] assert said == ['burst-%04d' % i for i in range(1000)], said[:3] + said[-3:] assert '\nrun ' not in log and 'exit 127' not in log, log[-400:] client.write('/exec', b'Msg no-newline-at-end') assert 'msg ' in client.read('/log').decode().split('no-newline-at-end')[0][-40:] serial = new_pane(client, b'') block = b'Edit ,a\n' + b''.join(b'line %05d of the block\n' % i for i in range(2500)) + b'.\n' assert len(block) > 50000 client.write(f'/pane/{serial}/ctl', block) body = client.read(f'/pane/{serial}/body').decode().splitlines() assert body == ['line %05d of the block' % i for i in range(2500)], (len(body), body[:2]) # A detached session with no frontend is 160x50 until told. screen = client.screen() assert (screen['cols'], screen['rows']) == (160, 50), (screen['cols'], screen['rows']) client.write('/ctl', b'size 132 40\n') screen = client.screen() assert (screen['cols'], screen['rows']) == (132, 40), (screen['cols'], screen['rows']) def test(binary, quic=False): started = time.monotonic() cut_writes(binary) odd_paths(binary) delcol_last_keeps_window(binary) run_file(binary) exit_answers(binary) unattended_messages_expire(binary) new_terminals_named_once(binary) workflow_canary(binary) recent_survives_restart(binary) python_repl_runs_one_line(binary) signal_cleans_up(binary) for options, message in [ (['--fs'], b'no such option'), (['--fs9'], b'no such option'), (['--9p'], b'--9p needs a socket name'), (['--9p='], b'invalid 9P socket name'), (['--9p=a/b'], b'invalid 9P socket name'), (['--attach=work', '--9p=work'], b'--attach has none'), (['--9p-tcp'], b'--9p-tcp needs'), (['--9p-tcp='], b'--9p-tcp needs'), (['--9p-tcp=tcp!localhost!5640'], b'--9p-tcp needs'), (['--9p-tcp=quic!127.0.0.1!5640'], b'--9p-tcp needs'), (['--9p-tcp=tcp!127.0.0.1!5640', '--9p-tcp=tcp!127.0.0.1!5641'], b'specified twice'), (['--attach=work', '--9p-tcp=tcp!127.0.0.1!5640'], b'--attach has none'), (['--9p-quic'], b'--9p-quic needs'), (['--9p-quic='], b'--9p-quic needs'), (['--9p-quic=tcp!127.0.0.1!5640'], b'--9p-quic needs'), (['--mount=os=work'], b'invalid mount name'), (['--mount=peer='], b'invalid mount name or dial'), (['--mount=peer=one', '--mount=peer=two'], b'duplicate mount'), (['--mount=peer=tcp!127.0.0.1!0'], b'invalid mount dial'), (['--attach=work', '--mount=peer=one'], b'--attach has none'), ]: result = subprocess.run([binary, *options], capture_output=True, timeout=5) assert result.returncode == 1 and message in result.stderr, (options, result) with tempfile.TemporaryDirectory(prefix='pardes-fs-') as directory: root = Path(directory) with session(binary, root, 'renamed', '--9p=explicit', socket_name='explicit') as (named, named_address): assert named_address.name == 'pardes-9p-explicit.sock' assert not (root / 'pardes-9p-renamed.sock').exists() assert named.read('/pane/1/body') == b'initial\n' with session(binary, root, 'first') as (client, address): 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' assert 'first.txt' in client.list('/os' + str(root)) assert client.read('/pane/1/../1/body') == b'initial\n' assert client.read('/os' + str(root) + '/../' + root.name + '/first.txt') == b'initial\n' listing = root / 'listing' listing.mkdir() (listing / 'broken').symlink_to(listing / 'missing') for number in range(24): (listing / f'entry-{number:02}').touch() with Client(address, msize=256) as small: assert set(small.list('/os' + str(listing))) == { f'entry-{number:02}' for number in range(24)} # 64 KiB frames: a ctl line with no newline, whole in its one # Twrite, runs with it however long -- the cutoff for a write # that may go on is the frame the client asked for, less 24. with Client(address, msize=65536) as big: assert big.msize == 65536 ctl = big.open('/pane/1/ctl', 1) line = b'bogus' + b'x' * 20000 try: big.rpc(118, struct.pack(' assert (b'\n'.join(b'restored %d %d' % (old, new) for old, new in zip(sorted(old_ids), sorted(int(r.split()[0]) for r in restored.read('/index').splitlines()))) + b'\nrestoredcol ') in log, log restored_ids = [int(row.split()[0]) for row in restored.read('/index').splitlines()] assert len(restored_ids) == 2 and old_ids.isdisjoint(restored_ids), restored_ids file_id, control_id = restored_ids contents = restored.read(f'/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 exec, not the body' assert restored.screen()['cols'] == (80 if tty else 160) # a detached one is 160x50 opened = look(restored, '/n/own/os' + str(root / (name + '.txt')), source=control_id) assert restored.read(f'/pane/{opened}/body') == b'initial\n' with session(binary, root, 'forwarding') as (client, address): child_dir = root / 'child-working-directory' child_dir.mkdir() spaced = child_dir / 'space name.txt' spaced.write_bytes(b'first line\nsecond line\n') env = os.environ.copy() # PARDES_PID is what says "you are inside a pardes"; the socket # and the pane only say how to reach it. Without the pid the child # starts its own session, so this test used to pass only when the # runner itself happened to be running inside one. status = dict(line.split(maxsplit=1) for line in client.read('/status').decode().splitlines()) env.update(PARDES_9P=str(address), PARDES_PANE='1', PARDES_PID=status['pid']) env.pop('PARDES_FORWARD_LOOK', None) for word, expected, selected, reuse in [('space name.txt:2:4', spaced.read_bytes(), [14, 14], False), ('/n/self/pane/1/body', b'initial\n', None, False), ('/virtual/pane/1/body:1:2-4', b'initial\n', [1, 4], True)]: before = {int(row.split()[0]) for row in client.read('/index').splitlines()} result = subprocess.run([binary, '--tty', word], cwd=child_dir, env=env, capture_output=True, timeout=3) assert result.returncode == 0, (word, result.stderr) after = {int(row.split()[0]) for row in client.read('/index').splitlines()} if reuse: assert after == before else: opened, = after - before assert client.read(f'/pane/{opened}/body') == expected assert client.read('/pane/1/body') == b'initial\n' if selected is not None: # 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) 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(' [--quic | --discovery]') test(str(Path(sys.argv[1]).resolve()), quic=len(sys.argv) == 3)