Add a few more tests.

This commit is contained in:
Nicholas Marriott
2026-07-04 23:09:16 +01:00
parent 7b833d07d9
commit 92028ed6e2
4 changed files with 1075 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

297
regress/mode-mutation.sh Normal file
View File

@@ -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