Extend tests.

This commit is contained in:
Nicholas Marriott
2026-07-05 10:19:58 +01:00
parent 5068e4149a
commit bd39ce0d49
2 changed files with 467 additions and 247 deletions

View File

@@ -1,18 +1,16 @@
#!/bin/sh #!/bin/sh
# Exercise format/style rendering in live contexts. These paths use # Exercise format and style rendering in live contexts.
# format_draw rather than plain format expansion, so they have historically
# found different crashes and runaway output.
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LANG=C.UTF-8 LANG=C.UTF-8
LC_ALL=C.UTF-8 LC_ALL=C.UTF-8
export TERM LANG LC_ALL export PATH TERM LANG LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null" TMUX="$TEST_TMUX -Lformat-render-contexts-$$ -f/dev/null"
TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" TMUX2="$TEST_TMUX -Lformat-render-contexts-outer-$$ -f/dev/null"
LIMIT=20000 LIMIT=20000
cleanup() cleanup()
@@ -39,23 +37,22 @@ run_cmd()
bounded() bounded()
{ {
name="$1" name=$1
text="$2" text=$2
n=$(printf '%s' "$text" | wc -c | tr -d ' ')
[ "$n" -le "$LIMIT" ] || fail "$name produced $n bytes" n=$(printf '%s' "$text" | wc -c)
[ "$n" -le "$LIMIT" ] || fail "$name too large: $n bytes"
} }
tmux_run() tmux_run()
{ {
name="$1" name=$1
shift shift
out=$(run_cmd $TMUX "$@" 2>&1) out=$(run_cmd $TMUX "$@" 2>&1)
rc=$? rc=$?
bounded "$name" "$out" bounded "$name" "$out"
[ "$rc" -eq 0 ] || fail "$name failed ($rc): $out" [ "$rc" -eq 0 ] || fail "$name failed: $out"
printf '%s' "$out" printf '%s' "$out"
} }
@@ -70,7 +67,7 @@ capture()
capture_esc() capture_esc()
{ {
out=$(run_cmd $TMUX2 capture-pane -Cep -t out:0 2>/dev/null) out=$(run_cmd $TMUX2 capture-pane -pe -t out:0 2>/dev/null)
rc=$? rc=$?
bounded "capture-pane -e" "$out" bounded "capture-pane -e" "$out"
[ "$rc" -eq 0 ] || fail "capture-pane -e failed" [ "$rc" -eq 0 ] || fail "capture-pane -e failed"
@@ -79,16 +76,15 @@ capture_esc()
assert_alive() assert_alive()
{ {
out=$(tmux_run "server alive ($1)" display-message -p alive) tmux_run "server alive ($1)" display-message -p alive >/dev/null
[ "$out" = "alive" ] || fail "server not alive after $1"
} }
wait_for() wait_for()
{ {
marker="$1" marker=$1
i=0 i=0
while [ "$i" -lt 50 ]; do while [ "$i" -lt 50 ]; do
if capture | grep -q "$marker"; then if capture | grep -F "$marker" >/dev/null 2>&1; then
sleep 0.1 sleep 0.1
return 0 return 0
fi fi
@@ -100,60 +96,69 @@ wait_for()
render_status_options() render_status_options()
{ {
label="$1" label=$1
fmt="$2" fmt=$2
tmux_run "$label status-left" \ tmux_run "$label status-left" \
set-option -g status-left "SL$label: $fmt" >/dev/null set-option -g status-left "SL$label: $fmt" >/dev/null
tmux_run "$label clear status-right" \
set-option -g status-right '' >/dev/null
tmux_run "$label status-format left" \
set-option -g status-format[0] '#{E:status-left}' >/dev/null
tmux_run "$label refresh status-left" refresh-client -S >/dev/null
wait_for "SL$label:"
tmux_run "$label clear status-left" \
set-option -g status-left '' >/dev/null
tmux_run "$label status-right" \ tmux_run "$label status-right" \
set-option -g status-right "SR$label: $fmt" >/dev/null set-option -g status-right "SR$label: $fmt" >/dev/null
tmux_run "$label status-format left/right" \ tmux_run "$label status-format right" \
set-option -g status-format[0] \ set-option -g status-format[0] \
'#{E:status-left}#[align=right]#{E:status-right}' >/dev/null '#[align=right]#{E:status-right}' >/dev/null
wait_for "SL$label" tmux_run "$label refresh status-right" refresh-client -S >/dev/null
wait_for "SR$label:"
assert_alive "$label status-left/right" assert_alive "$label status-left/right"
} }
render_status_format() render_status_format()
{ {
label="$1" label=$1
fmt="$2" fmt=$2
tmux_run "$label clear status-left" set-option -g status-left '' >/dev/null
tmux_run "$label clear status-right" set-option -g status-right '' >/dev/null
tmux_run "$label status-format" \ tmux_run "$label status-format" \
set-option -g status-format[0] "SF$label: $fmt" >/dev/null set-option -g status-format[0] "SF$label: $fmt" >/dev/null
wait_for "SF$label" tmux_run "$label refresh status-format" refresh-client -S >/dev/null
wait_for "SF$label:"
assert_alive "$label status-format" assert_alive "$label status-format"
} }
render_message() render_message()
{ {
label="$1" label=$1
fmt="$2" fmt=$2
tmux_run "$label display-message" \ tmux_run "$label display-message" \
display-message -t fmt:0 -d 1000 "DM$label: $fmt" >/dev/null display-message -t fmt:0 -d 1000 "DM$label: $fmt" >/dev/null
wait_for "DM$label" wait_for "DM$label:"
assert_alive "$label display-message" assert_alive "$label display-message"
} }
render_choose_tree() render_choose_tree()
{ {
label="$1" label=$1
fmt="$2" fmt=$2
tmux_run "$label choose-tree" \ tmux_run "$label choose-tree" \
choose-tree -t fmt:0 -F "CT$label: $fmt" >/dev/null choose-tree -t fmt:0 -F "CT$label: $fmt" >/dev/null
wait_for "CT$label" wait_for "CT$label:"
tmux_run "$label quit choose-tree" send-keys -t fmt:0 q >/dev/null tmux_run "$label quit choose-tree" send-keys -t fmt:0 q >/dev/null
assert_alive "$label choose-tree" assert_alive "$label choose-tree"
} }
render_customize() render_customize()
{ {
label="$1" label=$1
fmt="$2" fmt=$2
tmux_run "$label customize-mode" \ tmux_run "$label customize-mode" \
customize-mode -t fmt:0 -F "CM$label: $fmt" >/dev/null customize-mode -t fmt:0 -F "CM$label: $fmt" >/dev/null
@@ -166,65 +171,30 @@ render_customize()
render_list_output() render_list_output()
{ {
label="$1" label=$1
fmt="$2" fmt=$2
tmux_run "$label bind-key" \ tmux_run "$label bind-key" \
bind-key -T root F12 display-message "LK$label: $fmt" >/dev/null bind-key -T root F12 display-message "LK$label: $fmt" >/dev/null
out=$(tmux_run "$label list-keys" list-keys -T root F12) out=$(tmux_run "$label list-keys" list-keys -T root F12)
bounded "$label list-keys output" "$out" bounded "$label list-keys" "$out"
printf '%s' "$out" | grep -F "LK$label:" >/dev/null 2>&1 ||
fail "$label list-keys missing rendered command"
tmux_run "$label option value" \ tmux_run "$label set show option" \
set-option -g status-left "LO$label: $fmt" >/dev/null set-option -g "@format_render_$label" "SO$label: $fmt" >/dev/null
out=$(tmux_run "$label list-options" show-options -g status-left) out=$(tmux_run "$label show-options" show-options -g "@format_render_$label")
bounded "$label list-options output" "$out" bounded "$label show-options" "$out"
printf '%s' "$out" | grep -F "SO$label:" >/dev/null 2>&1 ||
fail "$label show-options missing option"
assert_alive "$label list-keys/list-options" assert_alive "$label list output"
}
check_sgr_sanity()
{
tmux_run "sgr status style" \
set-option -g status-style fg=default,bg=default >/dev/null
tmux_run "sgr status-format" \
set-option -g status-format[0] \
'#[fg=colour1,bg=default]SGR-STATUS#[default]' >/dev/null
wait_for "SGR-STATUS"
out=$(capture_esc | tail -n 1)
bounded "sgr status capture" "$out"
case "$out" in
*SGR-STATUS*) ;;
*) fail "status SGR capture lost text: $out" ;;
esac
case "$out" in
*"\\033["*) ;;
*) fail "status SGR capture has no escape reset/style data: $out" ;;
esac
tmux_run "sgr message style" \
set-option -g message-style fg=colour2,bg=default >/dev/null
tmux_run "sgr display-message" \
display-message -t fmt:0 -d 1000 \
'#[bg=default]SGR-MESSAGE#[default]' >/dev/null
wait_for "SGR-MESSAGE"
out=$(capture_esc | tail -n 1)
bounded "sgr message capture" "$out"
case "$out" in
*SGR-MESSAGE*) ;;
*) fail "message SGR capture lost text: $out" ;;
esac
case "$out" in
*"\\033["*) ;;
*) fail "message SGR capture has no escape reset/style data: $out" ;;
esac
assert_alive "SGR sanity"
} }
run_corpus() run_corpus()
{ {
label="$1" label=$1
fmt="$2" fmt=$2
render_status_options "$label" "$fmt" render_status_options "$label" "$fmt"
render_status_format "$label" "$fmt" render_status_format "$label" "$fmt"
@@ -234,6 +204,60 @@ run_corpus()
render_list_output "$label" "$fmt" render_list_output "$label" "$fmt"
} }
check_sgr_sanity()
{
esc=$(printf '\033')
tmux_run "set message style fill" \
set-option -g message-style \
"fg=#080808,bg=#ffff00,fill=#ffff00,bold" >/dev/null
tmux_run "show filled message" \
display-message -t fmt:0 -d 1000 \
'#[bg=blue,italics] hello, #[fg=#080808,bg=default] world!' \
>/dev/null
wait_for "hello, world!"
out=$(capture_esc)
printf '%s' "$out" | grep -F 'world!' >/dev/null 2>&1 ||
fail "message text missing from SGR capture"
printf '%s' "$out" | grep "$esc" >/dev/null 2>&1 ||
fail "SGR capture has no escapes"
world_sgr=$(
printf '%s\n' "$out" |
awk -v esc="$esc" '
/world!/ {
i = index($0, "world!")
pre = substr($0, 1, i - 1)
n = split(pre, parts, esc "\\[")
print parts[n]
exit
}'
)
case "$world_sgr" in
*'48;'*) ;;
*) fail "world segment missing explicit background SGR: $world_sgr" ;;
esac
case "$world_sgr" in
*'49'*)
fail "world segment used default background instead of message fill: $world_sgr"
;;
esac
assert_alive "message SGR sanity"
}
wide_fmt()
{
printf 'wide: \0316\0225\0316\0273\0316\0273\0316\0267\0316\0275\0316\0271\0316\0272\0316\0254 \0344\0270\0255\0346\0226\0207 #{=12:\0344\0270\0255\0346\0226\0207abc}'
}
long_style_fmt()
{
printf '%s' '#[fg=colour1,bg=colour2,us=colour3,acs,bright,dim,underscore,blink,reverse,hidden,italics,strikethrough,double-underscore,curly-underscore,dotted-underscore,dashed-underscore,overline,range=user|aaaaaaaaaaaaaaaa,align=absolute-centre,list=on,fill=colour200,width=4294967295,pad=4294967295]OVERLONG-STYLE#[default]'
}
trap cleanup 0 1 15 trap cleanup 0 1 15
cleanup cleanup
@@ -241,11 +265,10 @@ tmux_run "new inner session" \
new-session -d -s fmt -x 100 -y 30 "exec sleep 1000" >/dev/null new-session -d -s fmt -x 100 -y 30 "exec sleep 1000" >/dev/null
tmux_run "new inner window" \ tmux_run "new inner window" \
new-window -t fmt -n second "exec sleep 1000" >/dev/null new-window -t fmt -n second "exec sleep 1000" >/dev/null
tmux_run "select visible window" select-window -t fmt:0 >/dev/null tmux_run "select first window" select-window -t fmt:0 >/dev/null
tmux_run "set status interval" set-option -g status-interval 1 >/dev/null tmux_run "status interval" set-option -g status-interval 1 >/dev/null
tmux_run "set base message style" \ tmux_run "base status" set-option -g status on >/dev/null
set-option -g message-style fg=default,bg=default >/dev/null tmux_run "nested option" \
tmux_run "set nested option" \
set-option -g @nested 'NESTED-#{session_name}-#{window_index}' >/dev/null set-option -g @nested 'NESTED-#{session_name}-#{window_index}' >/dev/null
run_cmd $TMUX2 new-session -d -s out -x 100 -y 30 "$TMUX attach -t fmt" \ run_cmd $TMUX2 new-session -d -s out -x 100 -y 30 "$TMUX attach -t fmt" \
@@ -260,20 +283,11 @@ while [ "$i" -lt 50 ]; do
done done
[ "$i" -lt 50 ] || fail "inner client did not attach" [ "$i" -lt 50 ] || fail "inner client did not attach"
run_corpus A \ run_corpus A 'plain #{session_name}:#{window_index} #{@nested}'
'#[bold,dim,underscore,blink,reverse,italics,strikethrough,double-underscore,curly-underscore,dotted-underscore,dashed-underscore,overline,fg=colour196,bg=colour22,us=colour45]LONGSTYLE#[default]plain' run_corpus B "$(wide_fmt)"
run_corpus B \ run_corpus C '#[fg=colour10,bg=colour17,bold,italics]styled #{pane_current_command}#[default]'
'wide: Ελληνικά 中文 😀 #{=12:中文😀abc}' run_corpus D '#[push-default]#[fg=red,bg=blue]push #{?pane_active,active,inactive}#[pop-default] after'
run_corpus C \ run_corpus E "$(long_style_fmt)"
'#[fg=colour2,bg=colour4]base#[push-default]#[fg=colour1,bg=default]push#[pop-default]pop#[default]'
run_corpus D \
'#[list=on]list#[list=focus]focus#[nolist] #[align=centre]centre#[noalign] #[range=left]L#[range=right]R#[norange]'
run_corpus E \
'#{E:@nested}:#{T:%%H:%%M}:#{?#{==:#{session_name},fmt},yes,no}:#{?0,bad,}'
run_corpus F \
'job:#(printf job-ok)'
run_corpus G \
'empty:#{E:}:#{T:}:#{definitely_not_a_variable}:#{?0,no,}'
check_sgr_sanity check_sgr_sanity

View File

@@ -1,55 +1,176 @@
#!/bin/sh #!/bin/sh
PATH=/bin:/usr/bin TERM=screen PATH=/bin:/usr/bin
TERM=screen
LANG=C.UTF-8
LC_ALL=C.UTF-8
export PATH TERM LANG LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMP=$(mktemp) TMUX="$TEST_TMUX -Linput-reflow-stress-$$ -f/dev/null"
EXP=$(mktemp) TMUX2="$TEST_TMUX -Linput-reflow-stress-outer-$$ -f/dev/null"
trap 'rm -f "$TMP" "$EXP"; $TMUX kill-server 2>/dev/null' 0 1 15
TMP=$(mktemp "${TMPDIR:-/tmp}/input-reflow-stress.XXXXXX") || exit 1
EXP=$(mktemp "${TMPDIR:-/tmp}/input-reflow-stress.XXXXXX") || exit 1
exit_status=0
WIDTHS="80 40 20 10 7 5 4 3 2 1 2 3 4 5 7 10 20 40 80" WIDTHS="80 40 20 10 7 5 4 3 2 1 2 3 4 5 7 10 20 40 80"
LIVE_WIDTHS="68 24 80"
HEIGHT=24
HISTORY_LIMIT=220 HISTORY_LIMIT=220
HISTORY_BOUND=12000 HISTORY_BOUND=12000
JOINED_BOUND=500000 JOINED_BOUND=500000
RAW_BOUND=5000000 RAW_BOUND=5000000
COPY_BOUND=20000
LIVE_BOUND=50000
cleanup()
{
rm -f "$TMP" "$EXP"
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
}
trap cleanup 0 1 15
record_fail() record_fail()
{ {
echo "FAIL: $1" echo "FAIL: $1" >&2
exit_status=1 exit_status=1
} }
u8()
{
printf "$1"
}
wide_a()
{
u8 '\0343\0201\0202'
}
wide_b()
{
u8 '\0347\0225\0214'
}
combining_acute()
{
u8 '\0314\0201'
}
zero_width_space()
{
u8 '\0342\0200\0213'
}
zero_width_joiner()
{
u8 '\0342\0200\0215'
}
replacement()
{
u8 '\0357\0277\0275'
}
assert_alive()
{
$TMUX display-message -p -t stress: alive >/dev/null 2>&1 ||
record_fail "server exited after $1"
}
make_orphan_payload()
{
tag=$1
printf 'OP%s-01|ECH-half|left' "$tag"
wide_b
printf 'right'
printf '\033[6D\033[1X\n'
printf 'OP%s-02|EL-inside|left' "$tag"
wide_b
printf 'right'
printf '\033[6D\033[K\n'
printf 'OP%s-03|overwrite-leading|left' "$tag"
wide_b
printf 'right'
printf '\rOP%s-03|overwrite-leading|left!\n' "$tag"
printf 'OP%s-04|right-edge|' "$tag"
i=0
while [ "$i" -lt 60 ]; do
printf '.'
i=$((i + 1))
done
wide_b
printf 'Z'
printf '\033[2D\033[1X\n'
printf 'OP%s-05|ERASED|left' "$tag"
wide_b
printf 'right\r\033[2KOP%s-05|ERASED|left clear\n' "$tag"
printf 'SENT-%s|ORPHAN-PHASE|complete\n' "$tag"
}
make_payload() make_payload()
{ {
n=0
i=0 i=0
while [ "$i" -lt 16 ]; do while [ "$i" -lt 16 ]; do
printf 'L%04d|ascii|abcdefghijklmnopqrstuvwxyz\n' "$n"; n=$((n + 1)) printf 'ASCII%02d|abcdefghijklmnopqrstuvwxyz\n' "$i"
printf 'L%04d|wrap|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' "$n"; n=$((n + 1))
printf 'L%04d|wide|AあB界C\n' "$n"; n=$((n + 1)) printf 'WIDE%02d|' "$i"
printf 'L%04d|wide-edge|abcdあZ\n' "$n"; n=$((n + 1)) wide_a
printf 'L%04d|combining|é é é\n' "$n"; n=$((n + 1)) printf '|'
printf 'L%04d|orphan-combining|́A\n' "$n"; n=$((n + 1)) wide_b
printf 'L%04d|variation|✔️ ⚠️ 🏝️\n' "$n"; n=$((n + 1)) printf '|tail\n'
printf 'L%04d|emoji|🙂🙂🙂\n' "$n"; n=$((n + 1))
printf 'L%04d|flag|🇬🇧 🇺🇸 🇯🇵\n' "$n"; n=$((n + 1)) printf 'COMB%02d|e' "$i"
printf 'L%04d|mixed|Aあé✔️B\n' "$n"; n=$((n + 1)) combining_acute
printf 'L%04d|style|plain \033[31mred\033[0m plain\n' "$n"; n=$((n + 1)) printf '|zero'
printf 'L%04d|hyperlink|\033]8;;https://example.com/%04d\033\\LINK\033]8;;\033\\\n' "$n" "$n"; n=$((n + 1)) zero_width_space
printf 'L%04d|tabs|a\tb\tc\n' "$n"; n=$((n + 1)) zero_width_joiner
printf 'L%04d|backspace|abc\bX\n' "$n"; n=$((n + 1)) printf '|done\n'
printf 'carriage-return-%04d|abcdef\rL%04d|carriage-return|XY\n' "$n" "$n"; n=$((n + 1))
printf 'STYLE%02d|\033[1;31mred\033[0m|\033[4munder\033[0m\n' \
"$i"
printf 'LINK%02d|\033]8;;https://example.invalid/%d\007link%d\033]8;;\007|end\n' \
"$i" "$i" "$i"
printf 'WRAP%02d|%064d\n' "$i" "$i"
printf 'CRBS%02d|abcdef\rCRBS%02d|XYZ\n' "$i" "$i"
printf 'BS%02d|abc\bZ\n' "$i"
i=$((i + 1)) i=$((i + 1))
done done
i=0
while [ "$i" -lt 6 ]; do
printf 'L%04d|stable|abcdefghijklmnopqrstuvwxyz\n' "$i"
i=$((i + 1))
done
printf 'SENT-A|SURVIVES|plain logical line\n'
printf 'SENT-B|SURVIVES|wide-free after erases\n'
make_orphan_payload PRE
printf 'SENT-C|SURVIVES|tail logical line\n'
} }
make_alternate_payload() make_alternate_payload()
{ {
printf '\033[?1049h' printf '\033[?1049h'
printf 'ALT000|wide|AあB界C\n' printf 'ALT-SENT|alternate screen|'
printf 'ALT001|combining|é é é\n' wide_a
printf 'ALT002|style|plain \033[32mgreen\033[0m plain\n' printf '\nALT-SENT|resize target\n'
printf 'ALT003|emoji|🙂🙂🙂\n' }
exit_alternate_payload()
{
printf '\033[?1049l'
} }
load_and_paste() load_and_paste()
@@ -57,29 +178,20 @@ load_and_paste()
buffer=$1 buffer=$1
shift shift
"$@" | $TMUX load-buffer -b "$buffer" - || exit 1 "$@" >"$EXP"
$TMUX load-buffer -b "$buffer" "$EXP" || exit 1
$TMUX paste-buffer -d -b "$buffer" -t stress:0.0 || exit 1 $TMUX paste-buffer -d -b "$buffer" -t stress:0.0 || exit 1
sleep 0.3 sleep 0.3
} }
assert_alive()
{
alive=$($TMUX display-message -p -t stress: alive 2>&1) || {
record_fail "server died after $1"
return
}
[ "$alive" = alive ] || record_fail "server died after $1"
}
capture_joined() capture_joined()
{ {
$TMUX capture-pane -pNJ -t stress: -S - -E - >"$TMP" $TMUX capture-pane -pNJ -t stress: -S -5000 -E - >"$TMP"
} }
assert_joined_sane() assert_joined_sane()
{ {
label=$1 label=$1
check_sentinels=${2:-yes}
capture_joined || { capture_joined || {
record_fail "joined capture failed after $label" record_fail "joined capture failed after $label"
@@ -91,11 +203,12 @@ assert_joined_sane()
record_fail "joined capture too large after $label: $bytes" record_fail "joined capture too large after $label: $bytes"
fi fi
if grep -q "$(printf '\357\277\275')" "$TMP"; then if grep -q "$(replacement)" "$TMP"; then
record_fail "replacement character in joined capture after $label" record_fail "replacement character in joined capture after $label"
fi fi
awk -v label="$label" ' out=$(
awk -v label="$label" '
{ {
line = $0 line = $0
seen_on_line = 0 seen_on_line = 0
@@ -103,120 +216,129 @@ assert_joined_sane()
id = substr(line, RSTART + 1, 4) + 0 id = substr(line, RSTART + 1, 4) + 0
seen_on_line++ seen_on_line++
seen[id]++ seen[id]++
if (id < last) { ids[++count] = id
printf("IDs out of order after %s: L%04d after L%04d\n",
label, id, last)
exit 1
}
if (seen[id] > 1) {
printf("duplicate ID after %s: L%04d\n", label, id)
exit 1
}
last = id
found = 1 found = 1
last = id
line = substr(line, RSTART + RLENGTH) line = substr(line, RSTART + RLENGTH)
} }
if (seen_on_line > 1) { if (seen_on_line > 1)
printf("fused IDs after %s: %s\n", label, $0) printf("fused IDs after %s: %s\n", label, $0)
exit 1
}
} }
END { END {
if (!found) { if (!found) {
printf("no line IDs after %s\n", label) printf("no line IDs after %s\n", label)
exit 1 exit
} }
} for (i = 2; i <= count; i++) {
' "$TMP" || { if (ids[i] < ids[i - 1])
cp "$TMP" "$EXP" drops++
record_fail "joined structure failed after $label" }
if (drops > 1)
printf("IDs out of order after %s\n", label)
for (id in seen) {
if (seen[id] > 2)
printf("duplicate ID after %s: L%04d\n", label, id)
}
}' "$TMP"
)
[ -z "$out" ] || record_fail "$out"
for marker in \
'SENT-A|SURVIVES|plain logical line' \
'SENT-B|SURVIVES|wide-free after erases' \
'SENT-C|SURVIVES|tail logical line'
do
grep -F "$marker" "$TMP" >/dev/null 2>&1 ||
record_fail "missing sentinel after $label: $marker"
done
if grep '^OP.*-05|ERASED|.*' "$TMP" | grep -q "$(wide_b)"; then
record_fail "erased wide character visible after $label"
fi
}
assert_final_logical_text()
{
label=$1
capture_joined || {
record_fail "joined capture failed after $label"
return
} }
if [ "$check_sentinels" = yes ]; then for marker in \
for expected in \ 'SENT-A|SURVIVES|plain logical line' \
'L0234|mixed|' \ 'SENT-B|SURVIVES|wide-free after erases' \
'L0236|hyperlink|' \ 'SENT-C|SURVIVES|tail logical line' \
'L0239|carriage-return|XY'; do 'OPPRE-03|overwrite-leading|left!' \
if ! grep -Fq "$expected" "$TMP"; then 'OPPOST-03|overwrite-leading|left!' \
printf '%s\n' "$expected" >"$EXP" 'SENT-POST|ORPHAN-PHASE|complete'
record_fail "missing sentinel after $label" do
fi grep -F "$marker" "$TMP" >/dev/null 2>&1 ||
done record_fail "missing expected logical text after $label: $marker"
fi done
} }
assert_raw_sane() assert_raw_sane()
{ {
label=$1 label=$1
width=$2
$TMUX capture-pane -pR -t stress: >"$TMP" || { $TMUX capture-pane -pR -t stress: >"$TMP" ||
record_fail "raw capture failed after $label" record_fail "raw capture failed after $label"
return
}
bytes=$(wc -c <"$TMP") bytes=$(wc -c <"$TMP")
if [ "$bytes" -gt "$RAW_BOUND" ]; then if [ "$bytes" -gt "$RAW_BOUND" ]; then
record_fail "raw capture too large after $label: $bytes" record_fail "raw capture too large after $label: $bytes"
fi fi
awk -v label="$label" -v width="$width" ' width=$($TMUX display-message -p -t stress: '#{pane_width}' 2>/dev/null)
/^C / { case "$width" in
cell = $0 ''|*[!0-9]*) width=0 ;;
sub(/^C /, "", cell) esac
split(cell, fields, " ")
split(fields[1], position, ",") out=$(
row = position[1] + 0 awk -v label="$label" -v width="$width" '
col = position[2] + 0 {
if (col >= width) { sub(/^[ ]+/, "")
printf("cell column outside pane after %s: %s\n", label, $0) if ($1 != "C")
exit 1 next
}
coord = $2
sub(/^[0-9]*,/, "", coord)
sub(/[^0-9].*$/, "", coord)
col = coord + 0
if (width > 0 && (col < 0 || col >= width))
printf("cell column outside width after %s: width %d: %s\n", label, width, $0)
data = $0 data = $0
if (data !~ /data=\(/) if (data !~ /data=\(/) {
printf("cell without data after %s: %s\n", label, $0)
next next
}
sub(/^.*data=\(/, "", data) sub(/^.*data=\(/, "", data)
split(data, data_fields, ",") split(data, parts, ",")
cell_width = data_fields[1] + 0 cell_width = parts[1] + 0
padding = ($0 ~ /flags=PADDING/) padding = ($0 ~ /flags=[^ ]*PADDING/)
if (!padding && cell_width == 0)
if (!padding && cell_width == 0) { printf("visible zero-width cell after %s: %s\n", label, $0)
printf("zero-width non-padding cell after %s: %s\n", label, $0) }' "$TMP"
exit 1 )
} [ -z "$out" ] || record_fail "$out"
if (padding && !(last_row == row && last_width > 1 && !last_padding)) {
printf("padding without preceding wide cell after %s: %s\n", label, $0)
exit 1
}
last_row = row
last_width = cell_width
last_padding = padding
}
' "$TMP" || {
cp "$TMP" "$EXP"
record_fail "raw structure failed after $label"
}
} }
assert_history_sane() assert_history_sane()
{ {
label=$1 label=$1
size=$($TMUX display-message -p -t stress: '#{history_size}' 2>/dev/null)
size=$($TMUX display-message -p -t stress: '#{history_size}') || {
record_fail "history-size failed after $label"
return
}
case "$size" in case "$size" in
''|*[!0-9]*) ''|*[!0-9]*)
record_fail "bad history size after $label: $size" record_fail "bad history size after $label: $size"
;; ;;
*) *)
if [ "$size" -gt "$HISTORY_BOUND" ]; then if [ "$size" -gt "$HISTORY_BOUND" ]; then
record_fail "history too large after $label: $size" record_fail "history too large after $label: $size"
fi fi
;; ;;
esac esac
} }
@@ -224,18 +346,18 @@ assert_copy_mode_sane()
{ {
label=$1 label=$1
$TMUX capture-pane -pM -t stress: >"$TMP" || { $TMUX capture-pane -pM -S -5000 -E - -t stress: >"$TMP" ||
record_fail "copy-mode capture failed after $label" record_fail "copy-mode capture failed after $label"
return
}
bytes=$(wc -c <"$TMP") bytes=$(wc -c <"$TMP")
if [ "$bytes" -gt 20000 ]; then if [ "$bytes" -gt "$COPY_BOUND" ]; then
record_fail "copy-mode capture too large after $label" record_fail "copy-mode capture too large after $label: $bytes"
fi fi
if grep -q "$(printf '\357\277\275')" "$TMP"; then
if grep -q "$(replacement)" "$TMP"; then
record_fail "replacement character in copy-mode after $label" record_fail "replacement character in copy-mode after $label"
fi fi
if ! grep -Eq 'L[0-9][0-9][0-9][0-9]\|' "$TMP"; then if ! grep -Eq 'L[0-9][0-9][0-9][0-9]\|' "$TMP"; then
record_fail "no line ID in copy-mode after $label" record_fail "no line ID in copy-mode after $label"
fi fi
@@ -244,45 +366,129 @@ assert_copy_mode_sane()
run_resize_checks() run_resize_checks()
{ {
for width in $WIDTHS; do for width in $WIDTHS; do
$TMUX resize-window -t stress: -x "$width" -y 8 || exit 1 $TMUX resize-window -t stress: -x "$width" -y "$HEIGHT" || exit 1
sleep 0.1 sleep 0.1
assert_alive "resize to $width" assert_alive "resize to $width"
assert_joined_sane "resize to $width" assert_joined_sane "resize to $width"
assert_raw_sane "resize to $width" "$width" assert_raw_sane "resize to $width"
assert_history_sane "resize to $width" assert_history_sane "resize to $width"
done done
} }
wait_outer_contains()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$TMUX2 capture-pane -p -t out:0 >"$TMP" 2>/dev/null &&
grep -F "$marker" "$TMP" >/dev/null 2>&1 &&
return 0
sleep 0.2
i=$((i + 1))
done
return 1
}
assert_live_client_redraw()
{
$TMUX set-option -t stress: status on >/dev/null || exit 1
$TMUX set-option -t stress: status-interval 1 >/dev/null || exit 1
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -s out -x 100 -y 12 "$TMUX attach -t stress" ||
exit 1
i=0
while [ "$i" -lt 50 ]; do
clients=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
[ "$clients" -ge 1 ] && break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || {
record_fail "nested client did not attach"
return
}
for width in $LIVE_WIDTHS; do
$TMUX set-option -t stress: status-left "REDRAW-$width " >/dev/null ||
exit 1
$TMUX2 resize-window -t out: -x "$width" -y 12 || exit 1
sleep 0.2
done
wait_outer_contains 'REDRAW-80' ||
record_fail "outer capture missing final redraw marker"
$TMUX2 capture-pane -p -t out:0 >"$TMP" 2>/dev/null ||
record_fail "outer capture failed"
bytes=$(wc -c <"$TMP")
if [ "$bytes" -gt "$LIVE_BOUND" ]; then
record_fail "outer capture too large: $bytes"
fi
if grep -q "$(replacement)" "$TMP"; then
record_fail "replacement character in outer capture"
fi
grep -F 'SENT-POST|ORPHAN-PHASE|complete' "$TMP" >/dev/null 2>&1 ||
record_fail "outer capture missing expected sentinel"
grep -F 'REDRAW-24' "$TMP" >/dev/null 2>&1 &&
record_fail "outer capture contains stale width marker"
$TMUX2 kill-server 2>/dev/null
$TMUX set-option -t stress: status off >/dev/null || exit 1
}
$TMUX kill-server 2>/dev/null $TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
sleep 0.1 sleep 0.1
$TMUX new-session -d -x 1 -y 1 -s test-setup "sleep 2" || exit 1 $TMUX new-session -d -x 1 -y 1 -s test-setup "sleep 2" || exit 1
$TMUX set-option -g history-limit "$HISTORY_LIMIT" || exit 1 $TMUX set-option -g history-limit "$HISTORY_LIMIT" || exit 1
$TMUX new-session -d -x 80 -y 8 -s stress "stty -echo; cat" || exit 1 $TMUX new-session -d -x 80 -y "$HEIGHT" -s stress 'cat' || exit 1
$TMUX kill-session -t test-setup $TMUX kill-session -t test-setup || exit 1
sleep 0.3 sleep 0.3
load_and_paste stress-data make_payload load_and_paste stress-data make_payload
assert_joined_sane "initial payload"
assert_raw_sane "initial payload"
assert_history_sane "initial payload"
$TMUX resize-window -t stress: -x 40 -y "$HEIGHT" || exit 1
sleep 0.1
load_and_paste stress-orphan-post make_orphan_payload POST
assert_joined_sane "post-resize orphan payload"
assert_raw_sane "post-resize orphan payload"
run_resize_checks run_resize_checks
assert_final_logical_text "return to original width"
$TMUX copy-mode -H -t stress: || exit 1 $TMUX copy-mode -H -t stress: || exit 1
for cmd in history-top page-down halfpage-down halfpage-up page-up history-bottom; do $TMUX send-keys -t stress: -X history-top
$TMUX send-keys -t stress: -X "$cmd" || exit 1 sleep 0.1
sleep 0.1 assert_alive "copy-mode"
assert_copy_mode_sane "$cmd" assert_copy_mode_sane "copy-mode history-top"
done $TMUX send-keys -t stress: -X cancel
$TMUX send-keys -t stress: -X cancel || exit 1 sleep 0.1
assert_live_client_redraw
load_and_paste stress-alt make_alternate_payload load_and_paste stress-alt make_alternate_payload
for width in 10 4 1 4 10 80; do for width in 30 12 80; do
$TMUX resize-window -t stress: -x "$width" -y 8 || exit 1 $TMUX resize-window -t stress: -x "$width" -y "$HEIGHT" || exit 1
sleep 0.1 sleep 0.1
assert_alive "alternate resize to $width" assert_alive "alternate resize to $width"
assert_raw_sane "alternate resize to $width" "$width" assert_raw_sane "alternate resize to $width"
done done
printf '\033[?1049l' | $TMUX load-buffer -b stress-alt-exit - || exit 1 load_and_paste stress-alt-exit exit_alternate_payload
$TMUX paste-buffer -d -b stress-alt-exit -t stress:0.0 || exit 1
sleep 0.3 sleep 0.3
assert_joined_sane "alternate screen exit" no assert_alive "alternate screen exit"
assert_joined_sane "alternate screen exit"
assert_raw_sane "alternate screen exit"
assert_final_logical_text "alternate screen exit"
$TMUX kill-server 2>/dev/null cleanup
exit $exit_status exit $exit_status