summaryrefslogtreecommitdiff
path: root/test/fs.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/fs.py')
-rw-r--r--test/fs.py169
1 files changed, 169 insertions, 0 deletions
diff --git a/test/fs.py b/test/fs.py
index 204f4b37..97af9e6b 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -605,6 +605,56 @@ def unattended_messages_expire(binary):
assert not shown(), 'an unattended message outlived MessageLinger'
+def directory_panes(binary):
+ """A look at a directory opens a pane listing it, as acme's directory
+ window: a text pane named with a trailing /, its body the entries (a
+ directory's marked /), ctl's isdir 1, and get reads it again."""
+ with tempfile.TemporaryDirectory(prefix='pardes-dirpane-') as directory:
+ root = Path(directory).resolve()
+ listed = root / 'listed'
+ (listed / 'sub').mkdir(parents=True)
+ (listed / '.dot').write_bytes(b'')
+ with session(binary, root, 'dirpane') as (client, _):
+ serial = look(client, str(listed))
+ rows = [row.split() for row in client.read('/index').decode().splitlines()]
+ row = next(r for r in rows if int(r[0]) == serial)
+ assert row[1:4] == ['text', '0', f'{listed}/'], row
+ body = client.read(f'/pane/{serial}/body').decode()
+ assert body.split() == ['.dot', 'sub/'], body
+ assert client.read(f'/pane/{serial}/ctl').split()[3] == b'1'
+ (listed / 'later').write_bytes(b'')
+ client.write(f'/pane/{serial}/ctl', b'get\n')
+ assert client.read(f'/pane/{serial}/body').decode().split() == ['.dot', 'later', 'sub/']
+
+
+def putall_answers_after_its_saves(binary):
+ """Putall with one pane refused (its file changed on disk under unsaved
+ edits) fails, and is answered only once the other panes' saves have
+ landed."""
+ with tempfile.TemporaryDirectory(prefix='pardes-putall-') as directory:
+ root = Path(directory).resolve()
+ (root / 'a.txt').write_bytes(b'a\n')
+ (root / 'b.txt').write_bytes(b'b\n')
+ with session(binary, root, 'putall') as (client, _):
+ a = look(client, str(root / 'a.txt'))
+ b = look(client, str(root / 'b.txt'))
+ for serial in (a, b):
+ client.write(f'/pane/{serial}/body', b'edited\n')
+ (root / 'b.txt').write_bytes(b'changed on disk\n')
+ deadline = time.monotonic() + 10
+ while f'changed {b}' not in client.read('/log').decode():
+ assert time.monotonic() < deadline, 'the watch never saw b.txt change'
+ time.sleep(.05)
+ try:
+ client.write('/ctl', b'Putall\n')
+ except OSError as why:
+ assert 'modified on disk' in str(why), why
+ else:
+ raise AssertionError('Putall over a file changed on disk was taken')
+ assert (root / 'a.txt').read_bytes() == b'a\nedited\n', (root / 'a.txt').read_bytes()
+ assert (root / 'b.txt').read_bytes() == b'changed on disk\n'
+
+
def new_terminals_named_once(binary):
"""A terminal started outside the session's directory is named by that
directory from its `new` on: no rename to the session's and back."""
@@ -612,6 +662,9 @@ def new_terminals_named_once(binary):
root = Path(directory)
(root / 'sub').mkdir()
with session(binary, root, 'names') as (client, _):
+ # A look at a directory starts a terminal there (DirLook
+ # terminal); DirLook pane, the default, lists it in a pane.
+ client.write('/ctl', b'DirLook terminal\n')
made = [look(client, str(root / 'sub'))]
before = {int(row.split()[0]) for row in client.read('/index').splitlines()}
client.write('/pane/1/ctl', b'Tty\n')
@@ -983,15 +1036,128 @@ def cut_writes(binary):
assert (screen['cols'], screen['rows']) == (132, 40), (screen['cols'], screen['rows'])
+def edit_commands(binary):
+ """Edit's `< | >` run off the loop and answer the write that ran them
+ once applied; a failing one changes nothing and fails the write in its
+ words; X edits every open file it matches; w writes. The editor answers
+ other requests while a slow command runs."""
+ with tempfile.TemporaryDirectory(prefix='pardes-edit-') as directory:
+ root = Path(directory)
+ (root / 'a.txt').write_bytes(b'pear\napple\nfig\n')
+ (root / 'b.txt').write_bytes(b'apple pie\n')
+ with session(binary, root, 'edit') as (client, address):
+ a = look(client, str(root / 'a.txt'))
+ b = look(client, str(root / 'b.txt'))
+ # The write returns with the text sorted, through the session's
+ # shell in the file's directory.
+ client.write(f'/pane/{a}/ctl', b'Edit , | sort\n')
+ assert client.read(f'/pane/{a}/body') == b'apple\nfig\npear\n'
+ client.write(f'/pane/{a}/ctl', b'Edit $ < pwd\n')
+ assert client.read(f'/pane/{a}/body') == f'apple\nfig\npear\n{root}\n'.encode()
+ client.write(f'/pane/{a}/ctl', b'Undo\n')
+ assert client.read(f'/pane/{a}/body') == b'apple\nfig\npear\n'
+ # A per-match command, one undo step.
+ client.write(f'/pane/{a}/ctl', b'Edit ,x/[a-z]+/ | tr a-z A-Z\n')
+ assert client.read(f'/pane/{a}/body') == b'APPLE\nFIG\nPEAR\n'
+ # A failing command: nothing changed, the write fails in words,
+ # its stderr in +Errors.
+ try:
+ client.write(f'/pane/{a}/ctl', b'Edit , | echo nope >&2; exit 4\n')
+ raise AssertionError('a failing Edit command was taken')
+ except OSError as why:
+ assert 'exit status 4' in str(why), why
+ assert client.read(f'/pane/{a}/body') == b'APPLE\nFIG\nPEAR\n'
+ index = client.read('/index').decode()
+ errors = [line.split()[0] for line in index.splitlines() if line.endswith(('+Errors', f'{root}/+Errors')) or '+Errors' in line]
+ assert errors and any(b'nope' in client.read(f'/pane/{e}/body') for e in errors), index
+ # While a slow command runs, the editor answers.
+ done = threading.Event()
+
+ def slow():
+ with Client(address) as other:
+ other.write(f'/pane/{b}/ctl', b'Edit , | sleep 1; cat\n')
+ done.set()
+ worker = threading.Thread(target=slow)
+ worker.start()
+ time.sleep(.2)
+ began = time.monotonic()
+ client.read('/index')
+ assert time.monotonic() - began < .5 and not done.is_set(), 'the editor waited on the command'
+ worker.join(timeout=15)
+ assert done.is_set()
+ # On the same connection: the Edit's write is held, and a read sent
+ # after it is answered first; a Tflush of it stops it, changes
+ # nothing, and frees the way for the next Edit.
+ def send(kind, tag, payload):
+ client.socket.sendall(struct.pack('<IBH', 7 + len(payload), kind, tag) + payload)
+
+ def answer():
+ size, kind, tag = struct.unpack('<IBH', client.receive(7))
+ return kind, tag, client.receive(size - 7)
+ ctl = client.open(f'/pane/{a}/ctl', 1)
+ status = client.open('/status', 0)
+ line = b'Edit , | sleep 1; cat\n'
+ began = time.monotonic()
+ send(118, 900, struct.pack('<IQI', ctl, 0, len(line)) + line)
+ send(116, 901, struct.pack('<IQI', status, 0, 4096))
+ first = answer()
+ assert first[1] == 901 and first[0] == 117 and time.monotonic() - began < .5, (first[:2], time.monotonic() - began)
+ second = answer()
+ assert second[1] == 900 and second[0] == 119, second
+ line = b'Edit , | sleep 5; tr a-z A-Z\n'
+ send(118, 902, struct.pack('<IQI', ctl, 0, len(line)) + line)
+ time.sleep(.3)
+ send(108, 903, struct.pack('<H', 902))
+ tags = sorted(answer()[1] for _ in range(2))
+ assert tags == [902, 903], tags
+ client.close(ctl)
+ client.close(status)
+ time.sleep(1)
+ client.write(f'/pane/{a}/ctl', b'Edit , | cat\n')
+ time.sleep(4.5)
+ assert client.read(f'/pane/{a}/body') == b'APPLE\nFIG\nPEAR\n', client.read(f'/pane/{a}/body')
+ # X: each open file whose line matches; w writes it.
+ client.write(f'/pane/{a}/ctl', b'Edit X/b\\.txt$/ ,s/apple/APPLE/\n')
+ assert client.read(f'/pane/{b}/body') == b'APPLE pie\n'
+ client.write(f'/pane/{b}/ctl', b'Edit w\n')
+ assert (root / 'b.txt').read_bytes() == b'APPLE pie\n'
+ client.write(f'/pane/{a}/ctl', b'Edit 1 w first.txt\n')
+ assert (root / 'first.txt').read_bytes() == b'APPLE\n'
+ # A file changed on disk under unsaved edits: Edit's w refuses
+ # once, as Save does, and the next w overwrites it.
+ w1 = root / 'w1.txt'
+ w1.write_bytes(b'one\n')
+ w = look(client, str(w1))
+ client.write(f'/pane/{w}/ctl', b'Edit $a/two/\n')
+ seen = client.read('/log').decode().count(f'\nchanged {w}')
+ with open(w1, 'ab') as f:
+ f.write(b'external\n')
+ deadline = time.monotonic() + 10
+ while client.read('/log').decode().count(f'\nchanged {w}') == seen:
+ assert time.monotonic() < deadline, 'the change on disk was not heard'
+ time.sleep(.05)
+ try:
+ client.write(f'/pane/{w}/ctl', b'Edit w\n')
+ raise AssertionError('w overwrote a file changed on disk')
+ except OSError as why:
+ assert 'modified on disk since read (w again to overwrite)' in str(why), why
+ assert w1.read_bytes() == b'one\nexternal\n'
+ client.write(f'/pane/{w}/ctl', b'Edit w\n')
+ assert w1.read_bytes() == b'one\ntwo', w1.read_bytes()
+
+
def test(binary, quic=False):
started = time.monotonic()
cut_writes(binary)
+ edit_commands(binary)
odd_paths(binary)
delcol_last_keeps_window(binary)
run_file(binary)
exit_answers(binary)
unattended_messages_expire(binary)
new_terminals_named_once(binary)
+ directory_panes(binary)
+ putall_answers_after_its_saves(binary)
workflow_canary(binary)
recent_survives_restart(binary)
python_repl_runs_one_line(binary)
@@ -1429,6 +1595,9 @@ def test(binary, quic=False):
dead_env = dict(outside_env, PARDES_9P=str(root / 'pardes-9p-gone.sock'))
gone = subprocess.run([binary, '--wait', str(far)], cwd=root, env=dead_env, capture_output=True, timeout=10)
assert gone.returncode == 1 and b'does not answer' in gone.stderr, gone
+ # --wait with no FILE says it needs one, and waits on nothing.
+ bare = subprocess.run([binary, '--wait'], cwd=root, env=env, capture_output=True, timeout=10)
+ assert bare.returncode == 1 and b'--wait needs a FILE' in bare.stderr, bare
# `--` ends the options: a name starting with a dash is a name.
dash = root / '-dash.txt'
dash.write_bytes(b'dash\n')