diff options
Diffstat (limited to 'test/fs.py')
| -rw-r--r-- | test/fs.py | 84 |
1 files changed, 78 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() |
