From 2b2963859b70aa43c3367ae327b16b1904a7036b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 5 Jul 2026 21:56:52 +0100 Subject: [PATCH] Add some hooks tests. --- regress/hooks-after.sh | 124 +++++++++++++++++++++++++ regress/hooks-notify.sh | 199 ++++++++++++++++++++++++++++++++++++++++ regress/hooks.sh | 155 +++++++++++++++++++++++++++++++ 3 files changed, 478 insertions(+) create mode 100644 regress/hooks-after.sh create mode 100644 regress/hooks-notify.sh create mode 100644 regress/hooks.sh diff --git a/regress/hooks-after.sh b/regress/hooks-after.sh new file mode 100644 index 000000000..8677db7e4 --- /dev/null +++ b/regress/hooks-after.sh @@ -0,0 +1,124 @@ +#!/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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest-hooks-after-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +$TMUX new -d -s one || fail "new-session one failed" +$TMUX new -d -s two || fail "new-session two failed" + +# An after hook fires with the command arguments as formats. +$TMUX set -g @after 0 || fail "set @after failed" +$TMUX set-hook -g after-rename-window \ + 'set -gF @after "#{hook}|#{hook_argument_0}|#{hook_flag_t}"' || + fail "set-hook -g after-rename-window failed" +$TMUX rename-window -t one:0 first || fail "rename-window first failed" +wait_for @after 'after-rename-window|first|one:0' + +# An appended hook command runs after the first. +$TMUX set -g @after2 0 || fail "set @after2 failed" +$TMUX set-hook -ga after-rename-window \ + 'set -gF @after2 "#{@after}+2"' || + fail "set-hook -ga after-rename-window failed" +$TMUX rename-window -t one:0 second || fail "rename-window second failed" +wait_for @after 'after-rename-window|second|one:0' +wait_for @after2 'after-rename-window|second|one:0+2' +$TMUX set-hook -gu after-rename-window || fail "set-hook -gu failed" + +# A session after hook only fires for commands targeting that session and +# the hook commands run with the command target as current state. +$TMUX set -g @safter 0 || fail "set @safter failed" +$TMUX set-hook -t two after-rename-window \ + 'set -gF @safter "#{hook}:#{session_name}"' || + fail "set-hook -t two after-rename-window failed" +$TMUX rename-window -t one:0 third || fail "rename-window third failed" +assert_unchanged @safter 0 +$TMUX rename-window -t two:0 fourth || fail "rename-window fourth failed" +wait_for @safter 'after-rename-window:two' +$TMUX set-hook -u -t two after-rename-window || + fail "set-hook -u -t two failed" + +# The command-error hook fires when a command fails. +$TMUX set -g @error 0 || fail "set @error failed" +$TMUX set-hook -g command-error 'set -gF @error "#{hook}"' || + fail "set-hook -g command-error failed" +if $TMUX rename-window -t nosuchsession:0 x 2>/dev/null; then + fail "rename-window to missing session succeeded" +fi +wait_for @error 'command-error' +$TMUX set-hook -gu command-error || fail "set-hook -gu command-error failed" + +# Commands run from a hook do not fire their own after hooks. +$TMUX set -g @copy 0 || fail "set @copy failed" +$TMUX set-hook -g after-copy-mode 'set -gF @copy "#{hook}"' || + fail "set-hook -g after-copy-mode failed" +$TMUX copy-mode -t one:0 || fail "copy-mode failed" +wait_for @copy 'after-copy-mode' +$TMUX send-keys -t one:0 -X cancel || fail "cancel failed" +$TMUX set -g @copy 0 || fail "reset @copy failed" +$TMUX set -g @ran 0 || fail "set @ran failed" +$TMUX set-hook -g after-rename-window 'copy-mode -t one:0' || + fail "set-hook -g after-rename-window nested failed" +$TMUX set-hook -ga after-rename-window 'set -g @ran 1' || + fail "set-hook -ga after-rename-window nested failed" +$TMUX rename-window -t one:0 fifth || fail "rename-window fifth failed" +wait_for @ran 1 +mode=$($TMUX display -pt one:0 '#{pane_in_mode}') || + fail "display pane_in_mode failed" +[ "$mode" = 1 ] || fail "hook did not enter copy mode" +assert_unchanged @copy 0 + +exit 0 diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh new file mode 100644 index 000000000..66eac8735 --- /dev/null +++ b/regress/hooks-notify.sh @@ -0,0 +1,199 @@ +#!/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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest-hooks-notify-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +$TMUX new -d -s main || fail "new-session main failed" + +# session-created, session-renamed, session-closed. +$TMUX set -g @s 0 || fail "set @s failed" +$TMUX set-hook -g session-created \ + 'set -gF @s "#{hook}:#{hook_session_name}"' || + fail "set-hook session-created failed" +$TMUX new -d -s tmp || fail "new-session tmp failed" +wait_for @s 'session-created:tmp' +$TMUX set-hook -gu session-created || fail "unset session-created failed" + +$TMUX set-hook -g session-renamed \ + 'set -gF @s "#{hook}:#{hook_session_name}"' || + fail "set-hook session-renamed failed" +$TMUX rename-session -t tmp tmp2 || fail "rename-session failed" +wait_for @s 'session-renamed:tmp2' +$TMUX set-hook -gu session-renamed || fail "unset session-renamed failed" + +$TMUX set-hook -g session-closed \ + 'set -gF @s "#{hook}:#{hook_session_name}"' || + fail "set-hook session-closed failed" +$TMUX kill-session -t tmp2 || fail "kill-session failed" +wait_for @s 'session-closed:tmp2' +$TMUX set-hook -gu session-closed || fail "unset session-closed failed" + +# window-linked and window-unlinked from new-window and kill-window. +$TMUX set -g @w 0 || fail "set @w failed" +$TMUX set-hook -g window-linked \ + 'set -gF @w "#{hook}:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook window-linked failed" +$TMUX set-hook -g window-unlinked \ + 'set -gF @w "#{hook}:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook window-unlinked failed" +$TMUX neww -d -t main: -n mywin || fail "new-window failed" +wait_for @w 'window-linked:main:mywin' + +# window-linked and window-unlinked from link-window and unlink-window. +$TMUX new -d -s other || fail "new-session other failed" +$TMUX set -g @w 0 || fail "reset @w failed" +$TMUX link-window -s main:mywin -t other:5 || fail "link-window failed" +wait_for @w 'window-linked:other:mywin' +$TMUX set -g @w 0 || fail "reset @w failed" +$TMUX unlink-window -t other:5 || fail "unlink-window failed" +wait_for @w 'window-unlinked:other:mywin' +$TMUX set -g @w 0 || fail "reset @w failed" +$TMUX kill-window -t main:mywin || fail "kill-window failed" +wait_for @w 'window-unlinked:main:mywin' +$TMUX set-hook -gu window-linked || fail "unset window-linked failed" +$TMUX set-hook -gu window-unlinked || fail "unset window-unlinked failed" + +# window-renamed sees the window id. +window=$($TMUX display -pt main:0 '#{window_id}') || + fail "display window_id failed" +$TMUX set -g @r 0 || fail "set @r failed" +$TMUX set-hook -g window-renamed \ + 'set -gF @r "#{hook}:#{hook_window}:#{hook_window_name}"' || + fail "set-hook window-renamed failed" +$TMUX rename-window -t main:0 renamed || fail "rename-window failed" +wait_for @r "window-renamed:$window:renamed" +$TMUX set-hook -gu window-renamed || fail "unset window-renamed failed" + +# window-layout-changed from split-window. +$TMUX set -g @l 0 || fail "set @l failed" +$TMUX set-hook -g window-layout-changed \ + 'set -gF @l "#{hook}:#{hook_window}"' || + fail "set-hook window-layout-changed failed" +$TMUX splitw -d -t main:0 || fail "split-window failed" +wait_for @l "window-layout-changed:$window" +$TMUX set-hook -gu window-layout-changed || + fail "unset window-layout-changed failed" + +# window-pane-changed from select-pane. +$TMUX set -g @p 0 || fail "set @p failed" +$TMUX set-hook -g window-pane-changed \ + 'set -gF @p "#{hook}:#{hook_window}"' || + fail "set-hook window-pane-changed failed" +$TMUX selectp -t main:0.1 || fail "select-pane failed" +wait_for @p "window-pane-changed:$window" +$TMUX set-hook -gu window-pane-changed || + fail "unset window-pane-changed failed" + +# session-window-changed from select-window. +$TMUX neww -d -t main: -n w2 || fail "new-window w2 failed" +$TMUX set -g @c 0 || fail "set @c failed" +$TMUX set-hook -g session-window-changed \ + 'set -gF @c "#{hook}:#{hook_session_name}"' || + fail "set-hook session-window-changed failed" +$TMUX selectw -t main:w2 || fail "select-window failed" +wait_for @c 'session-window-changed:main' +$TMUX set-hook -gu session-window-changed || + fail "unset session-window-changed failed" + +# pane-mode-changed from entering and leaving copy mode. +$TMUX set -g @m 0 || fail "set @m failed" +pane=$($TMUX display -pt main:0.0 '#{pane_id}') || + fail "display pane_id failed" +$TMUX set-hook -g pane-mode-changed \ + 'set -gF @m "#{hook}:#{hook_pane}:#{pane_in_mode}"' || + fail "set-hook pane-mode-changed failed" +$TMUX copy-mode -t main:0.0 || fail "copy-mode failed" +wait_for @m "pane-mode-changed:$pane:1" +$TMUX send-keys -t main:0.0 -X cancel || fail "cancel failed" +wait_for @m "pane-mode-changed:$pane:0" +$TMUX set-hook -gu pane-mode-changed || fail "unset pane-mode-changed failed" + +# pane-exited when a pane's command exits with remain-on-exit off. +$TMUX set -g @x 0 || fail "set @x failed" +$TMUX set-hook -g pane-exited 'set -gF @x "#{hook}:#{hook_pane}"' || + fail "set-hook pane-exited failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') || + fail "split-window true failed" +wait_for @x "pane-exited:$pane" +$TMUX set-hook -gu pane-exited || fail "unset pane-exited failed" + +# pane-died when a pane's command exits with remain-on-exit on. +$TMUX set -g remain-on-exit on || fail "set remain-on-exit failed" +$TMUX set -g @d 0 || fail "set @d failed" +$TMUX set-hook -g pane-died 'set -gF @d "#{hook}:#{hook_pane}"' || + fail "set-hook pane-died failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') || + fail "split-window remain-on-exit failed" +wait_for @d "pane-died:$pane" +$TMUX set-hook -gu pane-died || fail "unset pane-died failed" +$TMUX killp -t "$pane" || fail "kill-pane failed" +$TMUX set -g remain-on-exit off || fail "reset remain-on-exit failed" + +# pane-title-changed when a pane sets its title. +$TMUX set -g @t 0 || fail "set @t failed" +$TMUX set-hook -g pane-title-changed \ + 'set -gF @t "#{hook}:#{hook_pane}:#{pane_title}"' || + fail "set-hook pane-title-changed failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' \ + 'printf "\033]2;mytitle\007"; sleep 30') || + fail "split-window title failed" +wait_for @t "pane-title-changed:$pane:mytitle" +$TMUX set-hook -gu pane-title-changed || + fail "unset pane-title-changed failed" +$TMUX killp -t "$pane" || fail "kill-pane title failed" + +# client-attached and client-detached using a control client. +$TMUX set -g @a 0 || fail "set @a failed" +$TMUX set-hook -g client-attached 'set -gF @a "#{hook}"' || + fail "set-hook client-attached failed" +$TMUX set-hook -g client-detached 'set -gF @a "#{hook}"' || + fail "set-hook client-detached failed" +mkfifo "$OUT/fifo" || fail "mkfifo failed" +$TMUX -C attach -t main <"$OUT/fifo" >"$OUT/control.out" 2>&1 & +exec 3>"$OUT/fifo" +wait_for @a 'client-attached' +exec 3>&- +wait_for @a 'client-detached' +$TMUX set-hook -gu client-attached || fail "unset client-attached failed" +$TMUX set-hook -gu client-detached || fail "unset client-detached failed" + +exit 0 diff --git a/regress/hooks.sh b/regress/hooks.sh new file mode 100644 index 000000000..b64943d84 --- /dev/null +++ b/regress/hooks.sh @@ -0,0 +1,155 @@ +#!/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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest-hooks-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +$TMUX new -d -s one || fail "new-session one failed" + +# A global hook fires and sees the hook formats. +$TMUX set -g @created 0 || fail "set @created failed" +$TMUX set-hook -g session-created \ + 'set -gF @created "#{hook}:#{hook_session_name}"' || + fail "set-hook -g session-created failed" +$TMUX new -d -s two || fail "new-session two failed" +wait_for @created 'session-created:two' + +# Hooks are arrays: an appended command runs after the first. +$TMUX set -g @second 0 || fail "set @second failed" +$TMUX set-hook -ga session-created \ + 'set -gF @second "#{@created}+second"' || + fail "set-hook -ga session-created failed" +$TMUX new -d -s three || fail "new-session three failed" +wait_for @created 'session-created:three' +wait_for @second 'session-created:three+second' + +# show-hooks lists both commands. +shown=$($TMUX show-hooks -g session-created) || + fail "show-hooks -g failed" +[ "$(echo "$shown" | wc -l)" -eq 2 ] || + fail "unexpected show-hooks output: $shown" +echo "$shown" | grep -q '^session-created\[0\]' || + fail "missing first array item: $shown" +echo "$shown" | grep -q '^session-created\[1\]' || + fail "missing second array item: $shown" + +# Unsetting removes the whole hook. +$TMUX set-hook -gu session-created || fail "set-hook -gu failed" +shown=$($TMUX show-hooks -g session-created) || + fail "show-hooks -g after unset failed" +if echo "$shown" | grep -q '\['; then + fail "show-hooks showed removed hook: $shown" +fi +$TMUX set -g @created 0 || fail "reset @created failed" +$TMUX new -d -s four || fail "new-session four failed" +assert_unchanged @created 0 + +# An unknown hook name is rejected. +if $TMUX set-hook -g no-such-hook 'display x' 2>/dev/null; then + fail "unknown hook name was accepted" +fi + +# A session hook only fires for events in that session. +$TMUX set -g @renamed 0 || fail "set @renamed failed" +$TMUX set-hook -t one window-renamed \ + 'set -gF @renamed "#{hook}:#{hook_window_name}"' || + fail "set-hook -t one window-renamed failed" +$TMUX rename-window -t two:0 twoname || fail "rename-window two failed" +assert_unchanged @renamed 0 +$TMUX rename-window -t one:0 onename || fail "rename-window one failed" +wait_for @renamed 'window-renamed:onename' +$TMUX set-hook -u -t one window-renamed || fail "set-hook -u failed" + +# A pane hook only fires for events in that pane. +pane2=$($TMUX splitw -d -t one:0 -P -F '#{pane_id}') || + fail "split-window failed" +$TMUX set -g @mode 0 || fail "set @mode failed" +$TMUX set-hook -p -t "$pane2" pane-mode-changed \ + 'set -gF @mode "#{hook}:#{hook_pane}"' || + fail "set-hook -p pane-mode-changed failed" +$TMUX copy-mode -t one:0.0 || fail "copy-mode pane 0 failed" +assert_unchanged @mode 0 +$TMUX copy-mode -t "$pane2" || fail "copy-mode pane 1 failed" +wait_for @mode "pane-mode-changed:$pane2" +$TMUX send-keys -t "$pane2" -X cancel || fail "cancel failed" +$TMUX set-hook -pu -t "$pane2" pane-mode-changed || + fail "set-hook -pu failed" + +# A window hook only fires for events in that window. +$TMUX set -g @wmode 0 || fail "set @wmode failed" +$TMUX set-hook -w -t two:0 pane-mode-changed \ + 'set -gF @wmode "#{hook}:#{hook_window_name}"' || + fail "set-hook -w pane-mode-changed failed" +$TMUX copy-mode -t three:0.0 || fail "copy-mode three failed" +assert_unchanged @wmode 0 +$TMUX copy-mode -t two:0.0 || fail "copy-mode two failed" +wait_for @wmode 'pane-mode-changed:twoname' +$TMUX set-hook -wu -t two:0 pane-mode-changed || + fail "set-hook -wu failed" + +# Commands run from a hook do not fire hooks again. +$TMUX set -g @renames '' || fail "set @renames failed" +$TMUX set-hook -g window-renamed \ + 'set -gF @renames "#{@renames}x" ; rename-window -t three:0 inner' || + fail "set-hook -g window-renamed failed" +$TMUX rename-window -t three:0 outer || fail "rename-window outer failed" +wait_for @renames 'x' +assert_unchanged @renames 'x' +name=$($TMUX display -pt three:0 '#{window_name}') || + fail "display window_name failed" +[ "$name" = inner ] || fail "expected window name inner but got $name" + +exit 0