diff --git a/regress/format-render-contexts.sh b/regress/format-render-contexts.sh new file mode 100644 index 000000000..81f1de462 --- /dev/null +++ b/regress/format-render-contexts.sh @@ -0,0 +1,281 @@ +#!/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. + +PATH=/bin:/usr/bin +TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export 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" +LIMIT=20000 + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} + +fail() +{ + echo "$1" >&2 + cleanup + exit 1 +} + +run_cmd() +{ + if command -v timeout >/dev/null 2>&1; then + timeout 10 "$@" + else + "$@" + fi +} + +bounded() +{ + name="$1" + text="$2" + n=$(printf '%s' "$text" | wc -c | tr -d ' ') + + [ "$n" -le "$LIMIT" ] || fail "$name produced $n bytes" +} + +tmux_run() +{ + name="$1" + shift + + out=$(run_cmd $TMUX "$@" 2>&1) + rc=$? + bounded "$name" "$out" + [ "$rc" -eq 0 ] || fail "$name failed ($rc): $out" + + printf '%s' "$out" +} + +capture() +{ + out=$(run_cmd $TMUX2 capture-pane -p -t out:0 2>/dev/null) + rc=$? + bounded "capture-pane" "$out" + [ "$rc" -eq 0 ] || fail "capture-pane failed" + printf '%s\n' "$out" +} + +capture_esc() +{ + out=$(run_cmd $TMUX2 capture-pane -Cep -t out:0 2>/dev/null) + rc=$? + bounded "capture-pane -e" "$out" + [ "$rc" -eq 0 ] || fail "capture-pane -e failed" + printf '%s\n' "$out" +} + +assert_alive() +{ + out=$(tmux_run "server alive ($1)" display-message -p alive) + [ "$out" = "alive" ] || fail "server not alive after $1" +} + +wait_for() +{ + marker="$1" + i=0 + while [ "$i" -lt 50 ]; do + if capture | grep -q "$marker"; then + sleep 0.1 + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for $marker" +} + +render_status_options() +{ + label="$1" + fmt="$2" + + tmux_run "$label status-left" \ + set-option -g status-left "SL$label: $fmt" >/dev/null + tmux_run "$label status-right" \ + set-option -g status-right "SR$label: $fmt" >/dev/null + tmux_run "$label status-format left/right" \ + set-option -g status-format[0] \ + '#{E:status-left}#[align=right]#{E:status-right}' >/dev/null + wait_for "SL$label" + assert_alive "$label status-left/right" +} + +render_status_format() +{ + 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" + assert_alive "$label status-format" +} + +render_message() +{ + 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" + assert_alive "$label display-message" +} + +render_choose_tree() +{ + label="$1" + fmt="$2" + + tmux_run "$label choose-tree" \ + choose-tree -t fmt:0 -F "CT$label: $fmt" >/dev/null + 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" + + tmux_run "$label customize-mode" \ + customize-mode -t fmt:0 -F "CM$label: $fmt" >/dev/null + sleep 0.5 + out=$(capture) + bounded "$label customize-mode capture" "$out" + tmux_run "$label quit customize-mode" send-keys -t fmt:0 q >/dev/null + assert_alive "$label customize-mode" +} + +render_list_output() +{ + 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" + + 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" + + 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" +} + +run_corpus() +{ + label="$1" + fmt="$2" + + render_status_options "$label" "$fmt" + render_status_format "$label" "$fmt" + render_message "$label" "$fmt" + render_choose_tree "$label" "$fmt" + render_customize "$label" "$fmt" + render_list_output "$label" "$fmt" +} + +trap cleanup 0 1 15 +cleanup + +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" \ + 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" \ + >/dev/null 2>&1 || fail "failed to start outer client" + +i=0 +while [ "$i" -lt 50 ]; do + c=$(tmux_run "wait clients" list-clients -F x | grep -c x) + [ "$c" -eq 1 ] && break + sleep 0.2 + i=$((i + 1)) +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,}' + +check_sgr_sanity + +cleanup +exit 0 diff --git a/regress/input-reflow-stress.sh b/regress/input-reflow-stress.sh new file mode 100644 index 000000000..c673b3472 --- /dev/null +++ b/regress/input-reflow-stress.sh @@ -0,0 +1,281 @@ +#!/bin/sh + +WIDTHS="80 40 20 10 7 5 4 3 2 1 2 3 4 5 7 10 20 40 80" +HISTORY_LIMIT=220 +HISTORY_BOUND=12000 +JOINED_BOUND=500000 +RAW_BOUND=5000000 + +record_fail() +{ + echo "FAIL: $1" + exit_status=1 +} + +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)) + i=$((i + 1)) + done +} + +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' +} + +load_and_paste() +{ + buffer=$1 + shift + + "$@" | $TMUX load-buffer -b "$buffer" - || 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" +} + +assert_joined_sane() +{ + label=$1 + check_sentinels=${2:-yes} + + capture_joined || { + record_fail "joined capture failed after $label" + return + } + + bytes=$(wc -c <"$TMP") + if [ "$bytes" -gt "$JOINED_BOUND" ]; then + record_fail "joined capture too large after $label: $bytes" + fi + + if grep -q "$(printf '\357\277\275')" "$TMP"; then + record_fail "replacement character in joined capture after $label" + fi + + awk -v label="$label" ' + { + line = $0 + seen_on_line = 0 + while (match(line, /L[0-9][0-9][0-9][0-9]\|/)) { + 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 + found = 1 + line = substr(line, RSTART + RLENGTH) + } + 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 + } + } + ' "$TMP" || { + cp "$TMP" "$EXP" + record_fail "joined structure failed after $label" + } + + 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 +} + +assert_raw_sane() +{ + label=$1 + width=$2 + + $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 + } + + data = $0 + if (data !~ /data=\(/) + 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" + } +} + +assert_history_sane() +{ + label=$1 + + size=$($TMUX display-message -p -t stress: '#{history_size}') || { + record_fail "history-size failed after $label" + return + } + 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 + ;; + esac +} + +assert_copy_mode_sane() +{ + label=$1 + + $TMUX capture-pane -pM -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" + fi + if grep -q "$(printf '\357\277\275')" "$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 +} + +run_resize_checks() +{ + for width in $WIDTHS; do + $TMUX resize-window -t stress: -x "$width" -y 8 || exit 1 + sleep 0.1 + assert_alive "resize to $width" + assert_joined_sane "resize to $width" + assert_raw_sane "resize to $width" "$width" + assert_history_sane "resize to $width" + done +} + +$TMUX 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 +sleep 0.3 + +load_and_paste stress-data make_payload +run_resize_checks + +$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 + +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 + sleep 0.1 + assert_alive "alternate resize to $width" + assert_raw_sane "alternate resize to $width" "$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 +sleep 0.3 +assert_joined_sane "alternate screen exit" no + +$TMUX kill-server 2>/dev/null +exit $exit_status diff --git a/regress/lifecycle-deferred.sh b/regress/lifecycle-deferred.sh new file mode 100644 index 000000000..2650f1827 --- /dev/null +++ b/regress/lifecycle-deferred.sh @@ -0,0 +1,216 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +TMUX="$TEST_TMUX -Llifecycle-deferred -f/dev/null" +TMUX2="$TEST_TMUX -Llifecycle-deferred-outer -f/dev/null" + +TMPDIR=$(mktemp -d) +IN="$TMPDIR/in" +OUT="$TMPDIR/out" +CONTROL_PID= + +cleanup() +{ + exec 3>&- 2>/dev/null + [ -n "$CONTROL_PID" ] && kill "$CONTROL_PID" 2>/dev/null + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + rm -rf "$TMPDIR" +} + +fail() +{ + echo "$1" >&2 + [ -s "$OUT" ] && sed -n '1,120p' "$OUT" >&2 + cleanup + exit 1 +} + +run_tmux() +{ + out= + if command -v timeout >/dev/null 2>&1; then + out=$(timeout 10 $TMUX "$@" 2>&1) + else + out=$($TMUX "$@" 2>&1) + fi + rc=$? + [ "$rc" -eq 0 ] || fail "tmux $* failed ($rc): $out" + printf '%s' "$out" +} + +send_control() +{ + printf '%s\n' "$1" >&3 || fail "failed to write control command: $1" +} + +wait_clients() +{ + want=$1 + i=0 + + while [ "$i" -lt 50 ]; do + have=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$have" -eq "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + return 1 +} + +wait_format() +{ + target=$1 + format=$2 + want=$3 + i=0 + + while [ "$i" -lt 50 ]; do + have=$($TMUX display-message -p -t "$target" "$format" 2>/dev/null) + [ "$have" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + return 1 +} + +assert_alive() +{ + run_tmux has-session -t life >/dev/null + fields=$(run_tmux display-message -p -t life \ + '#{session_name}:#{window_id}:#{pane_id}:#{session_windows}:#{window_panes}') + case "$fields" in + life:@*:%*:*) ;; + *) fail "bad current fields after $1: $fields" ;; + esac +} + +check_control_output() +{ + sleep 1 + + if grep -E '(^%error |server exited|lost server|\(null\)|no current)' \ + "$OUT" >/dev/null 2>&1; then + fail "control client reported an error or invalid object" + fi + + awk ' + $1 == "%session-window-changed" { + if (NF != 3 || $2 !~ /^\$[0-9]+$/ || $3 !~ /^@[0-9]+$/) + bad = 1 + } + $1 == "%subscription-changed" { + colon = 0 + for (i = 2; i <= NF; i++) + if ($i == ":") + colon = 1 + if (NF < 7 || !colon) + bad = 1 + } + END { exit bad } + ' "$OUT" || fail "control client received a malformed notification" +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +run_tmux new-session -d -s life -n prompt -x 80 -y 24 'sleep 1000' \ + >/dev/null +run_tmux set-option -g detach-on-destroy off >/dev/null +run_tmux new-window -t life -n tree 'sleep 1000' >/dev/null +run_tmux new-window -t life -n work 'sleep 1000' >/dev/null +run_tmux select-window -t life:prompt >/dev/null + +run_tmux set-hook -g after-new-window \ + 'display-message -p "hook-new #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g after-split-window \ + 'display-message -p "hook-split #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g after-kill-pane \ + 'display-message -p "hook-kill #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g pane-exited \ + 'display-message -p "hook-exit #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g window-layout-changed \ + 'display-message -p "hook-layout #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g session-window-changed \ + 'display-message -p "hook-current #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux bind-key -n M-p command-prompt -P -p '(life)' \ + "set -g @lifecycle-prompt '%% #{session_name}:#{window_id}:#{pane_id}'" \ + >/dev/null + +$TMUX2 new-session -d -s outer -n prompt -x 80 -y 24 "$TMUX attach -t life" \ + || fail "failed to start first attached client" +$TMUX2 new-window -t outer -n tree "$TMUX attach -t life" \ + || fail "failed to start second attached client" +wait_clients 2 || fail "normal clients did not attach" + +$TMUX2 send-keys -t outer:prompt M-p || fail "failed to open command prompt" +sleep 1 +run_tmux choose-tree -t life:tree.0 \ + -F 'tree #{session_name}:#{window_id}:#{pane_id}' >/dev/null +wait_format life:tree.0 '#{pane_mode}' tree-mode || \ + fail "choose-tree did not enter tree-mode" + +mkfifo "$IN" || fail "failed to create control fifo" +(cat "$IN" | $TMUX -C attach -t life >"$OUT" 2>&1) & +CONTROL_PID=$! +exec 3>"$IN" +wait_clients 3 || fail "control client did not attach" + +CONTROL_CLIENT=$($TMUX list-clients -F '#{client_name} #{client_control_mode}' | + awk '$2 == 1 { print $1; exit }') +[ -n "$CONTROL_CLIENT" ] || fail "missing control client" + +send_control "refresh-client -B 'all:%*:#{session_name}:#{window_id}:#{pane_id}:#{session_windows}:#{window_panes}'" +send_control "refresh-client -B 'windows:@*:#{session_name}:#{window_id}:#{window_index}:#{window_panes}'" +sleep 1 + +run_tmux kill-pane -t life:prompt.0 >/dev/null +run_tmux kill-window -t life:tree >/dev/null +assert_alive "killing prompt and tree panes" + +i=1 +while [ "$i" -le 20 ]; do + # Keep this sequence fixed: failures should reproduce on the same pass. + s=ld$i + ctl=ctl$i + idx=$((50 + i)) + + run_tmux new-session -d -s "$s" -n base 'sleep 1000' >/dev/null + run_tmux split-window -t "$s:base" 'sleep 1000' >/dev/null + run_tmux respawn-pane -k -t "$s:base.1" 'sleep 1000' >/dev/null + run_tmux kill-pane -t "$s:base.1" >/dev/null + + run_tmux new-window -t "$s" -n second 'sleep 1000' >/dev/null + run_tmux link-window -s "$s:second" -t "life:$idx" >/dev/null + run_tmux unlink-window -t "life:$idx" >/dev/null + + run_tmux new-window -t "$s" -n single 'sleep 1000' >/dev/null + run_tmux kill-pane -t "$s:single.0" >/dev/null + run_tmux kill-window -t "$s:base" >/dev/null + + run_tmux new-session -d -s "$ctl" -n ctl 'sleep 1000' >/dev/null + run_tmux switch-client -c "$CONTROL_CLIENT" -t "$ctl" >/dev/null + run_tmux kill-session -t "$ctl" >/dev/null + + run_tmux kill-session -t "$s" >/dev/null + assert_alive "iteration $i" + + i=$((i + 1)) +done + +check_control_output +cleanup +exit 0 diff --git a/regress/mode-mutation.sh b/regress/mode-mutation.sh new file mode 100644 index 000000000..cab8e72c8 --- /dev/null +++ b/regress/mode-mutation.sh @@ -0,0 +1,297 @@ +#!/bin/sh + +# Exercise modes while their backing objects are changed from outside the +# client displaying the mode. This catches stale selection indexes and pointers +# after a mode list is shrunk or rebuilt. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + sleep 0.5 +} + +fail() +{ + echo "$1" + cleanup + exit 1 +} + +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +assert_alive() +{ + $TMUX display-message -p 'alive' >/dev/null 2>&1 || \ + fail "$1: server exited" +} + +wait_clients() +{ + i=0 + while [ "$i" -lt 50 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "expected $1 clients, have $c" +} + +wait_for() +{ + i=0 + while [ "$i" -lt 50 ]; do + capture | grep -q "$1" && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1'" +} + +wait_mode() +{ + t=$1 + want=$2 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane $t mode state is $got, expected $want" +} + +repeat_key() +{ + t=$1 + key=$2 + count=$3 + + i=0 + while [ "$i" -lt "$count" ]; do + $TMUX send-keys -t "$t" "$key" || \ + fail "failed to send $key to $t" + i=$((i + 1)) + done +} + +start_client() +{ + s=$1 + cmd=${2:-cat} + + cleanup + $TMUX new-session -d -s "$s" -n main -x 80 -y 24 "$cmd" || \ + fail "$s: new-session failed" + $TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t $s" || \ + fail "$s: outer client failed" + wait_clients 1 +} + +new_outer_client() +{ + s=$1 + + $TMUX2 new-window -d -t out: "$TMUX attach -t $s" || \ + fail "$s: outer client failed" +} + +client_for_session() +{ + $TMUX list-clients -F '#{client_name} #{client_session}' | + awk -v s="$1" '$2 == s { print $1; exit }' +} + +test_choose_tree() +{ + start_client tree-a + $TMUX new-session -d -s tree-b -n one 'cat' || fail "tree-b failed" + $TMUX new-window -d -t tree-b -n two 'cat' || fail "tree-b:1 failed" + $TMUX new-session -d -s tree-c -n one 'cat' || fail "tree-c failed" + $TMUX new-session -d -s tree-d -n one 'cat' || fail "tree-d failed" + $TMUX split-window -d -t tree-a:0 'cat' || fail "tree split failed" + + $TMUX choose-tree -t tree-a:0 -O index -F 'MT #{session_name}:#{window_index}.#{pane_index}' || \ + fail "choose-tree failed" + wait_for 'MT ' + repeat_key tree-a:0 j 40 + + $TMUX kill-session -t tree-d || fail "tree kill-session failed" + $TMUX kill-session -t tree-c || fail "tree kill-session failed" + $TMUX rename-session -t tree-b tree-renamed || fail "tree rename failed" + $TMUX rename-window -t tree-renamed:0 renamed || fail "tree rename-window failed" + $TMUX kill-window -t tree-renamed:1 || fail "tree kill-window failed" + side=$($TMUX split-window -d -P -F '#{pane_id}' -t tree-a:0 'cat') || \ + fail "tree side split failed" + $TMUX break-pane -d -s "$side" || fail "tree break-pane failed" + $TMUX join-pane -d -s "$side" -t tree-a:0.0 || \ + fail "tree join-pane failed" + i=0 + while [ "$i" -lt 12 ]; do + $TMUX new-window -d -t tree-a -n "new$i" 'cat' || \ + fail "tree new-window failed" + i=$((i + 1)) + done + + assert_alive "choose-tree mutation" + $TMUX send-keys -t tree-a:0 k j l h Enter || \ + fail "choose-tree keys failed" + wait_mode tree-a:0 0 + assert_alive "choose-tree exit" +} + +test_choose_buffer() +{ + start_client buffer-a + + i=0 + while [ "$i" -lt 30 ]; do + $TMUX set-buffer -b "mbuf$i" "buffer mutation $i" || \ + fail "set-buffer failed" + i=$((i + 1)) + done + + $TMUX choose-buffer -t buffer-a:0 -F 'MB #{buffer_name}' || \ + fail "choose-buffer failed" + wait_for 'MB ' + repeat_key buffer-a:0 j 40 + + i=8 + while [ "$i" -lt 30 ]; do + $TMUX delete-buffer -b "mbuf$i" || fail "delete-buffer failed" + i=$((i + 1)) + done + i=30 + while [ "$i" -lt 50 ]; do + $TMUX set-buffer -b "mbuf$i" "new buffer mutation $i" || \ + fail "new set-buffer failed" + i=$((i + 1)) + done + + assert_alive "choose-buffer mutation" + $TMUX send-keys -t buffer-a:0 k j Enter || \ + fail "choose-buffer keys failed" + wait_mode buffer-a:0 0 + assert_alive "choose-buffer exit" +} + +test_choose_client() +{ + start_client client-a + $TMUX new-session -d -s client-b -n main 'cat' || fail "client-b failed" + $TMUX new-session -d -s client-c -n main 'cat' || fail "client-c failed" + new_outer_client client-b + new_outer_client client-c + wait_clients 3 + + $TMUX choose-client -t client-a:0 -F 'MC #{client_session}' || \ + fail "choose-client failed" + wait_for 'MC ' + repeat_key client-a:0 j 20 + + c=$(client_for_session client-c) + [ -n "$c" ] || fail "client-c client not found" + $TMUX detach-client -t "$c" || fail "detach client-c failed" + c=$(client_for_session client-b) + [ -n "$c" ] || fail "client-b client not found" + $TMUX detach-client -t "$c" || fail "detach client-b failed" + + $TMUX new-session -d -s client-d -n main 'cat' || fail "client-d failed" + new_outer_client client-d + wait_clients 2 + + assert_alive "choose-client mutation" + $TMUX send-keys -t client-a:0 k j Enter || \ + fail "choose-client keys failed" + wait_mode client-a:0 0 + assert_alive "choose-client exit" +} + +test_customize_mode() +{ + start_client option-a + + i=0 + while [ "$i" -lt 30 ]; do + $TMUX set-option -g "@mode_mut_$i" "$i" || \ + fail "set option failed" + i=$((i + 1)) + done + + $TMUX customize-mode -t option-a:0 -F 'MO #{option_name}=#{option_value}' || \ + fail "customize-mode failed" + wait_mode option-a:0 1 + repeat_key option-a:0 j 80 + + i=10 + while [ "$i" -lt 30 ]; do + $TMUX set-option -gu "@mode_mut_$i" || fail "unset option failed" + i=$((i + 1)) + done + i=30 + while [ "$i" -lt 55 ]; do + $TMUX set-option -g "@mode_mut_$i" "$i" || \ + fail "new option failed" + i=$((i + 1)) + done + $TMUX set-option -g status-left 'mutated' || fail "status-left failed" + $TMUX rename-session -t option-a option-renamed || fail "option rename failed" + + assert_alive "customize-mode mutation" + $TMUX send-keys -t option-renamed:0 k j C-d C-u q || \ + fail "customize-mode keys failed" + wait_mode option-renamed:0 0 + assert_alive "customize-mode exit" +} + +test_copy_mode() +{ + start_client copy-a 'i=0; while [ $i -lt 200 ]; do echo "copy mutation line $i"; i=$((i + 1)); done; cat' + $TMUX set-window-option -g mode-keys vi || fail "mode-keys failed" + $TMUX split-window -d -t copy-a:0 'cat' || fail "copy split failed" + + $TMUX copy-mode -t copy-a:0 || fail "copy-mode failed" + wait_mode copy-a:0 1 + repeat_key copy-a:0 k 20 + + $TMUX rename-window -t copy-a:0 renamed || fail "copy rename-window failed" + side=$($TMUX split-window -d -P -F '#{pane_id}' -t copy-a:renamed 'cat') || \ + fail "copy side split failed" + $TMUX break-pane -d -s "$side" || fail "copy break-pane failed" + $TMUX join-pane -d -s "$side" -t copy-a:renamed.0 || \ + fail "copy join-pane failed" + $TMUX kill-pane -t copy-a:renamed.1 || fail "copy kill-pane failed" + $TMUX new-window -d -t copy-a -n extra 'cat' || fail "copy new-window failed" + $TMUX kill-window -t copy-a:extra || fail "copy kill-window failed" + $TMUX rename-session -t copy-a copy-renamed || fail "copy rename failed" + + assert_alive "copy-mode mutation" + $TMUX send-keys -t copy-renamed:renamed.0 j k C-d C-u q || \ + fail "copy-mode keys failed" + wait_mode copy-renamed:renamed.0 0 + assert_alive "copy-mode exit" +} + +cleanup +test_choose_tree +test_choose_buffer +test_choose_client +test_customize_mode +test_copy_mode +cleanup +exit 0