Tests for 5675.

This commit is contained in:
Nicholas Marriott
2026-10-06 18:50:37 +01:00
parent 81dc40dedf
commit 059df9643c
7 changed files with 806 additions and 31 deletions

View File

@@ -1,5 +1,10 @@
#!/bin/sh
# Verify palette and clipboard payload delivery with both OSC terminators,
# fragmented replies and a separate Escape before the reply. Invalid payloads
# must leave the request pending for a subsequent valid reply. Check that
# foreground and background reports update the colours queried by a pane.
PATH=/bin:/usr/bin
TERM=screen
@@ -8,6 +13,7 @@ TERM=screen
python3 - "$TEST_TMUX" <<'PY'
import os
import select
import shlex
import signal
import subprocess
import sys
@@ -49,13 +55,13 @@ def read_until(fd, needle, timeout=5):
raise RuntimeError("did not see terminal request %r in %r" %
(needle, data))
def wait_file(path, timeout=5):
def wait_file(path, length, timeout=5):
end = time.time() + timeout
while time.time() < end:
try:
with open(path, "rb") as f:
data = f.read()
if data:
if len(data) >= length:
return data
except FileNotFoundError:
pass
@@ -66,6 +72,13 @@ def respawn(command):
run("respawn-window", "-k", "-t", "requests:0", command)
time.sleep(0.2)
def wait_pane(expected, context):
end = time.monotonic() + 5
while run("capture-pane", "-p").stdout.strip() != expected:
assert time.monotonic() < end, (context, "unexpected pane bytes",
run("capture-pane", "-p").stdout.strip())
time.sleep(.01)
def cleanup(pid=None):
if pid is not None:
try:
@@ -73,6 +86,8 @@ def cleanup(pid=None):
except ProcessLookupError:
pass
run("kill-server", check=False)
if pid is not None:
os.waitpid(pid, 0)
run("kill-server", check=False)
run("new-session", "-d", "-x", "80", "-y", "24", "-s", "requests",
@@ -82,37 +97,97 @@ pid, fd = attach()
try:
time.sleep(0.5)
with tempfile.NamedTemporaryFile(delete=False) as f:
palette_out = f.name
respawn("stty raw -echo min 1 time 50; "
"printf '\\033]4;99;?\\033\\\\'; "
"dd bs=1 count=27 2>/dev/null | cat -v >%s; sleep 1" %
palette_out)
read_until(fd, b"\033]4;99;?\033\\")
os.write(fd, b"\033]4;99;rgb:0101/0202/0303\033\\")
got = wait_file(palette_out)
expected = b"^[]4;99;rgb:0101/0202/0303^[\\"
if got != expected:
raise AssertionError("palette reply: expected %r got %r" %
(expected, got))
run("set-option", "-g", "assume-paste-time", "0")
run("set-option", "-s", "escape-time", "1000")
run("set-option", "-g", "@seen", "")
run("bind-key", "-n", "Escape", "set-option", "-gF", "@seen",
"#{@seen}E")
run("set-option", "-s", "set-clipboard", "on")
run("set-option", "-s", "get-clipboard", "request")
with tempfile.NamedTemporaryFile(delete=False) as f:
clip_out = f.name
respawn("stty raw -echo min 1 time 50; "
"printf '\\033]52;c;?\\033\\\\'; "
"dd bs=1 count=21 2>/dev/null | cat -v >%s; sleep 1" %
clip_out)
data = read_until(fd, b"]52;")
if b"?" not in data:
raise RuntimeError("clipboard request missing query in %r" % data)
os.write(fd, b"\033]52;c;UmVxdWVzdA==\033\\")
got = wait_file(clip_out)
expected = b"^[]52;c;UmVxdWVzdA==^[\\"
if got != expected:
raise AssertionError("clipboard reply: expected %r got %r" %
(expected, got))
expected_escapes = b""
replies = [
("palette", "\\033]4;99;?\\033\\\\", b"\033]4;99;?\033\\",
b"\033]4;99;rgb:0101/0202/0303", b"\033]4;99;invalid"),
("clipboard", "\\033]52;c;?\\033\\\\", b"]52;",
b"\033]52;c;UmVxdWVzdA==", b"\033]52;c;!!!!"),
]
with tempfile.TemporaryDirectory() as directory:
output = os.path.join(directory, "reply")
for name, query, needle, payload, invalid in replies:
for prefixed in (False, True):
for split in (False, True):
for terminator in (b"\007", b"\033\\"):
context = (name, prefixed, split, terminator)
# Replies to the pane use its request's ST terminator,
# regardless of the terminal reply's terminator.
expected = payload + b"\033\\"
respawn("stty raw -echo min 1 time 50; "
"printf '%s'; dd bs=1 count=%d 2>/dev/null >%s; "
"exec cat -v" %
(query, len(expected), shlex.quote(output)))
data = read_until(fd, needle)
assert b"?" in data, (context, "query missing", data)
os.write(fd, invalid + terminator)
time.sleep(.05)
with open(output, "rb") as f:
assert f.read() == b"", (context, "invalid payload delivered")
wire = (b"\033" if prefixed else b"") + payload + terminator
if split:
os.write(fd, wire[:-1])
time.sleep(.05)
with open(output, "rb") as f:
assert f.read() == b"", (context, "partial payload delivered")
os.write(fd, wire[-1:])
else:
os.write(fd, wire)
got = wait_file(output, len(expected))
assert got == expected, (context, expected, got)
if prefixed:
expected_escapes += b"E"
seen = run("show-option", "-gv", "@seen").stdout.strip()
assert seen == expected_escapes, (context, "Escape count", seen)
# Check that decoding did not deliver the reply twice
# or leak raw bytes after delivering the payload.
os.write(fd, b"Z")
wait_pane(b"Z", context)
colour_value = 1
for colour in (10, 11):
for prefixed in (False, True):
for terminator in (b"\007", b"\033\\"):
context = ("colour", colour, prefixed, terminator)
# A different colour each time prevents an earlier
# successful report from masking a later ignored one.
payload = b"\033]%d;rgb:%04x/0202/0303" % (
colour, colour_value * 257)
colour_value += 1
wire = (b"\033" if prefixed else b"") + payload + terminator
if prefixed:
os.write(fd, wire[:-1])
time.sleep(.05)
assert run("capture-pane", "-p").stdout.strip() == b"Z", (
context, "partial reply leaked")
os.write(fd, wire[-1:] + b"X")
expected_escapes += b"E"
else:
os.write(fd, wire + b"X")
wait_pane(b"ZX", context)
seen = run("show-option", "-gv", "@seen").stdout.strip()
assert seen == expected_escapes, (context, "Escape count", seen)
expected = payload + b"\033\\"
respawn("stty raw -echo min 1 time 50; "
"printf '\\033]%d;?\\033\\\\'; "
"dd bs=1 count=%d 2>/dev/null >%s; exec cat -v" %
(colour, len(expected), shlex.quote(output)))
got = wait_file(output, len(expected))
assert got == expected, (context, "colour not applied", got)
os.write(fd, b"Z")
wait_pane(b"Z", context)
finally:
cleanup(pid)
os.close(fd)
PY

View File

@@ -0,0 +1,97 @@
#!/bin/sh
# An Escape followed by an extended key must remain a separate key. The
# sequence's leading Escape must not be consumed as the second half of
# M-Escape.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$INNER new-session -d -s inner -x80 -y10 "stty raw -echo; exec cat -v" ||
exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g assume-paste-time 0 || exit 1
$INNER set-option -s escape-time 1000 || exit 1
$INNER set-option -s extended-keys on || exit 1
$INNER unbind-key -a -T root || exit 1
$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g assume-paste-time 0 || exit 1
i=0
while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do
[ "$i" -lt 100 ] || fail 'inner client did not attach'
sleep 0.05
i=$((i + 1))
done
bind_key()
{
$INNER bind-key -n "$1" send-keys -l "$2" || exit 1
}
bind_key Escape E
bind_key M-Escape A
bind_key C-a C
bind_key M-C-a M
bind_key M-Up U
bind_key M-DC D
expected_output=
send_raw()
{
# Hex arguments avoid command separators in a partial sequence.
bytes=$(printf '%s' "$1" | od -An -v -tx1)
$OUTER send-keys -H $bytes || exit 1
}
assert_events()
{
name=$1
expected_output=$expected_output$2
send_raw "$3"
if [ "$#" -eq 4 ]; then
sleep 0.05
send_raw "$4"
fi
i=0
while [ "$i" -lt 100 ]; do
actual=$($INNER capture-pane -p | tr -d '\n')
[ "$actual" = "$expected_output" ] && return 0
sleep 0.05
i=$((i + 1))
done
fail "$name: got '$actual', expected '$expected_output'"
}
assert_events 'CSI u' C "$(printf '\033[97;5u')"
assert_events 'Escape and CSI u' EC "$(printf '\033\033[97;5u')"
assert_events 'Escape and xterm' EC "$(printf '\033\033[27;5;97~')"
assert_events 'Escape and CSI u with Meta' EM "$(printf '\033\033[97;7u')"
assert_events 'split CSI u' EC "$(printf '\033\033[97;')" '5u'
assert_events 'split xterm' EC "$(printf '\033\033[27;5')" ';97~'
assert_events 'CSI u split after two Escapes' EC "$(printf '\033\033')" '[97;5u'
assert_events 'xterm split after two Escapes' EC "$(printf '\033\033')" '[27;5;97~'
assert_events 'M-Escape' A "$(printf '\033\033')"
assert_events 'M-Up' U "$(printf '\033\033[A')"
assert_events 'M-DC' D "$(printf '\033\033[3~')"
exit 0

View File

@@ -0,0 +1,143 @@
#!/bin/sh
# An Escape followed by a focus report must remain a separate key. The
# report's leading Escape must not be consumed as the second half of M-Escape,
# and the focus change must still reach the client and the pane.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$INNER new-session -d -s inner -x80 -y10 \
"printf '\033[?1004h'; stty raw -echo; exec cat -v" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g assume-paste-time 0 || exit 1
$INNER set-option -g focus-events on || exit 1
$INNER set-option -s escape-time 1000 || exit 1
$INNER unbind-key -a -T root || exit 1
$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g assume-paste-time 0 || exit 1
i=0
while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do
[ "$i" -lt 100 ] || fail 'inner client did not attach'
sleep 0.05
i=$((i + 1))
done
bind_key()
{
$INNER bind-key -n "$1" send-keys -l "$2" || exit 1
}
bind_key Escape E
bind_key M-Escape A
bind_key M-Up U
send_raw()
{
# Hex arguments avoid command separators in a partial sequence.
bytes=$(printf '%s' "$1" | od -An -v -tx1)
$OUTER send-keys -H $bytes || exit 1
}
capture()
{
$INNER capture-pane -p | tr -d '\n'
}
focused()
{
case $($INNER list-clients -F '#{client_flags}') in
*focused*)
echo yes
;;
*)
echo no
;;
esac
}
# Start unfocused, whatever the state after attaching, once the pane has
# enabled focus reporting.
i=0
while :; do
send_raw "$(printf '\033[I\033[O')"
sleep 0.05
case $(capture) in
*'^[[O')
[ "$(focused)" = no ] && break
;;
esac
[ "$i" -lt 100 ] || fail 'pane did not receive focus reports'
i=$((i + 1))
done
expected_output=$(capture)
# A focus report is forwarded to the pane directly, while the Escape binding
# runs from the command queue, so the pane may see them in either order. The
# second expected output, if not empty, is that other order.
assert_events()
{
name=$1
expected=$expected_output$2
if [ -n "$3" ]; then
alternative=$expected_output$3
else
alternative=$expected
fi
expected_focus=$4
send_raw "$5"
if [ "$#" -eq 6 ]; then
sleep 0.05
send_raw "$6"
fi
i=0
while [ "$i" -lt 100 ]; do
actual=$(capture)
actual_focus=$(focused)
if [ "$actual" = "$expected" ] ||
[ "$actual" = "$alternative" ]; then
if [ "$actual_focus" = "$expected_focus" ]; then
expected_output=$actual
return 0
fi
fi
sleep 0.05
i=$((i + 1))
done
fail "$name: got '$actual' focused=$actual_focus," \
"expected '$expected' focused=$expected_focus"
}
assert_events 'FocusIn' '^[[I' '' yes "$(printf '\033[I')"
assert_events 'FocusOut' '^[[O' '' no "$(printf '\033[O')"
assert_events 'Escape and FocusIn' 'E^[[I' '^[[IE' yes \
"$(printf '\033\033[I')"
assert_events 'Escape and FocusOut' 'E^[[O' '^[[OE' no \
"$(printf '\033\033[O')"
assert_events 'split FocusIn' 'E^[[I' '^[[IE' yes \
"$(printf '\033\033[')" 'I'
assert_events 'FocusOut split after two Escapes' 'E^[[O' '^[[OE' no \
"$(printf '\033\033')" '[O'
assert_events 'M-Escape' A '' no "$(printf '\033\033')"
assert_events 'M-Up' U '' no "$(printf '\033\033[A')"
exit 0

View File

@@ -0,0 +1,98 @@
#!/bin/sh
# An Escape followed by a mouse report must remain a separate key. The
# report's leading Escape must not be consumed as the second half of M-Escape.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$INNER new-session -d -s inner -x80 -y10 \
"stty raw -echo; printf '\033[?1003h\033[?1006h'; exec cat -v" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g assume-paste-time 0 || exit 1
$INNER set-option -s escape-time 1000 || exit 1
$INNER unbind-key -a -T root || exit 1
$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g assume-paste-time 0 || exit 1
i=0
while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do
[ "$i" -lt 100 ] || fail 'inner client did not attach'
sleep 0.05
i=$((i + 1))
done
bind_key()
{
$INNER bind-key -n "$1" send-keys -l "$2" || exit 1
}
bind_key Escape E
bind_key M-Escape A
bind_key M-a B
bind_key M-Up U
expected_output=
send_raw()
{
# Hex arguments avoid command separators in a partial mouse report.
bytes=$(printf '%s' "$1" | od -An -v -tx1)
$OUTER send-keys -H $bytes || exit 1
}
assert_events()
{
name=$1
expected_output=$expected_output$2
send_raw "$3"
if [ "$#" -eq 4 ]; then
sleep 0.05
send_raw "$4"
fi
i=0
while [ "$i" -lt 100 ]; do
actual=$($INNER capture-pane -p | tr -d '\n')
[ "$actual" = "$expected_output" ] && return 0
sleep 0.05
i=$((i + 1))
done
fail "$name: got '$actual', expected '$expected_output'"
}
assert_events 'SGR motion' 'E^[[<35;3;3M' "$(printf '\033\033[<35;3;3M')"
assert_events 'SGR press' 'E^[[<0;3;3M' "$(printf '\033\033[<0;3;3M')"
assert_events 'SGR release' 'E^[[<0;3;3m' "$(printf '\033\033[<0;3;3m')"
assert_events 'SGR wheel' 'E^[[<64;3;3M' "$(printf '\033\033[<64;3;3M')"
assert_events 'X10 motion' 'E^[[MC##' "$(printf '\033\033[MC##')"
assert_events 'X10 press' 'E^[[M ##' "$(printf '\033\033[M ##')"
assert_events 'split SGR report' 'E^[[<35;3;3M' "$(printf '\033\033[<35;')" '3;3M'
assert_events 'split mouse prefix' 'E^[[<35;3;3M' "$(printf '\033\033[')" '<35;3;3M'
assert_events 'split after two Escapes' 'E^[[<35;3;3M' "$(printf '\033\033')" '[<35;3;3M'
assert_events 'invalid SGR coordinates' E "$(printf '\033\033[<35;0;3M')"
assert_events 'discarded wheel release' E "$(printf '\033\033[<64;3;3m')"
assert_events 'M-Escape' A "$(printf '\033\033')"
assert_events 'M-Escape and bracket' 'A[' "$(printf '\033\033[')"
assert_events 'M-a' B "$(printf '\033a')"
assert_events 'M-Up' U "$(printf '\033\033[A')"
exit 0

87
regress/tty-keys-paste.sh Normal file
View File

@@ -0,0 +1,87 @@
#!/bin/sh
# Paste state must change only when the start or end key completes. A separate
# Escape before paste start uses its binding, pasted keys bypass bindings, and
# the first key after paste end uses its binding again.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$INNER new-session -d -s inner -x80 -y10 \
"stty raw -echo; printf '\033[?2004h'; exec cat -v" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g assume-paste-time 0 || exit 1
$INNER set-option -s escape-time 20 || exit 1
$INNER unbind-key -a -T root || exit 1
$INNER bind-key -n Escape send-keys -l E || exit 1
$INNER bind-key -n a send-keys -l A || exit 1
$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g assume-paste-time 0 || exit 1
i=0
while [ -z "$($INNER list-clients -F '#{client_name}')" ] ||
[ "$($INNER display-message -p '#{bracket_paste_flag}')" != 1 ]; do
[ "$i" -lt 100 ] || fail 'client did not attach or enable bracketed paste'
sleep 0.05
i=$((i + 1))
done
send_raw()
{
bytes=$(printf '%s' "$1" | od -An -v -tx1)
$OUTER send-keys -H $bytes || exit 1
}
assert_output()
{
i=0
while [ "$i" -lt 100 ]; do
actual=$($INNER capture-pane -p | tr -d '\n')
[ "$actual" = "$1" ] && return 0
sleep 0.05
i=$((i + 1))
done
fail "got '$actual', expected '$1'"
}
send_raw "$(printf '\033\033[200~a\033[201~a')"
expected='E^[[200~a^[[201~A'
assert_output "$expected"
# A partial paste end gets more time than escape-time. The final key must
# still be outside the paste when the rest arrives after that time.
send_raw "$(printf '\033[200~a\033[20')"
sleep 0.05
send_raw '1~a'
expected=$expected'^[[200~a^[[201~A'
assert_output "$expected"
# Split the start immediately before its final byte, including an extra
# Escape. Neither the Escape binding nor paste mode may be lost.
$INNER set-option -s escape-time 1000 || exit 1
send_raw "$(printf '\033\033[200')"
sleep 0.05
send_raw "$(printf '~a\033[201~a')"
expected=$expected'E^[[200~a^[[201~A'
assert_output "$expected"
exit 0

148
regress/tty-keys-replies.sh Normal file
View File

@@ -0,0 +1,148 @@
#!/bin/sh
# Test terminal reply state changes, including replies fragmented at every
# byte and replies preceded by a separate Escape. Use a PTY directly so that
# an outer tmux cannot answer startup queries before the test does.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 - "$TEST_TMUX" <<'PY'
import fcntl
import os
import select
import struct
import subprocess
import sys
import termios
import time
tmux = sys.argv[1]
server = [tmux, "", "-f/dev/null"]
def run(*args, check=True):
p = subprocess.run(server + list(args), capture_output=True)
if check and p.returncode != 0:
raise RuntimeError("%r: %s" % (args, p.stderr.decode()))
return p.stdout.decode().strip()
def state():
return run("list-clients", "-F",
"#{client_termfeatures}|#{client_termtype}|"
"#{client_width}x#{client_height}|"
"#{client_cell_width}x#{client_cell_height}")
def matches(name, value):
features, kind, size, cells = value.split("|")
features = set(features.split(","))
if name == "DA":
return {"sixel", "margins", "rectfill", "clipboard"} <= features
if name == "DA2":
return "ignorefkeys" in features
if name == "XDA":
return kind == "ReplyTest"
if name == "sync":
return "sync" in features
if name == "characters":
return size == "61x17"
if name == "pixels":
return cells == "10x20"
reports = [
("DA", b"\033[?65;4;21;28;52c"),
("DA2", b"\033[>85;1;0c"),
("XDA", b"\033P>|ReplyTest\033\\"),
("sync", b"\033[?2026;2$y"),
("characters", b"\033[8;17;61t"),
("pixels", b"\033[4;480;800t"),
]
cases = 0
for name, report in reports:
for prefixed in (False, True):
deliveries = ["whole", "split", "bytewise"]
if name == "sync" and not prefixed:
deliveries += ["invalid", "expired"]
for delivery in deliveries:
context = (name, prefixed, delivery)
pid = fd = None
# Each case needs fresh one-shot startup reply flags. A new
# socket also avoids racing shutdown of the previous server.
server[1] = "-LtestReplies%d-%d" % (os.getpid(), cases)
try:
run("new-session", "-d", "-s", "replies", "-x", "80",
"-y", "24", "stty raw -echo; exec cat -v")
run("set-option", "-g", "status", "off")
run("set-option", "-g", "assume-paste-time", "0")
run("set-option", "-s", "escape-time", "1000")
run("set-option", "-g", "@seen", "")
run("bind-key", "-n", "Escape", "set-option", "-gF",
"@seen", "#{@seen}E")
pid, fd = os.forkpty()
if pid == 0:
os.environ["TERM"] = "xterm-256color"
fcntl.ioctl(0, termios.TIOCSWINSZ,
struct.pack("HHHH", 24, 80, 0, 0))
os.execv(tmux, server + ["attach-session", "-t", "replies"])
output = b""
deadline = time.monotonic() + 5
while b"\033[?2026$p" not in output:
assert time.monotonic() < deadline, (context, "missing query")
if select.select([fd], [], [], .01)[0]:
output += os.read(fd, 65536)
baseline = state()
assert not matches(name, baseline), (context, baseline)
wire = (b"\033" if prefixed else b"") + report
if delivery == "invalid":
# A near match must fall back to ordinary key parsing.
wire = b"\033[?2026;5$y"
if delivery in ("whole", "invalid"):
chunks = [wire]
elif delivery in ("split", "expired"):
chunks = [wire[:-1], wire[-1:]]
else:
chunks = [wire[i:i+1] for i in range(len(wire))]
for part in chunks[:-1]:
os.write(fd, part)
time.sleep(.01)
actual = state()
assert actual == baseline, (context, "premature effect", actual)
assert run("capture-pane", "-p") == "", (context, "partial leak")
expected = "Z"
if delivery == "expired":
# Allow the partial reply's timer to expire. The pending
# query extends it to at least 500ms; wait for fallback
# rather than assuming a fixed expiration time.
expected = wire[:-1].replace(b"\033", b"^[").decode()
deadline = time.monotonic() + 5
while run("capture-pane", "-p") != expected:
assert time.monotonic() < deadline, (context, "timer stalled")
if select.select([fd], [], [], .01)[0]:
os.read(fd, 65536)
if delivery in ("invalid", "expired"):
expected = wire.replace(b"\033", b"^[").decode() + "Z"
# A trailing key confirms that the entire reply has been
# processed, including cases with no extra Escape.
os.write(fd, chunks[-1] + b"Z")
deadline = time.monotonic() + 5
while run("capture-pane", "-p") != expected:
assert time.monotonic() < deadline, (context,
"reply leaked or input stalled", run("capture-pane", "-p"))
if select.select([fd], [], [], .01)[0]:
os.read(fd, 65536)
actual = state()
if delivery in ("invalid", "expired"):
assert actual == baseline, (context, "unexpected effect", actual)
else:
assert matches(name, actual), (context, "missing effect", actual)
assert run("show-option", "-gv", "@seen") == (
"E" if prefixed else ""), (context, "Escape count")
cases += 1
finally:
run("kill-server", check=False)
if fd is not None:
os.close(fd)
if pid is not None:
os.waitpid(pid, 0)
PY

View File

@@ -0,0 +1,127 @@
#!/bin/sh
# An Escape followed by a terminal reply must remain a separate key. The
# reply's leading Escape must not be consumed as the second half of a Meta
# key, which would send the rest of the reply to the pane as text. A reply
# split before the end of a known key must wait for the rest.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$INNER new-session -d -s inner -x80 -y10 "stty raw -echo; exec cat -v" ||
exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g assume-paste-time 0 || exit 1
$INNER set-option -s escape-time 1000 || exit 1
$INNER unbind-key -a -T root || exit 1
$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g assume-paste-time 0 || exit 1
i=0
while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do
[ "$i" -lt 100 ] || fail 'inner client did not attach'
sleep 0.05
i=$((i + 1))
done
bind_key()
{
$INNER bind-key -n "$1" send-keys -l "$2" || exit 1
}
bind_key Escape E
bind_key M-Escape A
bind_key M-] B
bind_key M-O P
bind_key M-Up U
send_raw()
{
# Hex arguments avoid command separators in a partial sequence.
bytes=$(printf '%s' "$1" | od -An -v -tx1)
$OUTER send-keys -H $bytes || exit 1
}
capture()
{
$INNER capture-pane -p | tr -d '\n'
}
theme()
{
$INNER list-clients -F '#{client_theme}'
}
expected_output=
assert_keys()
{
name=$1
expected=$expected_output$2
expected_theme=$3
send_raw "$4"
if [ "$#" -eq 5 ]; then
sleep 0.05
send_raw "$5"
fi
i=0
while [ "$i" -lt 100 ]; do
actual=$(capture)
actual_theme=$(theme)
if [ "$actual" = "$expected" ] &&
[ "$actual_theme" = "$expected_theme" ]; then
expected_output=$actual
return 0
fi
sleep 0.05
i=$((i + 1))
done
fail "$name: got '$actual' theme=$actual_theme," \
"expected '$expected' theme=$expected_theme"
}
assert_keys 'Escape and dark theme report' E dark \
"$(printf '\033\033[?997;1n')"
assert_keys 'Escape and light theme report' E light \
"$(printf '\033\033[?997;2n')"
assert_keys 'split theme report' '' dark \
"$(printf '\033[?997')" ';1n'
assert_keys 'Escape and light theme report, split' E light \
"$(printf '\033\033[?99')" '7;2n'
assert_keys 'theme report split after two Escapes' E dark \
"$(printf '\033\033')" '[?997;1n'
assert_keys 'Escape and light theme report again' E light \
"$(printf '\033\033[?997;2n')"
assert_keys 'Escape and background colour' E light \
"$(printf '\033\033]11;rgb:ffff/ffff/ffff\033\\')"
assert_keys 'Escape and foreground colour (BEL)' E light \
"$(printf '\033\033]10;rgb:0000/0000/0000\007')"
assert_keys 'split background colour' E light \
"$(printf '\033\033]11;rgb:ff')" "$(printf 'ff/ffff/ffff\033\\')"
assert_keys 'Escape and palette colour' E light \
"$(printf '\033\033]4;1;rgb:ffff/0000/0000\033\\')"
assert_keys 'Escape and clipboard' E light \
"$(printf '\033\033]52;c;dGVzdA==\007')"
assert_keys 'M-Escape' A light "$(printf '\033\033')"
assert_keys 'M-]' B light "$(printf '\033]')"
assert_keys 'M-O' P light "$(printf '\033O')"
assert_keys 'M-Up' U light "$(printf '\033\033[A')"
exit 0