diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/fs.py | 147 |
1 files changed, 147 insertions, 0 deletions
@@ -514,6 +514,152 @@ def new_terminals_named_once(binary): assert f'\nnew {serial} {root}' in '\n' + log, (serial, log) +def workflow_canary(binary): + """One session driven as the dogfood agents drive it, asserting the log + and the read-backs at every step: the canary for the main paths (the + round-18 Grep that never found anything went through every narrower + test). Look files, Grep and Find hits and misses, a plain word looked + at, Edit in two files, Save, get, pty/run failing then passing, a + command pane and Kill, a REPL send, the layout and a column's files, + ask/answer, and Dump/Restore last.""" + started = time.monotonic() + with tempfile.TemporaryDirectory(prefix='pardes-flow-') as directory: + root = Path(directory) + (root / 'a.txt').write_bytes(b'alpha one\nshared word\n') + (root / 'b.txt').write_bytes(b'beta two\nshared word\n') + (root / 's.sh').write_bytes(b'echo from-file\n') + + def wait(ready, what, seconds=10): + deadline = time.monotonic() + seconds + while not ready(): + assert time.monotonic() < deadline, what + time.sleep(.05) + + with session(binary, root, 'flow') as (client, address): + def log(): + return client.read('/log').decode('utf-8', 'replace') + + def made_by(serial, line): + """The pane an exec at `serial` made, as exec reads it back.""" + client.write(f'/pane/{serial}/exec', line) + return int(client.read(f'/pane/{serial}/exec').split()[-1]) + + # Look opens files, and the log says new. + a = look(client, str(root / 'a.txt')) + b = look(client, str(root / 'b.txt')) + assert client.read(f'/pane/{a}/body') == b'alpha one\nshared word\n' + assert f'new {a} {root}/a.txt\n' in log() and f'new {b} {root}/b.txt\n' in log() + + # Grep and Find: a hit opens its +Search, rows and all; a miss + # fails ENOENT, opening nothing. + grep = made_by(a, b'Grep shared\n') + rows = client.read(f'/pane/{grep}/body').decode() + assert 'a.txt:2' in rows and 'b.txt:2' in rows, rows + client.remove(f'/pane/{grep}') + execute_refused(client, a, 'Grep zzqqnomatch', 'grep: no match') + found = made_by(a, b'Find b.txt\n') + assert 'b.txt' in client.read(f'/pane/{found}/body').decode() + client.remove(f'/pane/{found}') + execute_refused(client, a, 'Find zzqqnomatch', 'find: no match') + + # A plain word looked at is found in the pane, after its dot. + client.write(f'/pane/{a}/dot', b' 0 0 ') + client.write(f'/pane/{a}/look', b'shared\n') + assert client.read(f'/pane/{a}/dot').split() == [b'10', b'16'], client.read(f'/pane/{a}/dot') + + # Edit in two files, then Save each: on disk, and logged. + for pane in (a, b): + client.write(f'/pane/{pane}/ctl', b'Edit ,s/shared/SHARED/g\n') + assert client.read(f'/pane/{b}/body') == b'beta two\nSHARED word\n' + for pane in (a, b): + client.write(f'/pane/{pane}/exec', b'Save\n') + assert (root / 'a.txt').read_bytes() == b'alpha one\nSHARED word\n' + assert f'save {a} {root}/a.txt\n' in log() and f'save {b} {root}/b.txt\n' in log() + + # get reloads what changed on disk. + (root / 'b.txt').write_bytes(b'from disk\n') + client.write(f'/pane/{b}/ctl', b'get\n') + assert client.read(f'/pane/{b}/body') == b'from disk\n' + + # A terminal: pty/run failing then passing. + term = made_by(1, b'Tty\n') + + def run(line): + fid = client.open(f'/pane/{term}/pty/run', 2) + try: + client.rpc(118, struct.pack('<IQI', fid, 0, len(line)) + line) + return client.read_fid(fid) + finally: + client.close(fid) + deadline = time.monotonic() + 10 + while (answer := run(b'true\n')) != b'exit 0\n': + assert time.monotonic() < deadline, answer + time.sleep(.05) + assert run(b'false\n') == b'exit 1\n' + assert run(b'echo passed\n') == b'exit 0\npassed\n' + # (A run's answer is its record: the log has no err for these.) + assert f'err {term} pty/run' not in log(), log() + + # A command pane, and Kill: its exit is logged. + sleeper = made_by(a, b'sleep 30\n') + assert f'run {sleeper} sleep 30\n' in log() + client.write('/ctl', b'Kill sleep\n') + wait(lambda: f'\nexit {sleeper} ' in log(), 'Kill: no exit logged') + + # A REPL send: a program in the terminal (cat, which says back + # what it is sent; a nested shell reads as the shell itself) + # bound as bash's REPL, an exec on a .sh file's body (its event + # written back) goes there. + s = look(client, str(root / 's.sh')) + client.write(f'/pane/{term}/pty/data', b'cat\r') + wait(lambda: client.read(f'/pane/{term}/pty/status').split()[-1] == b'1', ('the REPL program did not start', client.read(f'/pane/{term}/body')[-300:])) + client.write(f'/pane/{term}/ctl', b'Repl bash\n') + fid = client.open(f'/pane/{s}/event', 2) + try: + record = b'MX0 0 0 14 echo from-file\n' + client.rpc(118, struct.pack('<IQI', fid, 0, len(record)) + record) + finally: + client.close(fid) + wait(lambda: f'\nsend {s} {term} ' in log(), ('no REPL send logged', log()[-800:])) + wait(lambda: client.read(f'/pane/{term}/body').count(b'echo from-file') >= 2, 'the REPL did not get it') + client.write(f'/pane/{term}/ctl', b'Repl -\n') + client.write(f'/pane/{term}/pty/data', b'\x04') + wait(lambda: run(b'true\n') == b'exit 0\n', 'the terminal did not come back to its shell') + + # The layout and a column's files: Newcol, its tag, rmdir. + assert len(client.read('/layout').splitlines()) >= 2 # a column, then `active` + client.write('/ctl', b'Newcol\n') + columns = [row for row in client.read('/layout').splitlines() if not row.startswith(b'active')] + assert len(columns) == 2, columns + empty = columns[-1].split()[0].decode() + assert b'New' in client.read(f'/col/{empty}/tag') + client.remove(f'/col/{empty}') + assert len([row for row in client.read('/layout').splitlines() if not row.startswith(b'active')]) == 1 + + # ask/answer: Save on a terminal asks for a path over 9P. + client.write(f'/pane/{term}/exec', b'Save\n') + wait(lambda: f'ask {term} save path\n' in log(), 'no ask logged') + client.write(f'/pane/{term}/ctl', f'answer {root}/scroll.txt\n'.encode()) + wait(lambda: (root / 'scroll.txt').exists(), 'the answered Save wrote nothing') + assert f'answer {term} {root}/scroll.txt\n' in log() + + # Dump, then Restore it: the new session names its panes. + client.write('/ctl', b'Dump\n') + saved = root / 'flow.dump.zon' + wait(saved.exists, 'Dump wrote nothing') + assert f'dump {saved}\n' in log() + try: + client.write('/ctl', f'Restore {saved}\n'.encode()) + except (EOFError, OSError): + pass + time.sleep(.5) + with Client(address) as restored: + wait(lambda: f'restore {saved}' in restored.read('/log').decode(), 'Restore not logged') + names = restored.read('/index').decode() + assert f'{root}/a.txt' in names and f'{root}/b.txt' in names, names + assert time.monotonic() - started < 30, 'the canary took too long' + + def python_repl_runs_one_line(binary): """A one-line exec sent to Python's own REPL (3.13's pyrepl, which asks for bracketed paste) runs at once, with no second Enter: the Enter @@ -631,6 +777,7 @@ def test(binary, quic=False): exit_answers(binary) unattended_messages_expire(binary) new_terminals_named_once(binary) + workflow_canary(binary) python_repl_runs_one_line(binary) signal_cleans_up(binary) for options, message in [ |
