summaryrefslogtreecommitdiff
path: root/test/fs.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/fs.py')
-rw-r--r--test/fs.py119
1 files changed, 111 insertions, 8 deletions
diff --git a/test/fs.py b/test/fs.py
index a6cd332e..80bee07f 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -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,31 @@ 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.
+ # `~` is home (this session's HOME, the test's root) wherever a
+ # path is typed: Save, name, a look, with an address or not.
+ tilde = new_pane(client, b'one\ntwo\nthree\n')
+ client.write(f'/pane/{tilde}/ctl', b'Save ~/tilde-made/sub/x.txt\n')
+ assert (root / 'tilde-made/sub/x.txt').read_bytes() == b'one\ntwo\nthree\n'
+ client.write(f'/pane/{tilde}/name', b'~/tilde-named.txt\n')
+ assert client.read(f'/pane/{tilde}/name') == f'{root}/tilde-named.txt\n'.encode()
+ client.remove(f'/pane/{tilde}')
+ client.write('/look', b'~/tilde-made/sub/x.txt:3\n')
+ looked = [row for row in client.read('/index').decode().splitlines() if row.split()[3] == f'{root}/tilde-made/sub/x.txt']
+ assert len(looked) == 1, client.read('/index')
+ assert client.read(f'/pane/{looked[0].split()[0]}/addr').startswith(b'8 '), client.read(f'/pane/{looked[0].split()[0]}/addr')
+ client.remove(f'/pane/{looked[0].split()[0]}')
+ 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 +459,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')
@@ -1101,7 +1130,15 @@ def test(binary, quic=False):
except OSError as refused:
assert 'Modified' in str(refused), refused
assert old.read('/pane/1/body') == b'changed after dump\n'
- execute(old, control, 'Restore ' + str(saved))
+ # Answered before the hang-up: the write's reply comes; the
+ # clunk after it may meet the connection already cut.
+ restore = old.open(f'/pane/{control}/exec', 1)
+ line = ('Restore ' + str(saved) + '\n').encode()
+ old.rpc(118, struct.pack('<IQI', restore, 0, len(line)) + line)
+ try:
+ old.close(restore)
+ except (EOFError, OSError):
+ pass
# Refused while the connections are cut, or cut already.
try:
old.read_fid(held_body)
@@ -1219,11 +1256,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)),
@@ -1292,7 +1336,8 @@ def test(binary, quic=False):
client.write(f'/pane/{named}/body', b'new\n')
execute(client, named, 'Save')
client.remove(f'/pane/{named}')
- assert made.wait(timeout=5) == 0
+ code = made.wait(timeout=5)
+ assert code == 0, (code, made.stderr.read())
assert fresh.read_bytes() == b'new\n'
# ...in a directory not there either: forwarded all the same, and
# Save makes the directories.
@@ -1310,10 +1355,68 @@ def test(binary, quic=False):
execute(client, named, 'Save')
client.remove(f'/pane/{named}')
assert deep.read_bytes() == b'deep\n'
+ # `pardes '~/x'`, quoted so the shell left the `~`: home all the
+ # same: the launching shell's HOME.
+ before = serials()
+ quoted = subprocess.run([binary, '~/fwd-tilde.txt'], cwd=root, env=dict(env, HOME=str(root)),
+ stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=10)
+ assert quoted.returncode == 0, quoted.stderr
+ assert f'{root}/fwd-tilde.txt'.encode() in client.read('/index'), client.read('/index')
+ for serial in serials() - before:
+ client.remove(f'/pane/{serial}')
+ # 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()