From bd39ce0d499b49d289164ddceb1a6caa7595feab Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 5 Jul 2026 10:19:58 +0100 Subject: [PATCH] Extend tests. --- regress/format-render-contexts.sh | 222 +++++++------- regress/input-reflow-stress.sh | 492 +++++++++++++++++++++--------- 2 files changed, 467 insertions(+), 247 deletions(-) diff --git a/regress/format-render-contexts.sh b/regress/format-render-contexts.sh index 81f1de462..a91fb88e1 100644 --- a/regress/format-render-contexts.sh +++ b/regress/format-render-contexts.sh @@ -1,18 +1,16 @@ #!/bin/sh -# Exercise format/style rendering in live contexts. These paths use -# format_draw rather than plain format expansion, so they have historically -# found different crashes and runaway output. +# Exercise format and style rendering in live contexts. PATH=/bin:/usr/bin TERM=screen LANG=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) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -Lformat-render-contexts-$$ -f/dev/null" +TMUX2="$TEST_TMUX -Lformat-render-contexts-outer-$$ -f/dev/null" LIMIT=20000 cleanup() @@ -39,23 +37,22 @@ run_cmd() bounded() { - name="$1" - text="$2" - n=$(printf '%s' "$text" | wc -c | tr -d ' ') + name=$1 + text=$2 - [ "$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() { - name="$1" + name=$1 shift out=$(run_cmd $TMUX "$@" 2>&1) rc=$? bounded "$name" "$out" - [ "$rc" -eq 0 ] || fail "$name failed ($rc): $out" - + [ "$rc" -eq 0 ] || fail "$name failed: $out" printf '%s' "$out" } @@ -70,7 +67,7 @@ capture() 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=$? bounded "capture-pane -e" "$out" [ "$rc" -eq 0 ] || fail "capture-pane -e failed" @@ -79,16 +76,15 @@ capture_esc() assert_alive() { - out=$(tmux_run "server alive ($1)" display-message -p alive) - [ "$out" = "alive" ] || fail "server not alive after $1" + tmux_run "server alive ($1)" display-message -p alive >/dev/null } wait_for() { - marker="$1" + marker=$1 i=0 while [ "$i" -lt 50 ]; do - if capture | grep -q "$marker"; then + if capture | grep -F "$marker" >/dev/null 2>&1; then sleep 0.1 return 0 fi @@ -100,60 +96,69 @@ wait_for() render_status_options() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 tmux_run "$label status-left" \ 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" \ 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] \ - '#{E:status-left}#[align=right]#{E:status-right}' >/dev/null - wait_for "SL$label" + '#[align=right]#{E:status-right}' >/dev/null + tmux_run "$label refresh status-right" refresh-client -S >/dev/null + wait_for "SR$label:" assert_alive "$label status-left/right" } render_status_format() { - label="$1" - fmt="$2" + label=$1 + 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" \ 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" } render_message() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 tmux_run "$label display-message" \ 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" } render_choose_tree() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 tmux_run "$label choose-tree" \ 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 assert_alive "$label choose-tree" } render_customize() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 tmux_run "$label customize-mode" \ customize-mode -t fmt:0 -F "CM$label: $fmt" >/dev/null @@ -166,65 +171,30 @@ render_customize() render_list_output() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 tmux_run "$label bind-key" \ bind-key -T root F12 display-message "LK$label: $fmt" >/dev/null 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" \ - set-option -g status-left "LO$label: $fmt" >/dev/null - out=$(tmux_run "$label list-options" show-options -g status-left) - bounded "$label list-options output" "$out" + tmux_run "$label set show option" \ + set-option -g "@format_render_$label" "SO$label: $fmt" >/dev/null + out=$(tmux_run "$label show-options" show-options -g "@format_render_$label") + 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" -} - -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" + assert_alive "$label list output" } run_corpus() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 render_status_options "$label" "$fmt" render_status_format "$label" "$fmt" @@ -234,6 +204,60 @@ run_corpus() 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 cleanup @@ -241,11 +265,10 @@ tmux_run "new inner session" \ new-session -d -s fmt -x 100 -y 30 "exec sleep 1000" >/dev/null tmux_run "new inner window" \ 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 "set status interval" set-option -g status-interval 1 >/dev/null -tmux_run "set base message style" \ - set-option -g message-style fg=default,bg=default >/dev/null -tmux_run "set nested option" \ +tmux_run "select first window" select-window -t fmt:0 >/dev/null +tmux_run "status interval" set-option -g status-interval 1 >/dev/null +tmux_run "base status" set-option -g status on >/dev/null +tmux_run "nested option" \ 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" \ @@ -260,20 +283,11 @@ while [ "$i" -lt 50 ]; do done [ "$i" -lt 50 ] || fail "inner client did not attach" -run_corpus A \ - '#[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: Ελληνικά 中文 😀 #{=12:中文😀abc}' -run_corpus C \ - '#[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,}' +run_corpus A 'plain #{session_name}:#{window_index} #{@nested}' +run_corpus B "$(wide_fmt)" +run_corpus C '#[fg=colour10,bg=colour17,bold,italics]styled #{pane_current_command}#[default]' +run_corpus D '#[push-default]#[fg=red,bg=blue]push #{?pane_active,active,inactive}#[pop-default] after' +run_corpus E "$(long_style_fmt)" check_sgr_sanity diff --git a/regress/input-reflow-stress.sh b/regress/input-reflow-stress.sh index 9b12f73d2..aa1bbd717 100644 --- a/regress/input-reflow-stress.sh +++ b/regress/input-reflow-stress.sh @@ -1,55 +1,176 @@ #!/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) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMP=$(mktemp) -EXP=$(mktemp) -trap 'rm -f "$TMP" "$EXP"; $TMUX kill-server 2>/dev/null' 0 1 15 + +TMUX="$TEST_TMUX -Linput-reflow-stress-$$ -f/dev/null" +TMUX2="$TEST_TMUX -Linput-reflow-stress-outer-$$ -f/dev/null" + +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" +LIVE_WIDTHS="68 24 80" +HEIGHT=24 HISTORY_LIMIT=220 HISTORY_BOUND=12000 JOINED_BOUND=500000 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() { - echo "FAIL: $1" + echo "FAIL: $1" >&2 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() { - n=0 i=0 while [ "$i" -lt 16 ]; do - printf 'L%04d|ascii|abcdefghijklmnopqrstuvwxyz\n' "$n"; n=$((n + 1)) - printf 'L%04d|wrap|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' "$n"; n=$((n + 1)) - printf 'L%04d|wide|AあB界C\n' "$n"; n=$((n + 1)) - printf 'L%04d|wide-edge|abcdあZ\n' "$n"; n=$((n + 1)) - printf 'L%04d|combining|é é é\n' "$n"; n=$((n + 1)) - printf 'L%04d|orphan-combining|́A\n' "$n"; n=$((n + 1)) - printf 'L%04d|variation|✔️ ⚠️ 🏝️\n' "$n"; n=$((n + 1)) - printf 'L%04d|emoji|🙂🙂🙂\n' "$n"; n=$((n + 1)) - printf 'L%04d|flag|🇬🇧 🇺🇸 🇯🇵\n' "$n"; n=$((n + 1)) - printf 'L%04d|mixed|Aあé✔️B\n' "$n"; n=$((n + 1)) - printf 'L%04d|style|plain \033[31mred\033[0m plain\n' "$n"; n=$((n + 1)) - printf 'L%04d|hyperlink|\033]8;;https://example.com/%04d\033\\LINK\033]8;;\033\\\n' "$n" "$n"; n=$((n + 1)) - printf 'L%04d|tabs|a\tb\tc\n' "$n"; n=$((n + 1)) - printf 'L%04d|backspace|abc\bX\n' "$n"; n=$((n + 1)) - printf 'carriage-return-%04d|abcdef\rL%04d|carriage-return|XY\n' "$n" "$n"; n=$((n + 1)) + printf 'ASCII%02d|abcdefghijklmnopqrstuvwxyz\n' "$i" + + printf 'WIDE%02d|' "$i" + wide_a + printf '|' + wide_b + printf '|tail\n' + + printf 'COMB%02d|e' "$i" + combining_acute + printf '|zero' + zero_width_space + zero_width_joiner + printf '|done\n' + + 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)) 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() { printf '\033[?1049h' - printf 'ALT000|wide|AあB界C\n' - printf 'ALT001|combining|é é é\n' - printf 'ALT002|style|plain \033[32mgreen\033[0m plain\n' - printf 'ALT003|emoji|🙂🙂🙂\n' + printf 'ALT-SENT|alternate screen|' + wide_a + printf '\nALT-SENT|resize target\n' +} + +exit_alternate_payload() +{ + printf '\033[?1049l' } load_and_paste() @@ -57,29 +178,20 @@ load_and_paste() buffer=$1 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 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() { - $TMUX capture-pane -pNJ -t stress: -S - -E - >"$TMP" + $TMUX capture-pane -pNJ -t stress: -S -5000 -E - >"$TMP" } assert_joined_sane() { label=$1 - check_sentinels=${2:-yes} capture_joined || { record_fail "joined capture failed after $label" @@ -91,11 +203,12 @@ assert_joined_sane() record_fail "joined capture too large after $label: $bytes" 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" fi - awk -v label="$label" ' + out=$( + awk -v label="$label" ' { line = $0 seen_on_line = 0 @@ -103,120 +216,129 @@ assert_joined_sane() id = substr(line, RSTART + 1, 4) + 0 seen_on_line++ seen[id]++ - if (id < last) { - 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 + ids[++count] = id found = 1 + last = id line = substr(line, RSTART + RLENGTH) } - if (seen_on_line > 1) { + if (seen_on_line > 1) printf("fused IDs after %s: %s\n", label, $0) - exit 1 - } } END { if (!found) { printf("no line IDs after %s\n", label) - exit 1 + exit } - } - ' "$TMP" || { - cp "$TMP" "$EXP" - record_fail "joined structure failed after $label" + for (i = 2; i <= count; i++) { + if (ids[i] < ids[i - 1]) + drops++ + } + 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 expected in \ - 'L0234|mixed|' \ - 'L0236|hyperlink|' \ - 'L0239|carriage-return|XY'; do - if ! grep -Fq "$expected" "$TMP"; then - printf '%s\n' "$expected" >"$EXP" - record_fail "missing sentinel after $label" - fi - done - fi + for marker in \ + 'SENT-A|SURVIVES|plain logical line' \ + 'SENT-B|SURVIVES|wide-free after erases' \ + 'SENT-C|SURVIVES|tail logical line' \ + 'OPPRE-03|overwrite-leading|left!' \ + 'OPPOST-03|overwrite-leading|left!' \ + 'SENT-POST|ORPHAN-PHASE|complete' + do + grep -F "$marker" "$TMP" >/dev/null 2>&1 || + record_fail "missing expected logical text after $label: $marker" + done } assert_raw_sane() { 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" - return - } bytes=$(wc -c <"$TMP") if [ "$bytes" -gt "$RAW_BOUND" ]; then record_fail "raw capture too large after $label: $bytes" fi - awk -v label="$label" -v width="$width" ' - /^C / { - cell = $0 - sub(/^C /, "", cell) - split(cell, fields, " ") - split(fields[1], position, ",") - row = position[1] + 0 - col = position[2] + 0 - if (col >= width) { - printf("cell column outside pane after %s: %s\n", label, $0) - exit 1 - } + width=$($TMUX display-message -p -t stress: '#{pane_width}' 2>/dev/null) + case "$width" in + ''|*[!0-9]*) width=0 ;; + esac + + out=$( + awk -v label="$label" -v width="$width" ' + { + sub(/^[ ]+/, "") + if ($1 != "C") + 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 - if (data !~ /data=\(/) + if (data !~ /data=\(/) { + printf("cell without data after %s: %s\n", label, $0) next + } sub(/^.*data=\(/, "", data) - split(data, data_fields, ",") - cell_width = data_fields[1] + 0 - padding = ($0 ~ /flags=PADDING/) - - if (!padding && cell_width == 0) { - printf("zero-width non-padding cell after %s: %s\n", label, $0) - exit 1 - } - 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" - } + split(data, parts, ",") + cell_width = parts[1] + 0 + padding = ($0 ~ /flags=[^ ]*PADDING/) + if (!padding && cell_width == 0) + printf("visible zero-width cell after %s: %s\n", label, $0) + }' "$TMP" + ) + [ -z "$out" ] || record_fail "$out" } assert_history_sane() { label=$1 - - size=$($TMUX display-message -p -t stress: '#{history_size}') || { - record_fail "history-size failed after $label" - return - } + size=$($TMUX display-message -p -t stress: '#{history_size}' 2>/dev/null) case "$size" in - ''|*[!0-9]*) - record_fail "bad history size after $label: $size" - ;; - *) - if [ "$size" -gt "$HISTORY_BOUND" ]; then - record_fail "history too large after $label: $size" - fi - ;; + ''|*[!0-9]*) + record_fail "bad history size after $label: $size" + ;; + *) + if [ "$size" -gt "$HISTORY_BOUND" ]; then + record_fail "history too large after $label: $size" + fi + ;; esac } @@ -224,18 +346,18 @@ assert_copy_mode_sane() { 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" - return - } bytes=$(wc -c <"$TMP") - if [ "$bytes" -gt 20000 ]; then - record_fail "copy-mode capture too large after $label" + if [ "$bytes" -gt "$COPY_BOUND" ]; then + record_fail "copy-mode capture too large after $label: $bytes" 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" fi + if ! grep -Eq 'L[0-9][0-9][0-9][0-9]\|' "$TMP"; then record_fail "no line ID in copy-mode after $label" fi @@ -244,45 +366,129 @@ assert_copy_mode_sane() run_resize_checks() { 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 assert_alive "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" 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 +$TMUX2 kill-server 2>/dev/null sleep 0.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 new-session -d -x 80 -y 8 -s stress "stty -echo; cat" || exit 1 -$TMUX kill-session -t test-setup +$TMUX new-session -d -x 80 -y "$HEIGHT" -s stress 'cat' || exit 1 +$TMUX kill-session -t test-setup || exit 1 sleep 0.3 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 +assert_final_logical_text "return to original width" $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 "$cmd" || exit 1 - sleep 0.1 - assert_copy_mode_sane "$cmd" -done -$TMUX send-keys -t stress: -X cancel || exit 1 +$TMUX send-keys -t stress: -X history-top +sleep 0.1 +assert_alive "copy-mode" +assert_copy_mode_sane "copy-mode history-top" +$TMUX send-keys -t stress: -X cancel +sleep 0.1 + +assert_live_client_redraw load_and_paste stress-alt make_alternate_payload -for width in 10 4 1 4 10 80; do - $TMUX resize-window -t stress: -x "$width" -y 8 || exit 1 +for width in 30 12 80; do + $TMUX resize-window -t stress: -x "$width" -y "$HEIGHT" || exit 1 sleep 0.1 assert_alive "alternate resize to $width" - assert_raw_sane "alternate resize to $width" "$width" + assert_raw_sane "alternate resize to $width" done -printf '\033[?1049l' | $TMUX load-buffer -b stress-alt-exit - || exit 1 -$TMUX paste-buffer -d -b stress-alt-exit -t stress:0.0 || exit 1 +load_and_paste stress-alt-exit exit_alternate_payload 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