diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/fs.py | 84 | ||||
| -rw-r--r-- | test/monkey9p.py | 3 | ||||
| -rw-r--r-- | test/selfmount.py | 7 |
3 files changed, 88 insertions, 6 deletions
@@ -4,6 +4,7 @@ import fcntl import json import os import shutil +import select import signal from pathlib import Path import subprocess @@ -380,8 +381,18 @@ def discovery(binary, embedded=False): # 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. + # A Save makes the directories its file goes in, whatever names + # it: the pane's own name or Save's path. + made_dirs = new_pane(client, b'deep\n') + client.write(f'/pane/{made_dirs}/ctl', f'Save {root}/save-made/a/b/x.txt\n'.encode()) + assert (root / 'save-made/a/b/x.txt').read_bytes() == b'deep\n' + client.write(f'/pane/{made_dirs}/name', f'{root}/save-named/c/y.txt\n'.encode()) + client.write(f'/pane/{made_dirs}/ctl', b'Save\n') + assert (root / 'save-named/c/y.txt').read_bytes() == b'deep\n' + client.remove(f'/pane/{made_dirs}') + # A save the host cannot do (a directory it may not make) 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') @@ -435,9 +446,14 @@ def run_file(binary): assert run(client, term, b'fi\n') == b'error not run\n' # a syntax error runs nothing assert run(client, term, b"echo 'open\n") == b'error not run\n' # nor does an open quote assert run(client, term, b'true\n') == b'exit 0\n' # ...and the shell is clear after - # A pty asked for one row gets two: one loses the prompt's input - # mark, and the shell would read busy for ever. - client.write(f'/pane/{term}/pty/ctl', b'winsize 20 1\n') + # A pty is not given one row (it loses the prompt's input mark, + # and the shell would read busy for ever): refused, and said. + try: + client.write(f'/pane/{term}/pty/ctl', b'winsize 20 1\n') + raise AssertionError('winsize 20 1 was taken') + except OSError as refused: + assert 'at least 2 rows' in str(refused), refused + client.write(f'/pane/{term}/pty/ctl', b'winsize 20 2\n') time.sleep(.5) assert run(client, term, b'true\n') == b'exit 0\n' assert client.read(f'/pane/{term}/pty/ctl').split()[2] == b'2', client.read(f'/pane/{term}/pty/ctl') @@ -1219,11 +1235,18 @@ def test(binary, quic=False): assert client.read('/index') == before assert client.read('/pane/1/body') == b'initial\n' + # A session that answers takes the file or says why, as acme's B + # does: a pane it has not is its refusal and exit 1, never a + # nested editor drawn inside the pane that asked. + gone = subprocess.run([binary, '--tty', str(spaced)], cwd=root, + env=dict(env, PARDES_PANE='4294967295'), stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10) + assert gone.returncode == 1 and gone.stderr.startswith(b'pardes: '), gone + assert client.read('/index') == before for name, identity, word in [ ('stale-file', dict(inherited, PARDES_9P=str(root / 'absent.sock')), str(spaced)), ('stale-missing', dict(inherited, PARDES_9P=str(root / 'absent.sock')), 'missing-child-file.txt'), ('stale-noarg', dict(inherited, PARDES_9P=str(root / 'absent.sock')), None), - ('stale-pane', dict(inherited, PARDES_PANE='4294967295'), str(spaced)), # No pid means "not inside a pardes at all", which is what # --nested withholds and what a plain shell has. ('not-nested', {k: v for k, v in inherited.items() if k != 'PARDES_PID'}, str(spaced)), @@ -1310,10 +1333,59 @@ def test(binary, quic=False): execute(client, named, 'Save') client.remove(f'/pane/{named}') assert deep.read_bytes() == b'deep\n' + # A name the session refuses: its reason and exit 1, as B's, + # never a nested editor, and no pane left for it. + before = serials() + refused = subprocess.run([binary, 'bad\x01name.txt'], cwd=root, env=env, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10) + assert refused.returncode == 1, refused + assert b'bad character in file name' in refused.stderr, refused.stderr + assert serials() == before, (serials(), before) orphan = launch('--wait') assert orphan.poll() is None assert orphan.wait(timeout=5) == 1 + # Killed with SIGTERM or SIGHUP, a tty editor puts its terminal + # back (main screen, cooked mode) before it dies: a nested one + # killed in a pane leaves that pane usable. + for sig in (signal.SIGTERM, signal.SIGHUP): + master, slave = os.openpty() + fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 24, 80, 0, 0)) + cooked = termios.tcgetattr(slave) + env = {k: v for k, v in os.environ.items() if not k.startswith('PARDES_')} + env.update(HOME=str(root), XDG_RUNTIME_DIR=str(root), TERM='xterm-256color', PARDES_NOTIME='1') + killed = subprocess.Popen([binary, '--tty', '--9p=killed', 'x.txt'], cwd=root, env=env, + stdin=slave, stdout=slave, stderr=subprocess.DEVNULL, + start_new_session=True, + preexec_fn=lambda: fcntl.ioctl(0, termios.TIOCSCTTY, 0)) + seen = b'' + try: + deadline = time.monotonic() + 10 + while b'\x1b[?1049h' not in seen: + assert time.monotonic() < deadline, seen[-200:] + if select.select([master], [], [], .1)[0]: + seen += os.read(master, 65536) + time.sleep(.3) + killed.send_signal(sig) + assert killed.wait(timeout=5) == -sig + while select.select([master], [], [], .2)[0]: + try: + chunk = os.read(master, 65536) + except OSError: + break + if not chunk: + break + seen += chunk + tail = seen[seen.rindex(b'\x1b[?1049h'):] + assert b'\x1b[?1049l' in tail, tail[-200:] + assert termios.tcgetattr(slave)[3] & termios.ICANON == cooked[3] & termios.ICANON + finally: + if killed.poll() is None: + killed.kill() + killed.wait() + os.close(master) + os.close(slave) + # A launch with no terminal to draw on (a pty that is no one's # controlling terminal) says so and exits 1, no error trace. master, slave = os.openpty() diff --git a/test/monkey9p.py b/test/monkey9p.py index 7ab6cb0c..4d190bbc 100644 --- a/test/monkey9p.py +++ b/test/monkey9p.py @@ -1518,6 +1518,9 @@ def one_failure_rule(ctx, res): # line cut across Twrites runs a piece a Twrite, and a piece that ran # (a Save) says so while a later one is refused. succeeded = [w for w in writes if w[2] is True and w[0] == 'write'] + # A file watch's notice (`<file> deleted on disk ...`, `changed on + # disk`) is the host's news, said whenever it arrives: no write's. + msgs = [m for m in msgs if ' deleted on disk (' not in m and ' changed on disk (' not in m] if failed and msgs and not succeeded: return f'a failed write also logged msg: {msgs}; failed={[(w[1], w[3]) for w in failed]}' return None diff --git a/test/selfmount.py b/test/selfmount.py index 55809740..6534aa25 100644 --- a/test/selfmount.py +++ b/test/selfmount.py @@ -138,6 +138,13 @@ def run(binary): check(run(f'python3 -c "{maker}" > {mount}/pane/{scratch}/ctl\n'.encode()) == '0', 'a 50 KB Edit block written through the mount') body = client.read(f'/pane/{scratch}/body').decode().splitlines() check(body == ['line %05d of the block' % i for i in range(2500)], f'it ran once, whole ({len(body)} lines)') + # Forty event reads held through the mount, a follower each, and + # the mount still answers the rest: 9ns keeps workers past what + # the editor holds (128), so `cat layout` is not queued behind + # them for ever. + held = run(f"for i in $(seq 40); do cat {mount}/pane/{first}/event > /dev/null & done; " + f"sleep 1; timeout 10 cat {mount}/layout > /dev/null; st=$?; kill $(jobs -p); exit $st\n".encode()) + check(held == '0', f'cat layout answers beside 40 held event reads ({held!r})') 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') |
