diff options
Diffstat (limited to 'test/fs.py')
| -rw-r--r-- | test/fs.py | 169 |
1 files changed, 169 insertions, 0 deletions
@@ -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') |
