From 2d6aed1d92d22864472b7958c68f3013c81ea050 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 1 Sep 2026 21:52:47 +0100 Subject: [PATCH] Add some more tests for recent stuff. --- regress/clear-on-attach.sh | 90 +++++++++++++++++ regress/client-key-order-focus.sh | 82 ++++++++++++++++ regress/control-client-popup.sh | 63 ++++++++++++ regress/copy-mode-selection-mode.sh | 81 ++++++++++++++++ regress/format-variables.sh | 18 ++++ regress/screen-redraw-scrollbars-auto-hide.sh | 96 +++++++++++++++++++ regress/terminal-feature-utf8.sh | 58 +++++++++++ 7 files changed, 488 insertions(+) create mode 100644 regress/clear-on-attach.sh create mode 100644 regress/client-key-order-focus.sh create mode 100644 regress/control-client-popup.sh create mode 100644 regress/copy-mode-selection-mode.sh create mode 100644 regress/screen-redraw-scrollbars-auto-hide.sh create mode 100644 regress/terminal-feature-utf8.sh diff --git a/regress/clear-on-attach.sh b/regress/clear-on-attach.sh new file mode 100644 index 000000000..89c8ebbbe --- /dev/null +++ b/regress/clear-on-attach.sh @@ -0,0 +1,90 @@ +#!/bin/sh + +# With clear-on-attach disabled tmux must preserve the terminal contents by +# scrolling them away, rather than entering and clearing the alternate screen. +# Capture the bytes written by a real attached client for both settings. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +INNER_ON="$TEST_TMUX -LtestIon$$ -f/dev/null" +OUTER_ON="$TEST_TMUX -LtestOon$$ -f/dev/null" +INNER_OFF="$TEST_TMUX -LtestIoff$$ -f/dev/null" +OUTER_OFF="$TEST_TMUX -LtestOoff$$ -f/dev/null" +DIR=$(mktemp -d) || exit 1 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $OUTER_ON kill-server 2>/dev/null + $INNER_ON kill-server 2>/dev/null + $OUTER_OFF kill-server 2>/dev/null + $INNER_OFF kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + $current_inner list-clients 2>/dev/null | grep -q . && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +capture_attach() +{ + setting=$1 + output=$2 + go=$DIR/go-$setting + if [ "$setting" = on ]; then + current_inner=$INNER_ON + current_outer=$OUTER_ON + else + current_inner=$INNER_OFF + current_outer=$OUTER_OFF + fi + + $current_inner new-session -d -s inner -x40 -y10 \ + 'exec sleep 100' || exit 1 + $current_inner set-option -g status off || exit 1 + $current_inner set-option -g clear-on-attach "$setting" || exit 1 + $current_outer new-session -d -s outer -x40 -y10 \ + "while [ ! -e '$go' ]; do sleep 0.01; done; exec $current_inner attach-session -t inner" || exit 1 + $current_outer set-option -g status off || exit 1 + $current_outer pipe-pane -O "cat >'$output'" || exit 1 + touch "$go" + wait_for_client + sleep 0.5 + $current_outer pipe-pane -O || exit 1 +} + +on=$DIR/on +off=$DIR/off +capture_attach on "$on" +capture_attach off "$off" + +smcup=$(printf '\033[?1049h') +grep -Fq "$smcup" "$on" || fail "clear-on-attach on did not use smcup" +if grep -Fq "$smcup" "$off"; then + fail "clear-on-attach off used smcup" +fi + +# screen has the indn capability (CSI Ps S), which the preserving path uses +# after setting the scrolling region and moving to its last line. +escape=$(printf '\033') +LC_ALL=C grep -Eq "${escape}\\[[0-9]+S" "$off" || + fail "clear-on-attach off did not scroll the old contents away" + +exit 0 diff --git a/regress/client-key-order-focus.sh b/regress/client-key-order-focus.sh new file mode 100644 index 000000000..4b0c27eb5 --- /dev/null +++ b/regress/client-key-order-focus.sh @@ -0,0 +1,82 @@ +#!/bin/sh + +# send-keys -K injects a key into a client. Commands produced by that key must +# be inserted immediately after send-keys, before later commands in the same +# command list. Focus events arriving between a prefix and its following key +# must be forwarded without cancelling the prefix table. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -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 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_for_option() +{ + option=$1 + expected=$2 + i=0 + while [ "$i" -lt 50 ]; do + actual=$($INNER show-option -gv "$option" 2>/dev/null) + [ "$actual" = "$expected" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "$option is '$actual', expected '$expected'" +} + +cleanup +$INNER new-session -d -s inner -x40 -y10 'exec sleep 100' || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g focus-events on || exit 1 +$OUTER new-session -d -s outer -x40 -y10 "$INNER attach -t inner" || exit 1 +$OUTER set-option -g status off || exit 1 +wait_for_client + +# The binding generated by send-keys -K must run before the command which +# follows send-keys in this command list. Before the queue-ordering fix this +# produces AB rather than BA. +$INNER set-option -g @order '' || exit 1 +$INNER bind-key -n a set-option -gF @order '#{@order}B' || exit 1 +$INNER send-keys -K -c "$client" a \; \ + set-option -gF @order '#{@order}A' || exit 1 +wait_for_option @order BA + +# Put the real attached client in the prefix table, deliver FocusIn, then the +# bound key. FocusIn is not bindable and must not reset the prefix table. +$INNER bind-key x set-option -g @focus-prefix yes || exit 1 +$OUTER send-keys C-b || exit 1 +focus_in=$(printf '\033[I') +$OUTER send-keys -l "$focus_in" || exit 1 +$OUTER send-keys x || exit 1 +wait_for_option @focus-prefix yes + +exit 0 diff --git a/regress/control-client-popup.sh b/regress/control-client-popup.sh new file mode 100644 index 000000000..8a98d34e9 --- /dev/null +++ b/regress/control-client-popup.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +# Popups require a tty overlay and cannot be displayed by a control client. +# A popup command from control mode must be ignored cleanly, leaving the +# client command queue and server usable. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +DIR=$(mktemp -d) || exit 1 +FIFO=$DIR/input +OUT=$DIR/output +RAN=$DIR/popup-ran + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + exec 3>&- + $TMUX kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +mkfifo "$FIFO" || exit 1 +$TMUX new-session -d -s control -x40 -y10 'exec sleep 100' || exit 1 +$TMUX -C attach-session -t control <"$FIFO" >"$OUT" 2>&1 & +control_pid=$! +exec 3>"$FIFO" + +i=0 +while [ "$i" -lt 50 ]; do + $TMUX list-clients -F '#{client_flags}' 2>/dev/null | + grep -q 'control-mode' && break + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "control client did not attach" + +printf '%s\n' "display-popup -E 'touch $RAN'" >&3 +printf '%s\n' "display-message -p CONTROL-POPUP-ALIVE" >&3 + +i=0 +while [ "$i" -lt 50 ]; do + grep -q 'CONTROL-POPUP-ALIVE' "$OUT" 2>/dev/null && break + if ! kill -0 "$control_pid" 2>/dev/null; then + fail "control client exited after display-popup" + fi + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "command after control-client popup did not run" +$TMUX has-session -t control || fail "server exited after control-client popup" +[ ! -e "$RAN" ] || fail "control client started a popup command" + +exit 0 diff --git a/regress/copy-mode-selection-mode.sh b/regress/copy-mode-selection-mode.sh new file mode 100644 index 000000000..bcd070d59 --- /dev/null +++ b/regress/copy-mode-selection-mode.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +# Changing an existing character selection to line mode must reset both ends +# to complete lines, including when the cursor subsequently crosses the fixed +# end. An invalid regular-expression search must also discard marks left by a +# previous successful search. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null +} +trap cleanup 0 1 15 + +$TMUX new-session -d -x30 -y6 \ + "printf 'alpha\nbeta\ngamma\ndelta\n'; exec sleep 100" || exit 1 +$TMUX set-option -g window-size manual || exit 1 +sleep 1 + +# A valid regular-expression search creates marks. Replacing it with an +# invalid expression, growing the mode, and scrolling must not leave an old +# mark array indexed with the new geometry (the original bug was an ASan heap +# overflow in this sequence). +$TMUX copy-mode || exit 1 +$TMUX send-keys -X history-top || exit 1 +$TMUX send-keys -X search-forward beta || exit 1 +[ "$($TMUX display-message -p '#{search_present}')" = 1 ] || + fail "successful search did not create marks" +$TMUX send-keys -X search-forward '[' || exit 1 +$TMUX resize-window -x40 -y8 || exit 1 +$TMUX send-keys -X scroll-down || exit 1 +[ "$($TMUX display-message -p '#{search_present}')" = 0 ] || + fail "invalid regular expression left stale marks after resize" +$TMUX send-keys -X cancel || exit 1 + +# Select part of alpha through part of beta, then change the existing +# selection to line mode. Both lines, and no partial columns, must be copied. +$TMUX copy-mode || exit 1 +$TMUX send-keys -X history-top || exit 1 +$TMUX send-keys -N2 -X cursor-right || exit 1 +$TMUX send-keys -X begin-selection || exit 1 +$TMUX send-keys -X cursor-down || exit 1 +$TMUX send-keys -N2 -X cursor-right || exit 1 +$TMUX send-keys -X selection-mode line || exit 1 +[ "$($TMUX display-message -p '#{selection_mode}')" = line ] || + fail "selection did not change to line mode" +$TMUX send-keys -X copy-selection || exit 1 +expected=$(printf 'alpha\nbeta') +[ "$($TMUX show-buffer)" = "$expected" ] || + fail "line-mode selection did not expand to complete lines" + +# Start with the cursor above the fixed end so selection-mode itself takes the +# reverse-direction path, then extend it through another line. +$TMUX copy-mode || exit 1 +$TMUX send-keys -X history-top || exit 1 +$TMUX send-keys -N2 -X cursor-down || exit 1 +$TMUX send-keys -N2 -X cursor-right || exit 1 +$TMUX send-keys -X begin-selection || exit 1 +$TMUX send-keys -X cursor-up || exit 1 +$TMUX send-keys -N2 -X cursor-right || exit 1 +$TMUX send-keys -X selection-mode line || exit 1 +$TMUX send-keys -X cursor-up || exit 1 +$TMUX send-keys -X copy-selection || exit 1 +expected=$(printf 'alpha\nbeta\ngamma') +[ "$($TMUX show-buffer)" = "$expected" ] || + fail "reversed line-mode selection did not keep complete lines" + +exit 0 diff --git a/regress/format-variables.sh b/regress/format-variables.sh index 449712e34..6908ad956 100644 --- a/regress/format-variables.sh +++ b/regress/format-variables.sh @@ -71,7 +71,10 @@ cursor_very_visible cursor_x cursor_y history_all_bytes +history_added history_bytes +history_collected +history_generation history_limit history_size host @@ -103,6 +106,11 @@ pane_at_right pane_at_top pane_bg pane_bottom +pane_command_duration +pane_command_end_time +pane_command_running +pane_command_start_time +pane_command_status pane_current_command pane_current_path pane_dead @@ -120,16 +128,21 @@ pane_index pane_input_off pane_key_mode pane_last +pane_last_output_time +pane_last_prompt_time pane_left pane_marked pane_marked_set +pane_modal_flag pane_mode +pane_output_generation pane_path pane_pb_progress pane_pb_state pane_pid pane_pipe pane_pipe_pid +pane_private_modes pane_right pane_search_string pane_start_command @@ -141,6 +154,8 @@ pane_title pane_top pane_tty pane_unseen_changes +pane_unzoomed_height +pane_unzoomed_width pane_width pane_x pane_y @@ -206,7 +221,10 @@ window_layout window_linked window_linked_sessions window_linked_sessions_list +window_manual_height +window_manual_width window_marked_flag +window_modal_pane window_name window_offset_x window_offset_y diff --git a/regress/screen-redraw-scrollbars-auto-hide.sh b/regress/screen-redraw-scrollbars-auto-hide.sh new file mode 100644 index 000000000..85093a5ae --- /dev/null +++ b/regress/screen-redraw-scrollbars-auto-hide.sh @@ -0,0 +1,96 @@ +#!/bin/sh + +# Auto-hide scrollbars appear on activity or pointer hover, then disappear +# after pane-scrollbars-timeout. Compare captures of the same copy-mode view so +# only the overlay scrollbar changes. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUTER="$TEST_TMUX -LtestA$$ -f/dev/null" +INNER="$TEST_TMUX -LtestB$$ -f/dev/null" +DIR=$(mktemp -d) || exit 1 +HIDDEN=$DIR/hidden +VISIBLE=$DIR/visible +AFTER=$DIR/after + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +capture() +{ + $OUTER capture-pane -pe -t outer:0.0 >"$1" || exit 1 +} + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + $INNER list-clients 2>/dev/null | grep -q . && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +$INNER new-session -d -s inner -x40 -y12 \ + "sh -c 'seq 50; exec sleep 100'" || 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-window-option pane-scrollbars auto-hide || exit 1 +$INNER set-window-option pane-scrollbars-timeout 300 || exit 1 +$INNER set-window-option pane-scrollbars-style \ + 'bg=colour196,fg=colour231,width=1,pad=0' || exit 1 + +$OUTER new-session -d -s outer -x40 -y12 "$INNER attach -t inner" || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +wait_for_client + +# Enter copy mode and choose a stable view. The initial show timer is allowed +# to expire before taking the hidden reference capture. +$INNER copy-mode -H || exit 1 +$INNER send-keys -X history-top || exit 1 +sleep 0.7 +capture "$HIDDEN" + +# A copy-mode page movement shows the overlay and starts its timer. Capture +# that view immediately, then again after the timeout without moving it. +$INNER send-keys -X page-down || exit 1 +sleep 0.1 +capture "$VISIBLE" +sleep 0.7 +capture "$AFTER" +cmp -s "$VISIBLE" "$AFTER" && fail "scrollbar did not hide after timeout" + +# Use the post-movement hidden scene as the reference for pointer hover. SGR +# mouse column 40 is the right-hand scrollbar; moving to column 5 restarts the +# timer without changing the copy-mode view. +cp "$AFTER" "$HIDDEN" +hover=$(printf '\033[<35;40;5M') +$OUTER send-keys -l "$hover" || exit 1 +sleep 0.1 +capture "$VISIBLE" +cmp -s "$HIDDEN" "$VISIBLE" && fail "scrollbar did not appear on hover" +away=$(printf '\033[<35;5;5M') +$OUTER send-keys -l "$away" || exit 1 +sleep 0.7 +capture "$AFTER" +cmp -s "$HIDDEN" "$AFTER" || fail "scrollbar did not hide after hover" + +exit 0 diff --git a/regress/terminal-feature-utf8.sh b/regress/terminal-feature-utf8.sh new file mode 100644 index 000000000..b1e0a1fbd --- /dev/null +++ b/regress/terminal-feature-utf8.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +# The utf8 terminal feature must mark an attached client as UTF-8 even when +# neither its environment nor -u does so. It must also be reported by the +# client terminal-feature format lookup. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -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 1 15 + +cleanup +$INNER new-session -d -s inner -x40 -y10 'exec sleep 100' || exit 1 +$INNER set-option -g status off || exit 1 + +# A clean C-locale environment avoids the ordinary UTF-8 detection paths. +# -T utf8 is therefore solely responsible for setting client_utf8. +client_command="env -i PATH=/bin:/usr/bin TERM=screen LC_ALL=C $TEST_TMUX -T utf8 -LtestI$$ -f/dev/null attach-session -t inner" +$OUTER new-session -d -s outer -x40 -y10 "$client_command" || exit 1 +$OUTER set-option -g status off || exit 1 + +i=0 +while [ "$i" -lt 50 ]; do + client=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + utf8=$($INNER list-clients -F '#{client_utf8}' 2>/dev/null) + [ -n "$client" ] && [ "$utf8" = 1 ] && break + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "utf8 terminal feature did not set client_utf8" + +features=$($INNER list-clients -F '#{client_termfeatures}') +case ",$features," in +*,utf8,*) ;; +*) fail "utf8 is missing from client_termfeatures: $features" ;; +esac + +present=$($INNER display-message -c "$client" -p '#{I/f:utf8}') +[ "$present" = 1 ] || fail "utf8 terminal feature lookup returned '$present'" + +exit 0