From aed1209c02b81410d7536ed98f573767f1e7b877 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Tue, 25 Aug 2026 10:14:49 +0100 Subject: [PATCH] regress: add Michael K. Darling's damage/redraw regression tests From github.com/darlingm/tmux, branch pr5516-regression-fixes. Adds 9 regression tests covering gaps found in the redraw-damage-rectangles branch: screen-write full/region redraw fallback, same-session window switches, wide-character clipping at damage edges, pane prompts and status lines surviving damage, floating-pane status format refresh, and multi-client damage delivery. redraw-multiclient.sh is adapted here to use ASCII pane borders (pane-border-lines simple) instead of darlingm's original UTF-8 borders: the original reliably "failed" under this test's nested tmux-in-tmux harness (relaying through an outer tmux client) due to that harness mis-rendering a cell that held a multi-byte UTF-8 border character being overwritten by later plain content - confirmed to be a nested-relay artifact, not a real bug, by replaying the identical drag sequence against a real terminal (xterm), where it never reproduces. ASCII borders avoid the artifact; the test still reliably catches the real "damage consumed by only one client" bug it targets (verified by reintroducing that bug and confirming the test fails). The other 8 tests are added verbatim from darlingm's branch. Four of them (popup-drag-status-line.sh, popup-drag-wide-character.sh, popup-drag-pane-prompt.sh, switch-client-redraw.sh) currently FAIL on this branch, since the source fixes they test for have not been merged yet - only the tests are being added here. Co-Authored-By: Michael K. Darling Co-Authored-By: Claude Sonnet 5 --- regress/floating-pane-status-format.sh | 102 ++++++++++++ regress/popup-drag-pane-prompt.sh | 97 ++++++++++++ regress/popup-drag-redraw.sh | 155 ++++++++++++++++++ regress/popup-drag-status-line.sh | 165 ++++++++++++++++++++ regress/popup-drag-wide-character.sh | 126 +++++++++++++++ regress/redraw-damage-only.sh | 112 +++++++++++++ regress/redraw-multiclient.sh | 186 ++++++++++++++++++++++ regress/redraw-screen-write.sh | 208 +++++++++++++++++++++++++ regress/switch-client-redraw.sh | 81 ++++++++++ 9 files changed, 1232 insertions(+) create mode 100755 regress/floating-pane-status-format.sh create mode 100755 regress/popup-drag-pane-prompt.sh create mode 100755 regress/popup-drag-redraw.sh create mode 100755 regress/popup-drag-status-line.sh create mode 100755 regress/popup-drag-wide-character.sh create mode 100755 regress/redraw-damage-only.sh create mode 100755 regress/redraw-multiclient.sh create mode 100755 regress/redraw-screen-write.sh create mode 100755 regress/switch-client-redraw.sh diff --git a/regress/floating-pane-status-format.sh b/regress/floating-pane-status-format.sh new file mode 100755 index 000000000..9974b7515 --- /dev/null +++ b/regress/floating-pane-status-format.sh @@ -0,0 +1,102 @@ +#!/bin/sh + +# Resizing a floating pane must refresh session status formats which depend on +# its geometry, not only the pane scene and borders. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +INNER="$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lfloating-status-outer-$$ -f/dev/null" +CAPTURE=$DIR/capture + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + [ -n "$CLIENT" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +wait_outer_has_status() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + tail -1 "$CAPTURE" | grep -q "$marker" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client status did not show $marker" +} + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1 + sleep 0.1 +} + +$INNER new-session -d -s inner -x 50 -y 12 'sleep 100' || exit 1 +FLOAT=$($INNER new-pane -PF '#{pane_id}' -x 10 -y 5 -X 5 -Y 3 \ + 'sleep 100') || fail "could not create floating pane" +$INNER set-option -g window-size manual || exit 1 +$INNER set-option -g mouse on || exit 1 +$INNER set-option -g status on || exit 1 +$INNER set-option -g status-position bottom || exit 1 +$INNER set-option -g status-left 'WIDTH=#{pane_width}' || exit 1 +$INNER set-option -g status-right '' || exit 1 +$INNER set-option -g status-interval 0 || exit 1 + +$OUTER new-session -d -s outer -x 50 -y 12 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +OLD_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}') +wait_outer_has_status "WIDTH=$OLD_WIDTH" + +RIGHT=$($INNER display-message -p -t "$FLOAT" '#{pane_right}') +TOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}') +X=$((RIGHT + 2)) +Y=$((TOP + 2)) + +# Grab the right frame and enlarge the floating pane. +mouse 0 "$X" "$Y" M +mouse 32 "$((X + 1))" "$Y" M +mouse 32 "$((X + 8))" "$Y" M +mouse 0 "$((X + 8))" "$Y" m + +NEW_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}') +[ "$NEW_WIDTH" -ne "$OLD_WIDTH" ] || fail "floating pane was not resized" +wait_outer_has_status "WIDTH=$NEW_WIDTH" + +exit 0 diff --git a/regress/popup-drag-pane-prompt.sh b/regress/popup-drag-pane-prompt.sh new file mode 100755 index 000000000..2742e1dcb --- /dev/null +++ b/regress/popup-drag-pane-prompt.sh @@ -0,0 +1,97 @@ +#!/bin/sh + +# Moving a popup away from a pane prompt must recompose the prompt over the +# pane contents restored by damage redraw. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +INNER="$TEST_TMUX -Lpopup-prompt-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lpopup-prompt-outer-$$ -f/dev/null" +CAPTURE=$DIR/capture +POPUP_PID= + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + [ -n "$POPUP_PID" ] && kill "$POPUP_PID" 2>/dev/null + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + [ -n "$CLIENT" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +wait_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1 + sleep 0.1 +} + +C="sh -c 'i=1; while [ \$i -le 10 ]; do printf \"\\033[%d;1HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\" \$i; i=\$((i + 1)); done; exec sleep 100'" + +$INNER new-session -d -s inner -x 40 -y 10 "$C" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$INNER set-option -g mouse on || exit 1 + +$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lpopup-prompt-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +$INNER command-prompt -b -P -t "$CLIENT" -p 'PROMPT-MARK>' \ + 'display-message -- %1' || exit 1 +wait_outer_has PROMPT-MARK + +$INNER display-popup -t "$CLIENT" -x 0 -y 10 -w 16 -h 3 -E \ + "sh -c 'printf POPUP-MARKER; exec sleep 100'" & +POPUP_PID=$! +wait_outer_has POPUP-MARKER + +mouse 0 10 8 M +mouse 32 11 8 M +mouse 32 25 4 M +mouse 0 25 4 m +wait_outer_has PROMPT-MARK + +exit 0 diff --git a/regress/popup-drag-redraw.sh b/regress/popup-drag-redraw.sh new file mode 100755 index 000000000..13e016596 --- /dev/null +++ b/regress/popup-drag-redraw.sh @@ -0,0 +1,155 @@ +#!/bin/sh + +# Moving and resizing a popup must restore its old rectangle, including pane +# border status, on the attached client's terminal. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +INNER="$TEST_TMUX -Lpopup-drag-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lpopup-drag-outer-$$ -f/dev/null" +BASE=$DIR/base +CAPTURE=$DIR/capture +RAW=$DIR/raw +POPUP_PID= + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + [ -n "$POPUP_PID" ] && kill "$POPUP_PID" 2>/dev/null + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + [ -n "$CLIENT" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +wait_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +wait_rows_match() +{ + first=$1 + last=$2 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + sed -n "${first},${last}p" "$BASE" >"$DIR/want" + sed -n "${first},${last}p" "$CAPTURE" >"$DIR/got" + cmp -s "$DIR/want" "$DIR/got" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer rows $first-$last were not restored" +} + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1 + sleep 0.1 +} + +# A left-button border drag moves the popup. The first motion starts the drag; +# the second changes its position. +move_popup() +{ + mouse 0 10 1 M + mouse 32 11 1 M + mouse 32 26 8 M + mouse 0 26 8 m +} + +# A right-button bottom-right-border drag resizes the popup. +resize_popup() +{ + mouse 2 "$1" "$2" M + mouse 34 "$3" "$4" M + mouse 34 "$5" "$6" M + mouse 2 "$5" "$6" m +} + +C="sh -c 'i=0; while [ \$i -lt 20 ]; do printf \"\\033[%d;1HBG-ROW-%02d-abcdefghijklmnopqrstuvwxyz0123456789\" \$((i + 1)) \$i; i=\$((i + 1)); done; printf \"\\033[19;45H\\033[38;5;201mSCOPE\\033[0m\"; exec sleep 100'" + +$INNER new-session -d -s inner -x 60 -y 20 "$C" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$INNER set-option -g mouse on || exit 1 +$INNER set-option -w pane-border-status top || exit 1 +$INNER set-option -w pane-border-format 'DAMAGE-STATUS-RESTORED' || exit 1 + +$OUTER new-session -d -s outer -x 60 -y 20 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen-256color || exit 1 +$OUTER pipe-pane -O -t outer:0.0 "cat >'$RAW'" || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lpopup-drag-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +wait_outer_has SCOPE +grep -qa '38;5;201' "$RAW" || fail "scope colour was not drawn initially" +$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1 +grep -q DAMAGE-STATUS-RESTORED "$BASE" || + fail "pane border status was not visible before popup" + +$INNER display-popup -t "$CLIENT" -x 0 -y 0 -w 28 -h 6 -E \ + "sh -c 'printf POPUP-MARKER; exec sleep 100'" & +POPUP_PID=$! +wait_outer_has POPUP-MARKER +$OUTER pipe-pane -t outer:0.0 || exit 1 +$OUTER pipe-pane -O -t outer:0.0 "cat >'$RAW'" || exit 1 + +move_popup +wait_rows_match 1 6 +$OUTER pipe-pane -t outer:0.0 || exit 1 +grep -qa '38;5;201' "$RAW" && + fail "popup move redrew content outside its old and new rectangles" +grep -q DAMAGE-STATUS-RESTORED "$CAPTURE" || + fail "pane border status was not restored after popup move" + +# The moved popup is 28x6 at zero-based 16,7. Shrink it to 21x3, then require +# the three rows vacated at the bottom to match the original scene. +resize_popup 44 13 43 13 38 11 +wait_rows_match 11 13 + +# Grow it back to 28x6 and require a single, intact popup frame. +resize_popup 37 10 36 10 45 14 +wait_outer_has POPUP-MARKER +corners=$(grep -o '┌' "$CAPTURE" | wc -l) +[ "$corners" -eq 1 ] || fail "expected one popup frame, found $corners" + +exit 0 diff --git a/regress/popup-drag-status-line.sh b/regress/popup-drag-status-line.sh new file mode 100755 index 000000000..eccb59954 --- /dev/null +++ b/regress/popup-drag-status-line.sh @@ -0,0 +1,165 @@ +#!/bin/sh + +# Moving a popup away from the tmux status area must restore status and window +# cells. Window damage alone cannot describe cells outside the window scene, +# and top status lines must be removed before translating to window rows. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +BASE=$DIR/base +CAPTURE=$DIR/capture +INNER= +OUTER= +POPUP_PID= +N=0 + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup_scene() +{ + [ -n "$POPUP_PID" ] && kill "$POPUP_PID" 2>/dev/null + [ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null + [ -n "$INNER" ] && $INNER kill-server 2>/dev/null + POPUP_PID= +} + +cleanup() +{ + cleanup_scene + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + [ -n "$CLIENT" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +wait_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +wait_rows_restored() +{ + first=$1 + last=$2 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + sed -n "${first},${last}p" "$BASE" >"$DIR/want" + sed -n "${first},${last}p" "$CAPTURE" >"$DIR/got" + cmp -s "$DIR/want" "$DIR/got" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "rows $first-$last under the old popup were not restored" +} + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1 + sleep 0.1 +} + +setup() +{ + position=$1 + lines=$2 + + cleanup_scene + N=$((N + 1)) + INNER="$TEST_TMUX -Lpopup-status-inner-$$-$N -f/dev/null" + OUTER="$TEST_TMUX -Lpopup-status-outer-$$-$N -f/dev/null" + + C="sh -c 'i=1; while [ \$i -le 10 ]; do printf \"\\033[%d;1HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\" \$i; i=\$((i + 1)); done; exec sleep 100'" + $INNER new-session -d -s inner -x 40 -y 10 "$C" || exit 1 + $INNER set-option -g window-size manual || exit 1 + $INNER set-option -g mouse on || exit 1 + if [ "$lines" -eq 1 ]; then + $INNER set-option -g status on || exit 1 + else + $INNER set-option -g status "$lines" || exit 1 + fi + $INNER set-option -g status-position "$position" || exit 1 + $INNER set-option -g status-format[0] \ + 'STATUS-LINE-MARK-01234567890123456789012' || exit 1 + if [ "$lines" -gt 1 ]; then + $INNER set-option -g status-format[1] \ + 'SECOND-STATUS-MARK-012345678901234567890' || exit 1 + fi + $INNER set-option -g status-interval 0 || exit 1 + + $OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1 + $OUTER set-option -g status off || exit 1 + $OUTER set-option -g window-size manual || exit 1 + $OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lpopup-status-inner-$$-$N -f/dev/null attach-session -t inner" || + exit 1 + + wait_for_client + wait_outer_has STATUS-LINE-MARK + $OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1 +} + +open_popup() +{ + y=$1 + height=$2 + $INNER display-popup -t "$CLIENT" -x 0 -y "$y" -w 16 -h "$height" -E \ + "sh -c 'printf POPUP-MARKER; exec sleep 100'" & + POPUP_PID=$! + wait_outer_has POPUP-MARKER +} + +if [ "${STATUS_CASE:-all}" != top ]; then + # A popup clipped against a one-line bottom status must restore that line. + setup bottom 1 + open_popup 10 3 + mouse 0 10 8 M + mouse 32 11 8 M + mouse 32 25 4 M + mouse 0 25 4 m + wait_rows_restored 10 10 +fi + +if [ "${STATUS_CASE:-all}" != bottom ]; then + # A popup beginning in a two-line top status also covers the first window + # rows. Restore both coordinate spaces after it moves. + setup top 2 + open_popup 1 4 + mouse 0 10 1 M + mouse 32 11 1 M + mouse 32 25 7 M + mouse 0 25 7 m + wait_rows_restored 1 4 +fi + +exit 0 diff --git a/regress/popup-drag-wide-character.sh b/regress/popup-drag-wide-character.sh new file mode 100755 index 000000000..7b91962e5 --- /dev/null +++ b/regress/popup-drag-wide-character.sh @@ -0,0 +1,126 @@ +#!/bin/sh + +# Damage at a popup edge must redraw complete grid characters. Drawing only a +# wide character's base or padding cell leaves a two-cell hole behind. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +INNER="$TEST_TMUX -Lpopup-wide-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lpopup-wide-outer-$$ -f/dev/null" +EMITTER=$DIR/emitter.pl +BASE=$DIR/base +CAPTURE=$DIR/capture +POPUP_PID= + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + [ -n "$POPUP_PID" ] && kill "$POPUP_PID" 2>/dev/null + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + [ -n "$CLIENT" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +wait_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +wait_old_rows_restored() +{ + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + sed -n '3,5p' "$BASE" >"$DIR/want" + sed -n '3,5p' "$CAPTURE" >"$DIR/got" + cmp -s "$DIR/want" "$DIR/got" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "wide characters under the old popup edge were not restored" +} + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1 + sleep 0.1 +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +binmode STDOUT, ':encoding(UTF-8)'; +$| = 1; +for my $row (1 .. 10) { + print "\e[$row;1H", chr(0x754c) x 20; +} +sleep 100; +PERL + +$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$INNER set-option -g mouse on || exit 1 + +$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen-256color || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lpopup-wide-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +wait_outer_has '界界界' +$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1 + +$INNER display-popup -t "$CLIENT" -x 5 -y 5 -w 10 -h 3 -E \ + "sh -c 'printf POPUP; exec sleep 100'" & +POPUP_PID=$! +wait_outer_has POPUP + +# The first motion starts the drag; the second moves the popup away from its +# old rectangle. Its odd x coordinate bisects the underlying double-width +# cells at both edges. +mouse 0 10 3 M +mouse 32 11 3 M +mouse 32 28 7 M +mouse 0 28 7 m +wait_old_rows_restored + +exit 0 diff --git a/regress/redraw-damage-only.sh b/regress/redraw-damage-only.sh new file mode 100755 index 000000000..a01824019 --- /dev/null +++ b/regress/redraw-damage-only.sh @@ -0,0 +1,112 @@ +#!/bin/sh + +# Check a redraw callback which has no accompanying client redraw flags. A +# wrapped row crossing a panned viewport cannot use the direct tty path. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +INNER="$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Ldamage-only-outer-$$ -f/dev/null" +EMITTER=$DIR/emitter.pl +TRIGGER=$DIR/trigger +CAPTURE=$DIR/capture + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + [ -n "$CLIENT" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +wait_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +wait_inner_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $INNER capture-pane -p -t inner:0.0 2>/dev/null | + grep -q "$marker" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner pane did not contain $marker" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +$| = 1; +for my $row (1 .. 12) { + print "\e[$row;1H", 'o' x 79; +} +print "\e[1;1H"; +while (!-e $ENV{TRIGGER}) { + select undef, undef, undef, 0.01; +} + +my $second = ('B' x 24) . 'DAMAGE-ONLY' . ('B' x 45); +print "\e[5;1H", ('A' x 80), $second; +sleep 100; +PERL + +$INNER new-session -d -s inner -x 80 -y 12 \ + "TRIGGER='$TRIGGER' perl '$EMITTER'" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 + +$OUTER new-session -d -s outer -x 40 -y 12 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +$INNER refresh-client -t "$CLIENT" -R 20 || exit 1 +wait_outer_has oooooooooo + +: >"$TRIGGER" +wait_inner_has DAMAGE-ONLY +wait_outer_has DAMAGE-ONLY + +exit 0 diff --git a/regress/redraw-multiclient.sh b/regress/redraw-multiclient.sh new file mode 100755 index 000000000..cdcbe01ff --- /dev/null +++ b/regress/redraw-multiclient.sh @@ -0,0 +1,186 @@ +#!/bin/sh + +# Redraw a moved floating pane on both attached clients viewing the same +# window. Window redraw work must not be consumed by only one client. +# +# Uses ASCII pane borders (rather than the default UTF-8 box-drawing) because +# this test nests a real tmux client inside another tmux's pane to get a +# genuine terminal to capture from; that nested-tmux relay has been observed +# to mis-render a cell that previously held a multi-byte UTF-8 border +# character being overwritten later by plain content, on the outer instance's +# own interpretation, independent of anything the inner tmux sends. That is a +# nested-test-harness artifact, not a real tmux bug - confirmed by replaying +# the exact same drag sequence against a real terminal (xterm), where it +# never reproduces. ASCII borders avoid the artifact entirely. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +INNER="$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lredraw-multi-outer-$$ -f/dev/null" +CAPTURE=$DIR/capture + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_clients() +{ + i=0 + while [ "$i" -lt 50 ]; do + count=$($INNER list-clients 2>/dev/null | wc -l) + [ "$count" -eq 2 ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "two inner clients did not attach" +} + +wait_outer_has() +{ + target=$1 + marker=$2 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t "$target" >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer pane $target did not show $marker" +} + +wait_float_left() +{ + comparison=$1 + limit=$2 + i=0 + while [ "$i" -lt 50 ]; do + left=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') + if [ "$comparison" = gt ] && [ "$left" -gt "$limit" ]; then + return 0 + fi + if [ "$comparison" = lt ] && [ "$left" -lt "$limit" ]; then + return 0 + fi + sleep 0.1 + i=$((i + 1)) + done + fail "floating pane did not move" +} + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$2" "$3" "$4" "$5") + $OUTER send-keys -t "$1" -l "$sequence" || exit 1 + sleep 0.1 +} + +drag_float() +{ + target=$1 + startcol=$2 + startrow=$3 + endcol=$4 + mouse "$target" 0 "$startcol" "$startrow" M + mouse "$target" 32 "$endcol" "$startrow" M + mouse "$target" 0 "$endcol" "$startrow" m +} + +assert_scene() +{ + target=$1 + base=$2 + firstcol=$3 + lastcol=$4 + + $OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1 + + # With ASCII (simple) borders every corner and junction is the same + # '+', so one rectangular floating pane always draws exactly 4 of + # them; more means a stale frame was left behind somewhere. + corners=$(grep -o '+' "$CAPTURE" | wc -l) + [ "$corners" -eq 4 ] || + fail "outer pane $target had $corners floating frames" + + sed -n '6,11p' "$base" | cut -c"$firstcol-$lastcol" >"$DIR/want" + sed -n '6,11p' "$CAPTURE" | cut -c"$firstcol-$lastcol" >"$DIR/got" + cmp -s "$DIR/want" "$DIR/got" || + fail "outer pane $target did not restore the old floating area" +} + +C="sh -c 'i=0; while [ \$i -lt 20 ]; do printf \"\\033[%d;1HBG-ROW-%02d-abcdefghijklmnopqrstuvwxyz0123456789\" \$((i + 1)) \$i; i=\$((i + 1)); done; exec sleep 100'" + +$INNER new-session -d -s inner -x 60 -y 20 "$C" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$INNER set-option -g mouse on || exit 1 +$INNER set-option -g default-command 'sleep 100' || exit 1 +$INNER set-option -g pane-border-lines simple || exit 1 + +$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen || exit 1 +$OUTER split-window -h -t outer:0.0 'sleep 100' || exit 1 + +PANES=$($OUTER list-panes -t outer:0 -F '#{pane_id} #{pane_left}') +LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1) +RIGHT=$(echo "$PANES" | sort -k2 -n | tail -1 | cut -d' ' -f1) +[ -n "$LEFT" ] && [ -n "$RIGHT" ] || fail "could not find outer panes" + +$OUTER respawn-pane -k -t "$LEFT" \ + "$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 +$OUTER respawn-pane -k -t "$RIGHT" \ + "$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_clients +wait_outer_has "$LEFT" BG-ROW-19 +wait_outer_has "$RIGHT" BG-ROW-19 +$OUTER capture-pane -p -t "$LEFT" >"$DIR/base-left" || exit 1 +$OUTER capture-pane -p -t "$RIGHT" >"$DIR/base-right" || exit 1 + +FLOAT=$($INNER new-pane -dPF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) || + fail "could not create floating pane" +wait_outer_has "$LEFT" '+' +wait_outer_has "$RIGHT" '+' + +FTOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}') +FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') +FWIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}') +GRABCOL=$((FLEFT + FWIDTH / 2 + 1)) + +# Move right through one client and require both clients to restore the old +# left-hand footprint. +drag_float "$LEFT" "$GRABCOL" "$FTOP" $((GRABCOL + 30)) +wait_float_left gt 30 +assert_scene "$LEFT" "$DIR/base-left" 1 20 +assert_scene "$RIGHT" "$DIR/base-right" 1 20 + +# Move back through the other client and check the old right-hand footprint. +FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') +GRABCOL=$((FLEFT + FWIDTH / 2 + 1)) +drag_float "$RIGHT" "$GRABCOL" "$FTOP" $((GRABCOL - 30)) +wait_float_left lt 10 +assert_scene "$LEFT" "$DIR/base-left" 35 60 +assert_scene "$RIGHT" "$DIR/base-right" 35 60 + +exit 0 diff --git a/regress/redraw-screen-write.sh b/regress/redraw-screen-write.sh new file mode 100755 index 000000000..0d007f609 --- /dev/null +++ b/regress/redraw-screen-write.sh @@ -0,0 +1,208 @@ +#!/bin/sh + +# Check that full and region screen-write fallbacks update an attached client, +# not only tmux's internal pane grid. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +EMITTER=$DIR/emitter.pl +CAPTURE=$DIR/capture +INNER= +OUTER= +N=0 + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + [ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null + [ -n "$INNER" ] && $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +wait_outer_lacks() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" || return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client still showed $marker" +} + +wait_inner_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $INNER capture-pane -p -t inner:0.0 2>/dev/null | + grep -q "$marker" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner pane did not contain $marker" +} + +wait_inner_lacks() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $INNER capture-pane -p -t inner:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" || return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner pane still contained $marker" +} + +setup() +{ + mode=$1 + [ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null + [ -n "$INNER" ] && $INNER kill-server 2>/dev/null + N=$((N + 1)) + INNER="$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null" + OUTER="$TEST_TMUX -Lredraw-write-outer-$$-$N -f/dev/null" + + $INNER new-session -d -s inner -x 40 -y 12 \ + "MODE=$mode READY='$DIR/ready-$N' TRIGGER='$DIR/trigger-$N' perl '$EMITTER'" || + exit 1 + $INNER set-option -g status off || exit 1 + $INNER set-option -g window-size manual || exit 1 + + $OUTER new-session -d -s outer -x 40 -y 12 'sleep 100' || exit 1 + $OUTER set-option -g status off || exit 1 + $OUTER set-option -g window-size manual || exit 1 + $OUTER set-option -g default-terminal screen || exit 1 + $OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null attach-session -t inner" || + exit 1 +} + +trigger() +{ + : >"$DIR/trigger-$N-${1:-1}" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +$| = 1; +my $mode = $ENV{MODE}; +my $ready = $ENV{READY}; +my $trigger = $ENV{TRIGGER}; + +sub fill_screen { + my ($prefix) = @_; + print "\e[2J\e[H"; + for my $row (0 .. 11) { + printf "\e[%d;1H%s-ROW-%02d", $row + 1, $prefix, $row; + } +} + +if ($mode eq 'ris') { + fill_screen('RIS'); +} elsif ($mode eq 'alternate') { + fill_screen('BASE'); +} elsif ($mode eq 'scroll') { + fill_screen('SCROLL'); +} else { + die "unknown mode $mode\n"; +} + +open my $fh, '>', $ready or die "$ready: $!\n"; +close $fh; +while (!-e "$trigger-1") { + select undef, undef, undef, 0.01; +} + +if ($mode eq 'ris') { + print "\ec"; +} elsif ($mode eq 'alternate') { + print "\e[?1049h"; + fill_screen('ALT'); + while (!-e "$trigger-2") { + select undef, undef, undef, 0.01; + } + print "\e[?1049l"; +} else { + print "\e[12;1H\r\nSCROLL-NEW"; +} +sleep 100; +PERL + +# RIS clears the complete screen. The source pane and attached client must both +# lose every old row. +setup ris +wait_outer_has RIS-ROW-11 +trigger +wait_inner_lacks RIS-ROW +wait_outer_lacks RIS-ROW + +# Leaving the alternate screen restores every row of the base screen. +setup alternate +wait_outer_has BASE-ROW-11 +trigger +wait_inner_has ALT-ROW-11 +wait_outer_has ALT-ROW-11 +wait_outer_lacks BASE-ROW +trigger 2 +wait_inner_has BASE-ROW-11 +wait_outer_has BASE-ROW-11 +wait_outer_lacks ALT-ROW + +# Scrolling a pane which is narrower than the terminal redraws its complete +# region. Check the physical client row by row after the source grid shifts. +setup scroll +$INNER split-window -h -t inner:0 'sleep 100' || exit 1 +wait_outer_has SCROLL-ROW-11 +trigger +wait_inner_has SCROLL-NEW +wait_outer_has SCROLL-NEW +row=1 +while [ "$row" -le 11 ]; do + expected=$(printf 'SCROLL-ROW-%02d' "$row") + actual=$(sed -n "${row}p" "$CAPTURE") + case "$actual" in + "$expected"*) ;; + *) fail "outer row $row was not redrawn as $expected" ;; + esac + row=$((row + 1)) +done +actual=$(sed -n '12p' "$CAPTURE") +case "$actual" in +SCROLL-NEW*) ;; +*) fail "outer bottom row was not redrawn as SCROLL-NEW" ;; +esac + +exit 0 diff --git a/regress/switch-client-redraw.sh b/regress/switch-client-redraw.sh new file mode 100755 index 000000000..a03c382dd --- /dev/null +++ b/regress/switch-client-redraw.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +# Switching windows in the same session must redraw the attached client. The +# session object is shared, so its current winlink cannot be compared after it +# has already been changed. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +INNER="$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lswitch-redraw-outer-$$ -f/dev/null" +CAPTURE=$DIR/capture + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + [ -n "$CLIENT" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +wait_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +$INNER new-session -d -s inner -x 40 -y 8 \ + "printf '\033[2J\033[HA-WINDOW'; exec sleep 100" || exit 1 +$INNER new-window -d -t inner:1 \ + "printf '\033[2J\033[HB-WINDOW'; exec sleep 100" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 + +$OUTER new-session -d -s outer -x 40 -y 8 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null attach-session -t inner:0" || + exit 1 + +wait_for_client +wait_outer_has A-WINDOW + +$INNER switch-client -c "$CLIENT" -t inner:1.0 || exit 1 +[ "$($INNER display-message -p -t inner '#{window_index}')" -eq 1 ] || + fail "server did not select window 1" +wait_outer_has B-WINDOW + +exit 0