summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/fs.py119
-rw-r--r--test/monkey9p.py15
-rw-r--r--test/selfmount.py46
-rw-r--r--test/snapshots/diffview.golden148
-rw-r--r--test/snapshots/diffview.snap37
-rw-r--r--test/snapshots/lspcomplete.snap20
6 files changed, 361 insertions, 24 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()
diff --git a/test/monkey9p.py b/test/monkey9p.py
index 7ab6cb0c..87e386b3 100644
--- a/test/monkey9p.py
+++ b/test/monkey9p.py
@@ -674,8 +674,9 @@ def served_lines(data, sizes, room):
(fs.md, tree.zig write): each ended line, and the unended tail of a piece
shorter than a Twrite holds (`room`, msize less 24), which is whole in
itself, as acme takes each write; a piece that fills its Twrite may go
- on, and its tail waits for the next. A line written a byte a Twrite is
- so as many lines as bytes."""
+ on, and its tail waits for the next, and so does one of a multiple of
+ 4096 bytes, where a writer's buffer filled. A line written a byte a
+ Twrite is so as many lines as bytes."""
lines, pending, at = [], b'', 0
for size in sizes:
piece = data[at:at + size]
@@ -683,7 +684,7 @@ def served_lines(data, sizes, room):
pending += piece
*done, pending = pending.split(b'\n')
lines += done
- if pending and len(piece) < room:
+ if pending and len(piece) < room and len(piece) % 4096 != 0:
lines.append(pending)
pending = b''
if pending:
@@ -1518,6 +1519,9 @@ def one_failure_rule(ctx, res):
# line cut across Twrites runs a piece a Twrite, and a piece that ran
# (a Save) says so while a later one is refused.
succeeded = [w for w in writes if w[2] is True and w[0] == 'write']
+ # A file watch's notice (`<file> deleted on disk ...`, `changed on
+ # disk`) is the host's news, said whenever it arrives: no write's.
+ msgs = [m for m in msgs if ' deleted on disk (' not in m and ' changed on disk (' not in m]
if failed and msgs and not succeeded:
return f'a failed write also logged msg: {msgs}; failed={[(w[1], w[3]) for w in failed]}'
return None
@@ -1612,7 +1616,10 @@ def chunked_equals_whole(ctx, res):
whole_n, chunk_n = dir_names(whole), dir_names(chunked)
if whole_n != chunk_n:
return f'{path}: whole {whole_n} != chunked({size}) {chunk_n}'
- elif whole != chunked:
+ elif whole != chunked and w.read_all(fid) != chunked:
+ # A live file (a tag says `running`, then `exit 0`) may change
+ # between the two reads: only chunks that no whole read matches,
+ # read again, are a chunking fault.
return f'{path}: whole {whole[:200]!r} != chunked({size}) {chunked[:200]!r}'
finally:
w.clunk(fid)
diff --git a/test/selfmount.py b/test/selfmount.py
index 55809740..86c32fa8 100644
--- a/test/selfmount.py
+++ b/test/selfmount.py
@@ -20,6 +20,7 @@ import signal
import struct
import sys
import termios
+import threading
import time
sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -127,17 +128,54 @@ def run(binary):
return exits[-1].split()[2]
time.sleep(.1)
return 'timeout'
+ # Followed from before the burst: a fresh open of the log reads
+ # only what its 64 KiB ring still holds, which a burst this size
+ # outgrows, and a follower the ring outruns reads `lost N`, so
+ # every line is either heard once, in order, or a loss is said.
+ heard = []
+
+ def follow():
+ with Client(sock) as follower:
+ fid = follower.open('/log', 2)
+ start = len(follower.read_fid(fid))
+ follower.rpc(118, struct.pack('<IQI', fid, 0, len(b'follow new\n')) + b'follow new\n')
+ ready.set()
+ deadline = time.monotonic() + 60
+ while time.monotonic() < deadline:
+ record = follower.read_fid(fid, start).decode()
+ heard.append(record)
+ if 'mnt-0999' in record:
+ return
+ ready = threading.Event()
+ reader = threading.Thread(target=follow, daemon=True)
+ reader.start()
+ check(ready.wait(10), 'a follower of the log is waiting')
got = run(f"seq -f 'Msg mnt-%04g' 0 999 > {mount}/exec\n".encode())
check(got == '0', f'a 1000-line burst written through the mount ({got!r})')
- log = client.read('/log').decode()
- said = [line.split(' ', 2)[2] for line in log.splitlines() if line.startswith('msg ') and 'mnt-' in line]
- check(said == ['mnt-%04d' % i for i in range(1000)], f'each line of it ran once, whole ({len(said)} said)')
- check('exit 127' not in log, 'no piece of a line ran as a command')
+ reader.join(60)
+ for r in heard:
+ if r.startswith(('err ', 'exit ')):
+ print(' heard: ' + r.rstrip())
+ said = [r.rstrip('\n').split(' ', 2)[2] for r in heard if r.startswith('msg ') and 'mnt-' in r]
+ lost = [r for r in heard if r.startswith('lost ')]
+ numbers = [int(s[4:]) for s in said]
+ check(numbers == list(range(numbers[0] if numbers else 0, 1000)),
+ f'each line it heard ran once, whole, in order, through the last ({len(said)} heard)')
+ check(len(said) == 1000 or (lost and heard.index(lost[0]) < heard.index(next(r for r in heard if 'mnt-' in r))),
+ f'no line was lost unsaid ({len(said)} heard, {lost!r})')
+ check(not any('exit 127' in r for r in heard), 'no piece of a line ran as a command')
scratch = int(client.read('/pane/new'))
maker = "import sys; sys.stdout.write('Edit ,a\\n' + ''.join('line %05d of the block\\n' % i for i in range(2500)) + '.\\n')"
check(run(f'python3 -c "{maker}" > {mount}/pane/{scratch}/ctl\n'.encode()) == '0', 'a 50 KB Edit block written through the mount')
body = client.read(f'/pane/{scratch}/body').decode().splitlines()
check(body == ['line %05d of the block' % i for i in range(2500)], f'it ran once, whole ({len(body)} lines)')
+ # Forty event reads held through the mount, a follower each, and
+ # the mount still answers the rest: 9ns keeps workers past what
+ # the editor holds (128), so `cat layout` is not queued behind
+ # them for ever.
+ held = run(f"for i in $(seq 40); do cat {mount}/pane/{first}/event > /dev/null & done; "
+ f"sleep 1; timeout 10 cat {mount}/layout > /dev/null; st=$?; kill $(jobs -p); exit $st\n".encode())
+ check(held == '0', f'cat layout answers beside 40 held event reads ({held!r})')
client.write('/exec', b'Msg still here\n')
text = ''.join(cell[0] for cell in client.screen()['cells'])
check('still here' in text, 'the editor answers after all of that')
diff --git a/test/snapshots/diffview.golden b/test/snapshots/diffview.golden
new file mode 100644
index 00000000..9e171fd8
--- /dev/null
+++ b/test/snapshots/diffview.golden
@@ -0,0 +1,148 @@
+== snap diff-pane grid=100x31 cursor=7,3
+|Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit
+| New Tty Find Grep Joincol Delcol
+| /tmp/pardes-snap/diffview/cwd/change.diff Save Tty Collapse Del
+| 1 diff --git a/src/shape.zig b/src/shape.zig
+| 2 index 1111111..2222222 100644
+| 3 --- a/src/shape.zig
+| 4 +++ b/src/shape.zig
+| 5 @@ -1,10 +1,12 @@
+| 6 const std = @import("std");
+| 7
+| 8 pub const Shape = union(enum) {
+| 9 circle: f32,
+| 10 - square: u8 = 7,
+| 11 + square: f32,
+| 12 + triangle: struct { base: f32, height: f32 },
+| 13
+| 14 const note =
+| 15 - \\areas
+| 16 + \\areas, defer
+| 17 + \\summed
+| 18 ;
+| 19 };
+| 20
+== style diff-colours grid=100x31
+|0: 0-99 #b2c3b8,#1d2824,
+|1: 0-1 d,#887455, 2-2 d,#314738, 3-99 #becfc4,#314738,
+|2: 0-1 d,#add38a, 2-2 d,#314738, 3-32 #becfc4,#314738, 33-44 #dfbce5,#314738, 45-99 #becfc4,#314738,
+|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-4 d,#0d1410,b 5-6 #616f64,#0d1410,b 7-99 #b9f480,#0d1410,
+|4: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-99 #b9f480,#0d1410,
+|5: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-99 #b9f480,#0d1410,
+|6: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-99 #b9f480,#0d1410,
+|7: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-99 #c9a7ed,#0d1410,
+|8: 0-0 d,#608269, 1-4 d,#0d1410, 5-7 #59675c,#0d1410, 8-19 #e5eee5,#0d1410, 20-26 #c9a7ed,#0d1410, 27-27 #e5eee5,#0d1410, 28-32 #b8d88a,#0d1410, 33-99 #e5eee5,#0d1410,
+|9: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
+|10: 0-0 d,#608269, 1-4 d,#0d1410, 5-7 #59675c,#0d1410, 8-25 #e5eee5,#0d1410, 26-30 #c9a7ed,#0d1410, 31-31 #e5eee5,#0d1410, 32-35 #c9a7ed,#0d1410, 36-99 #e5eee5,#0d1410,
+|11: 0-0 d,#608269, 1-4 d,#0d1410, 5-11 #59675c,#0d1410, 12-99 #e5eee5,#0d1410,
+|12: 0-0 d,#608269, 1-3 d,#0d1410, 4-6 #59675c,#0d1410, 7-7 #f1a39b,#302b25,b 8-11 #f1a39b,#302b25, 12-24 #e5eee5,#302b25, 25-25 #f8c082,#302b25, 26-99 #e5eee5,#302b25,
+|13: 0-0 d,#608269, 1-3 d,#0d1410, 4-6 #59675c,#0d1410, 7-7 #b6ce94,#263025,b 8-11 #b6ce94,#263025, 12-99 #e5eee5,#263025,
+|14: 0-0 d,#608269, 1-3 d,#0d1410, 4-6 #59675c,#0d1410, 7-7 #b6ce94,#263025,b 8-11 #b6ce94,#263025, 12-21 #e5eee5,#263025, 22-28 #c9a7ed,#263025, 29-99 #e5eee5,#263025,
+|15: 0-0 d,#608269, 1-3 d,#0d1410, 4-99 #59675c,#0d1410,
+|16: 0-0 d,#608269, 1-3 d,#0d1410, 4-11 #59675c,#0d1410, 12-99 #e5eee5,#0d1410,
+|17: 0-0 d,#608269, 1-3 d,#0d1410, 4-6 #59675c,#0d1410, 7-7 #f1a39b,#302b25,b 8-15 #f1a39b,#302b25, 16-99 #b8d88a,#302b25,
+|18: 0-0 d,#608269, 1-3 d,#0d1410, 4-6 #59675c,#0d1410, 7-7 #b6ce94,#263025,b 8-15 #b6ce94,#263025, 16-99 #b8d88a,#263025,
+|19: 0-0 d,#608269, 1-3 d,#0d1410, 4-6 #59675c,#0d1410, 7-7 #b6ce94,#263025,b 8-15 #b6ce94,#263025, 16-99 #b8d88a,#263025,
+|20: 0-0 d,#608269, 1-3 d,#0d1410, 4-11 #59675c,#0d1410, 12-99 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-3 d,#0d1410, 4-7 #59675c,#0d1410, 8-99 #e5eee5,#0d1410,
+|22: 0-0 d,#608269, 1-3 d,#0d1410, 4-99 #59675c,#0d1410,
+|23: 0-0 d,#608269, 1-99 d,#0d1410,
+|24: 0-0 d,#608269, 1-99 d,#0d1410,
+|25: 0-0 d,#608269, 1-99 d,#0d1410,
+|26: 0-0 d,#608269, 1-99 d,#0d1410,
+|27: 0-0 d,#608269, 1-99 d,#0d1410,
+|28: 0-0 d,#608269, 1-99 d,#0d1410,
+|29: 0-0 d,#608269, 1-99 d,#0d1410,
+|30: 0-0 d,#608269, 1-99 d,#0d1410,
+== snap look-added-line grid=100x31 cursor=55,22
+|16: /tmp/pardes-snap/diffview/cwd/src/shape.zig Save Tty Collapse Del TreeContext
+|17: 1 const std = @import("std");
+|18: 2
+|19: 3 pub const Shape = union(enum) {
+|20: 4 circle: f32,
+|21: 5 square: f32,
+|22: 6 triangle: struct { base: f32, height: f32 },
+|23: 7
+|24: 8 const note =
+|25: 9 \\areas, defer
+|26: 10 \\summed
+|27: 11 ;
+|28: 12 };
+|29: 13
+== snap command-pane grid=100x31 cursor=7,3
+|Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit
+| New Tty Find Grep Joincol Delcol
+| /tmp/pardes-snap/diffview/cwd/cmds.txt Save Tty Collapse Del
+| 1 cat change.diff Exec
+| 2
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| /tmp/pardes-snap/diffview/cwd (cat change.diff) exit 0 Kill Save Collapse Del
+| circle: f32,
+| - square: u8 = 7,
+| + square: f32,
+| + triangle: struct { base: f32, height: f32 },
+|
+| const note =
+| - \\areas
+| + \\areas, defer
+| + \\summed
+| ;
+| };
+| exit 0
+== style command-colours grid=100x31
+|0: 0-99 #b2c3b8,#1d2824,
+|1: 0-1 d,#887455, 2-2 d,#314738, 3-99 #becfc4,#314738,
+|2: 0-1 d,#add38a, 2-2 d,#314738, 3-32 #becfc4,#314738, 33-41 #dfbce5,#314738, 42-99 #becfc4,#314738,
+|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-4 d,#0d1410,b 5-6 #616f64,#0d1410,b 7-93 #e5eee5,#0d1410, 94-94 #e5eee5,#1d2824, 95-99 #b2c3b8,#1d2824,
+|4: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
+|5: 0-0 d,#608269, 1-99 d,#0d1410,
+|6: 0-0 d,#608269, 1-99 d,#0d1410,
+|7: 0-0 d,#608269, 1-99 d,#0d1410,
+|8: 0-0 d,#608269, 1-99 d,#0d1410,
+|9: 0-0 d,#608269, 1-99 d,#0d1410,
+|10: 0-0 d,#608269, 1-99 d,#0d1410,
+|11: 0-0 d,#608269, 1-99 d,#0d1410,
+|12: 0-0 d,#608269, 1-99 d,#0d1410,
+|13: 0-0 d,#608269, 1-99 d,#0d1410,
+|14: 0-0 d,#608269, 1-99 d,#0d1410,
+|15: 0-0 d,#608269, 1-99 d,#0d1410,
+|16: 0-0 d,#608269, 1-99 d,#0d1410,
+|17: 0-1 d,#496656, 2-2 d,#1d2824, 3-99 #b2c3b8,#1d2824,
+|18: 0-0 d,#152019, 1-6 d,#0d1410, 7-99 #e5eee5,#0d1410,
+|19: 0-0 d,#152019, 1-1 d,#0d1410, 2-2 #f1a39b,#302b25,b 3-6 #f1a39b,#302b25, 7-19 #e5eee5,#302b25, 20-20 #f8c082,#302b25, 21-99 #e5eee5,#302b25,
+|20: 0-0 d,#152019, 1-1 d,#0d1410, 2-2 #b6ce94,#263025,b 3-6 #b6ce94,#263025, 7-99 #e5eee5,#263025,
+|21: 0-0 d,#152019, 1-1 d,#0d1410, 2-2 #b6ce94,#263025,b 3-6 #b6ce94,#263025, 7-16 #e5eee5,#263025, 17-23 #c9a7ed,#263025, 24-99 #e5eee5,#263025,
+|22: 0-0 d,#152019, 1-99 d,#0d1410,
+|23: 0-0 d,#608269, 1-6 d,#0d1410, 7-99 #e5eee5,#0d1410,
+|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-2 #f1a39b,#302b25,b 3-10 #f1a39b,#302b25, 11-99 #b8d88a,#302b25,
+|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-2 #b6ce94,#263025,b 3-10 #b6ce94,#263025, 11-99 #b8d88a,#263025,
+|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-2 #b6ce94,#263025,b 3-10 #b6ce94,#263025, 11-99 #b8d88a,#263025,
+|27: 0-0 d,#608269, 1-6 d,#0d1410, 7-99 #e5eee5,#0d1410,
+|28: 0-0 d,#608269, 1-2 d,#0d1410, 3-99 #e5eee5,#0d1410,
+|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-99 #e5eee5,#0d1410,
+|30: 0-0 d,#152019, 1-99 d,#0d1410,
+== snap command-look grid=100x31 cursor=23,10
+|3: 1 cat change.diff
+|5: /tmp/pardes-snap/diffview/cwd/src/shape.zig Save Tty Collapse Del TreeContext
+|6: 1 const std = @import("std");
+|7: 2
+|8: 3 pub const Shape = union(enum) {
+|9: 4 circle: f32,
+|10: 5 square: f32,
+|11: 6 triangle: struct { base: f32, height: f32 },
+|12: 7
+|13: 8 const note =
+|14: 9 \\areas, defer
+|15: 10 \\summed
+|16: 11 ;
diff --git a/test/snapshots/diffview.snap b/test/snapshots/diffview.snap
new file mode 100644
index 00000000..23d060b7
--- /dev/null
+++ b/test/snapshots/diffview.snap
@@ -0,0 +1,37 @@
+# A diff pane, for reviewing a change: each hunk's code coloured as Zig (a
+# multi-line string over three lines, `defer` inside it no keyword), added and
+# removed rows tinted to the pane's edge with their prefix in the tint's hue,
+# headers muted. Then a right click on an added line's `+` opens the new file
+# at that line: the path is the repository's (a `.git` above the diff), b/
+# dropped.
+dirmk .git
+dirmk src
+file src/shape.zig const std = @import("std");\n\npub const Shape = union(enum) {\n circle: f32,\n square: f32,\n triangle: struct { base: f32, height: f32 },\n\n const note =\n \\\\areas, defer\n \\\\summed\n ;\n};\n
+file change.diff diff --git a/src/shape.zig b/src/shape.zig\nindex 1111111..2222222 100644\n--- a/src/shape.zig\n+++ b/src/shape.zig\n@@ -1,10 +1,12 @@\n const std = @import("std");\n \n pub const Shape = union(enum) {\n circle: f32,\n- square: u8 = 7,\n+ square: f32,\n+ triangle: struct { base: f32, height: f32 },\n \n const note =\n- \\\\areas\n+ \\\\areas, defer\n+ \\\\summed\n ;\n };\n
+start 31 100 change.diff
+wait 8000 Newcol Joincol
+stable 700 20000
+snap diff-pane
+snapstyle diff-colours
+press right 8 15
+release right 8 15
+stable 700 15000
+snap look-added-line
+# The same diff printed by a command (`git diff`, here `cat`) into a command
+# pane: its output is told for a diff and coloured the same, over the colours
+# the program printed, and a right click on a `+` there opens the file too.
+file cmds.txt cat change.diff\n
+start 31 100 cmds.txt
+wait 8000 cmds.txt
+stable 700 20000
+press middle 7 4
+drag middle 22 4
+release middle 22 4
+wait 10000 exit 0
+stable 700 15000
+snap command-pane
+snapstyle command-colours
+press right 3 21
+release right 3 21
+stable 700 15000
+snap command-look
diff --git a/test/snapshots/lspcomplete.snap b/test/snapshots/lspcomplete.snap
index d9e165a1..eaa26c61 100644
--- a/test/snapshots/lspcomplete.snap
+++ b/test/snapshots/lspcomplete.snap
@@ -1,4 +1,8 @@
-# Tab after a dot in insert mode: "what could go here". The answer is a
+# Tab after a dot in insert mode: "what could go here".
+#
+# Each wait for zls's answer allows a minute: it returns as soon as the answer
+# is drawn (2 s alone), and a zls slowed by a loaded machine (the parallel
+# snap run) took over 15 s, which failed the old wait. The answer is a
# `+Search` buffer of zls's candidates, one row each at the cursor (a
# completion item names no declaration) — nothing is inserted and nothing pops
# up.
@@ -62,8 +66,8 @@ key tab
# The rows that prove it: `verdigris` and `cinnabar` can only have come from
# resolving the switch's condition to `Shade` and asking what it declares.
# Line 8 still reads `.` and nothing else — a completion INSERTS NOTHING.
-wait 15000 +Search
-stable 700 15000
+wait 60000 +Search
+stable 700 30000
snap completion
# Two more Tabs. Every one of them answers, and every answer REFILLS the one
# `+Search` rather than opening its twin. The SECOND press is already the whole
@@ -78,7 +82,7 @@ settle 150
key tab
settle 150
key tab
-stable 700 20000
+stable 700 30000
snap refill
# The pane is still in insert mode, which is right — the query answered beside
# the cursor and typing goes on — so walking the answer means leaving it first,
@@ -127,8 +131,8 @@ key end
text .
stable 400 5000
key tab
-wait 15000 +Search
-stable 700 15000
+wait 60000 +Search
+stable 700 30000
snap nosemi
# ...and a construct still hanging OPEN. The switch has no closing brace at
# all — the one `}` in the file closes it and leaves the function unclosed —
@@ -148,6 +152,6 @@ key end
text .
stable 400 5000
key tab
-wait 15000 +Search
-stable 700 15000
+wait 60000 +Search
+stable 700 30000
snap nobrace