diff --git a/regress/layout-custom.sh b/regress/layout-custom.sh index 3b7a2de13..6fc7f76d4 100644 --- a/regress/layout-custom.sh +++ b/regress/layout-custom.sh @@ -377,10 +377,7 @@ must_equal 'Layout after select-pane back' "$(layout L:two)" "$SPLIT" # With nothing zoomed the two layout formats agree. # -# The zoomed case is deliberately not covered here. While a pane is zoomed -# #{window_layout} dumps the saved (unzoomed) layout and -# #{window_visible_layout} the zoomed one, but that depends on how zooming -# stashes the layout root rather than on anything in layout-custom.c. +# Zoomed layouts are checked below with floating panes. must_equal 'Visible layout' "$(visible_layout L:two)" "$SPLIT" # --------------------------------------------------------------------------- @@ -704,6 +701,42 @@ must_contain 'Floating layout back z-index' "$floating" '"z":1' check_ok select-layout -t L:float "$floating" must_equal 'Floating layout after round trip' "$(raw_layout L:float)" "$floating" +# A dump taken while zoomed must retain the unzoomed floating z-indexes. +# Check hidden floats, floats above zoom, and a mixture of the two. +for flags in '' A mixed; do + check_ok new-window -d -t L: -n zoom + zt=$($TMUX display-message -p -t L:zoom '#{pane_id}') + check_ok split-window -d -h -t "$zt" + case "$flags" in + A|mixed) first=-Ad ;; + *) first=-d ;; + esac + case "$flags" in + A) second=-Ad ;; + *) second=-d ;; + esac + check_ok new-pane "$first" -t "$zt" -x 20 -y 6 '' + check_ok new-pane "$second" -t "$zt" -x 30 -y 8 '' + for pane in 0 2 3; do + check_ok select-pane -t "L:zoom.$pane" + before=$(raw_layout L:zoom) + legacy=$(v1_layout L:zoom) + check_ok resize-pane -Z -t "L:zoom.$pane" + during=$(raw_layout L:zoom) + must_equal 'Layout while zoomed' "$during" "$before" + must_equal 'Legacy layout while zoomed' \ + "$(v1_layout L:zoom)" "$legacy" + must_equal 'Zoom after dumping layout' \ + "$($TMUX display-message -p -t L:zoom '#{window_zoomed_flag}')" 1 + must_differ 'Visible layout while zoomed' \ + "$(visible_layout L:zoom)" "$(layout L:zoom)" + check_ok select-layout -t L:zoom "$during" + must_equal 'Round trip from zoomed layout' \ + "$(raw_layout L:zoom)" "$before" + done + check_ok kill-window -t L:zoom +done + # --------------------------------------------------------------------------- # Floating panes and the legacy (v1) format. # diff --git a/regress/sync-output-wrapped.sh b/regress/sync-output-wrapped.sh new file mode 100755 index 000000000..0038eb913 --- /dev/null +++ b/regress/sync-output-wrapped.sh @@ -0,0 +1,116 @@ +#!/bin/sh + +# When only some lines are redrawn after a synchronized update, a line +# following a wrapped line must be drawn at its own position, not where the +# cursor happens to be. + +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 +TMUX_TMPDIR=$DIR +export TMUX_TMPDIR + +INNER="$TEST_TMUX -Li$$ -f/dev/null" +OUTER="$TEST_TMUX -Lo$$ -f/dev/null" +CONTROL=$DIR/control +EMITTER=$DIR/emitter.pl + +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 + +wait_for_file() +{ + i=0 + while [ "$i" -lt 50 ] && [ ! -e "$1" ]; do + sleep 0.1 + i=$((i + 1)) + done + [ -e "$1" ] || fail "$2" +} + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + $INNER list-clients -F '#{client_termfeatures}' 2>/dev/null | + grep -q 'sync' && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "sync-capable client did not attach" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +my $control = $ENV{CONTROL}; +(my $dir = $control) =~ s{/[^/]+$}{}; + +# Fill the screen with 80 column lines and no newlines so every line wraps. +my $out = "\e[H\e[2J"; +for my $row (1 .. 24) { + $out .= substr(sprintf("ROW%02d_", $row) . ('x' x 80), 0, 80); +} +syswrite STDOUT, $out . "\e[1;1H"; + +open my $painted, '>', "$dir/painted" or die "$dir/painted: $!\n"; +close $painted; +while (!-e $control) { + select undef, undef, undef, 0.01; +} + +# Change rows 2 and 6 in one synchronized update. +my $frame = "\e[?2026h" . + "\e[2;1H" . substr("NEW02_" . ('#' x 80), 0, 80) . + "\e[6;1H" . substr("NEW06_" . ('%' x 80), 0, 80) . + "\e[24;1H" . + "\e[?2026l"; +my $written = syswrite STDOUT, $frame; +die "short synchronized frame write\n" + unless defined $written && $written == length $frame; +open my $done, '>', "$dir/done" or die "$dir/done: $!\n"; +close $done; +select undef, undef, undef, 3; +PERL + +$INNER new-session -d -s inner -x 80 -y 24 \ + "CONTROL='$CONTROL' perl '$EMITTER'" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$INNER set-option -as terminal-features '*:sync' || exit 1 + +$OUTER new-session -d -s outer -x 80 -y 24 \ + "$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" || + exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +wait_for_client + +wait_for_file "$DIR/painted" "application did not paint" +sleep 1 +: >"$CONTROL" +wait_for_file "$DIR/done" "application did not finish" +sleep 1 + +$OUTER capture-pane -p -t outer:0.0 >"$DIR/screen" || exit 1 +sed -n 2p "$DIR/screen" | grep -q '^NEW02_#' || fail "row 2 not redrawn" +sed -n 3p "$DIR/screen" | grep -q '^ROW03_x' || fail "row 3 overwritten" +sed -n 6p "$DIR/screen" | grep -q '^NEW06_%' || fail "row 6 not redrawn" +exit 0 diff --git a/regress/wait-for-client-lost.sh b/regress/wait-for-client-lost.sh new file mode 100644 index 000000000..cb36b22a4 --- /dev/null +++ b/regress/wait-for-client-lost.sh @@ -0,0 +1,164 @@ +#!/bin/sh + +# A disconnected waiter must release both its wait and its command queue. +PATH=/bin:/usr/bin +TERM=screen +export TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX="$TEST_TMUX -S$OUT/socket -f/dev/null" +pids= + +cleanup() +{ + for pid in $pids; do + kill "$pid" 2>/dev/null || true + done + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +fail() +{ + echo "$*" >&2 + exit 1 +} + +wait_list() +{ + expected=$1 + shift + i=0 + while [ $i -lt 100 ]; do + value=$($TMUX wait-for -l "$@") || fail "list waiters failed" + [ "$value" = "$expected" ] && return + i=$((i + 1)) + sleep 0.05 + done + fail "expected waiters '$expected', got '$value'" +} + +wait_free() +{ + client_pid=$1 + i=0 + while [ $i -lt 100 ]; do + awk -v pid="$client_pid" ' + $4 == "IDENTIFY_CLIENTPID" && $5 == pid { address = $3 } + address != "" && $2 == "free" && $3 == "client" && + $4 == address && $5 == "(0" { freed = 1 } + END { exit !freed } + ' "$OUT"/tmux-server-*.log && return + i=$((i + 1)) + sleep 0.05 + done + fail "client $client_pid was not freed" +} + +cd "$OUT" || exit 1 +$TMUX -vv new-session -d -s test || fail "new-session failed" +$TMUX set -g @continued 0 || fail "set option failed" + +for signal in TERM KILL; do + for kind in channel lock event; do + name="$kind-$signal" + case $kind in + channel) args="$name"; list_args="$name" ;; + lock) + $TMUX wait-for -L "$name" || fail "lock failed" + args="-L $name"; list_args="$name" + ;; + event) args="-E @$name"; list_args="-E @$name" ;; + esac + + $TMUX wait-for $args \; set -g @continued 1 & + dead=$! + pids="$dead" + wait_list "client-$dead" $list_args + + $TMUX wait-for $args & + live=$! + pids="$dead $live" + wait_list "$(printf 'client-%s\nclient-%s' "$dead" "$live")" \ + $list_args + + kill -"$signal" "$dead" || fail "kill failed" + wait "$dead" 2>/dev/null || true + pids="$live" + wait_list "client-$live" $list_args + wait_free "$dead" + [ "$($TMUX show -gqv @continued)" = 0 ] || + fail "disconnected client's next command ran" + + case $kind in + channel) $TMUX wait-for -S "$name" || fail "signal failed" ;; + lock) $TMUX wait-for -U "$name" || fail "unlock failed" ;; + event) + $TMUX wait-for -E -w "client-$live" "@$name" || + fail "wake event waiter failed" + ;; + esac + wait_list "" $list_args + wait_free "$live" + wait "$live" || fail "surviving waiter failed" + pids= + if [ "$kind" = lock ]; then + $TMUX wait-for -U "$name" || fail "final unlock failed" + fi + done +done + +# Removing the last waiter must remove its otherwise unused channel, and must +# not create a pending signal. Unrelated pending signals must be preserved. +$TMUX wait-for -S saved || fail "save signal failed" +$TMUX wait-for last & +dead=$! +pids="$dead" +wait_list "client-$dead" last +kill -KILL "$dead" || fail "kill last waiter failed" +wait "$dead" 2>/dev/null || true +pids= +wait_list "" last +wait_free "$dead" +grep -F 'remove empty wait channel last' "$OUT"/tmux-server-*.log \ + >/dev/null || fail "empty channel was not removed" + +$TMUX wait-for saved \; wait-for last & +live=$! +pids="$live" +wait_list "client-$live" last +$TMUX wait-for -S last || fail "signal last failed" +wait_free "$live" +wait "$live" || fail "pending signal was not preserved" +pids= + +# Source-file leaves a cleanup callback behind the blocked command. +printf 'wait-for sourced\nset -g @continued 1\n' >"$OUT/source.conf" +$TMUX source-file "$OUT/source.conf" & +dead=$! +pids="$dead" +wait_list "client-$dead" sourced +kill -KILL "$dead" || fail "kill source-file waiter failed" +wait "$dead" 2>/dev/null || true +pids= +wait_list "" sourced +wait_free "$dead" +[ "$($TMUX show -gqv @continued)" = 0 ] || + fail "disconnected source-file client's next command ran" + +# Firing an event after cancellation must not call the removed event sink. +$TMUX wait-for -E window-renamed & +dead=$! +pids="$dead" +wait_list "client-$dead" -E window-renamed +kill -KILL "$dead" || fail "kill event waiter failed" +wait "$dead" 2>/dev/null || true +pids= +wait_list "" -E window-renamed +wait_free "$dead" +$TMUX rename-window -t test:0 renamed || fail "rename-window failed" +$TMUX display-message -p '#{pid}' >/dev/null || fail "server exited" + +exit 0