mirror of
https://github.com/robbyrussell/oh-my-zsh.git
synced 2026-09-21 18:46:06 +02:00
351 lines
13 KiB
Bash
351 lines
13 KiB
Bash
#!/usr/bin/env zsh
|
|
# Run with: zsh -df tools/theme-browser/tests/browser_test.zsh
|
|
# Optionally pass one group, e.g. `slow-interrupt`, to run it alone.
|
|
# Each browser runs under a real controlling PTY, with no user startup files.
|
|
emulate -R zsh
|
|
|
|
if [[ $1 == --child ]]; then
|
|
if [[ $2 == completion ]]; then
|
|
function compdef { print -r -- "REGISTERED:${(j.:.)@}"; }
|
|
fi
|
|
source "$ZSH/lib/cli.zsh"
|
|
# Match normal OMZ sessions without sourcing any user startup/configuration.
|
|
setopt promptsubst
|
|
print -r -- "LAZY:$+functions[_omz_theme_browser]:$+functions[_omz_theme_preview]"
|
|
if [[ $2 == completion ]]; then
|
|
# Capture the real completion function's candidates without loading the
|
|
# user's completion setup or replacing any CLI/backend functions.
|
|
function _describe { print -rl -- "DESCRIBE:$1" "${(@P)2}"; }
|
|
CURRENT=3 words=(omz theme '')
|
|
print -r -- SUBCOMMANDS
|
|
_omz
|
|
print -r -- END_SUBCOMMANDS
|
|
for action in browse preview set use; do
|
|
CURRENT=4 words=(omz theme "$action" '')
|
|
print -r -- "CANDIDATES:$action"
|
|
_omz
|
|
print -r -- "END:$action"
|
|
done
|
|
print -r -- "STILL_LAZY:$+functions[_omz_theme_browser]:$+functions[_omz_theme_preview]"
|
|
print -r -- COMPLETIONS_FINISHED
|
|
exit
|
|
fi
|
|
if [[ $2 == reject* ]]; then
|
|
case $2 in
|
|
reject-stdin) omz theme browse </dev/null ;;
|
|
reject-stdout) omz theme browse >/dev/null ;;
|
|
*) omz theme browse ;;
|
|
esac
|
|
result=$?
|
|
print -r -- "REJECT:$result:$+functions[_omz_theme_browser]:$+functions[_omz_theme_preview]"
|
|
exit
|
|
fi
|
|
function _omz::theme::use { calls+=("use:$1"); ZSH_THEME=$1; }
|
|
function _omz::theme::set { calls+=("set:$1"); ZSH_THEME=$1; }
|
|
function browser_test_hook { : parent-hook; }
|
|
function precmd { : parent-precmd; }
|
|
typeset -a calls=() precmd_functions=(browser_test_hook) chpwd_functions=(browser_test_hook)
|
|
ZSH_THEME=parent-theme PROMPT=parent-prompt RPROMPT=parent-rprompt
|
|
typeset original_hook=$functions[browser_test_hook] original_precmd=$functions[precmd]
|
|
typeset original_options=$(setopt) original_pwd=$PWD
|
|
stty rows 24 cols 100
|
|
# PENDIN is a transient kernel input-reprocessing flag, not a user mode.
|
|
typeset original_tty=$(stty -a)
|
|
original_tty=${${original_tty//-pendin/}//pendin/}
|
|
tty > "$TEST_SCRATCH/tty"
|
|
print -r -- READY
|
|
# An interactive script otherwise aborts on SIGINT instead of returning to
|
|
# the next prompt as an interactive command loop would.
|
|
trap ':' INT
|
|
omz theme browse "$2"
|
|
result=$?
|
|
typeset restored_tty=$(stty -a)
|
|
restored_tty=${${restored_tty//-pendin/}//pendin/}
|
|
if [[ $restored_tty == "$original_tty" ]]; then
|
|
print -r -- TTY_RESTORED
|
|
else
|
|
print -r -- "TTY_MISMATCH:before=$original_tty after=$restored_tty"
|
|
fi
|
|
if [[ $PROMPT == parent-prompt && $RPROMPT == parent-rprompt &&
|
|
$functions[browser_test_hook] == "$original_hook" &&
|
|
$functions[precmd] == "$original_precmd" &&
|
|
${(j:,:)precmd_functions} == browser_test_hook &&
|
|
${(j:,:)chpwd_functions} == browser_test_hook &&
|
|
-o promptsubst && $(setopt) == "$original_options" && $PWD == "$original_pwd" &&
|
|
${BROWSER_TEST_LEAK-unset} == unset ]]; then
|
|
print -r -- PARENT_UNCHANGED
|
|
fi
|
|
print -r -- "RESULT:$result:${(j:,:)calls}:$ZSH_THEME"
|
|
print -r -- FINISHED
|
|
exit 0
|
|
fi
|
|
|
|
setopt err_exit pipe_fail
|
|
[[ -n $BROWSER_TEST_TRACE ]] && setopt xtrace
|
|
zmodload zsh/zpty
|
|
zmodload zsh/datetime
|
|
zmodload zsh/zselect
|
|
typeset -r repo=${0:A:h:h:h:h} self=${0:A}
|
|
typeset scratch=$(mktemp -d "${TMPDIR:-/tmp}/omz-browser-test.XXXXXXXX")
|
|
scratch=${scratch:A}
|
|
trap 'zpty -d 2>/dev/null; command rm -rf -- "$scratch"' EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
mkdir -p "$scratch/home" "$scratch/custom/themes/nested" "$scratch/tmp"
|
|
export ZSH=$repo ZSH_CUSTOM=$scratch/custom HOME=$scratch/home ZDOTDIR=$scratch/home
|
|
export TMPDIR=$scratch/tmp TEST_SCRATCH=$scratch TERM=xterm-256color
|
|
unset ZSH_THEME
|
|
# Even a mistakenly enabled startup-file load must never reach real dotfiles.
|
|
print -r -- 'print STARTUP_RAN; exit 99' > "$HOME/.zshenv"
|
|
print -r -- 'PROMPT="nested fixture"' > "$ZSH_CUSTOM/themes/nested/browser-fixture.zsh-theme"
|
|
for number in {01..10}; do
|
|
print -r -- 'typeset -g BROWSER_TEST_LEAK=bad
|
|
ZSH_THEME=fixture-theme
|
|
precmd_functions=() chpwd_functions=()
|
|
function precmd { PROMPT="%F{red}FIXTURE_PREVIEW%f" }
|
|
RPROMPT="fixture-right"' > "$ZSH_CUSTOM/zz-omz-test-$number.zsh-theme"
|
|
done
|
|
print -r -- 'sleep 30 &
|
|
print -r -- $! > "$TEST_SCRATCH/slow-pid"
|
|
wait
|
|
PROMPT="SLOW_FINISHED"' > "$ZSH_CUSTOM/zz-omz-test-slow.zsh-theme"
|
|
typeset filter_probe='$(print HIT > $TEST_SCRATCH/filter-executed)'
|
|
typeset preview_probe='$(print HIT > $TEST_SCRATCH/preview-executed)'
|
|
# Emit literal preview output, rather than asking the worker to evaluate a
|
|
# prompt expression. Only a second evaluation by the browser can execute it.
|
|
print -rl -- "print -r -- ${(q)preview_probe}" 'PROMPT="literal-preview-ready"' > "$ZSH_CUSTOM/literal-browser.zsh-theme"
|
|
|
|
typeset buffer='' transcript='' chunk='' label=''
|
|
typeset -F browser_finished_at=0
|
|
function cleanup_pty {
|
|
local record group
|
|
for record in "${(@f)$(zpty)}"; do
|
|
[[ $record == \(<->\)\ browser:* ]] || continue
|
|
group=${${record#\(}%%\)*}
|
|
kill -HUP -- -$group 2>/dev/null || true
|
|
zselect -t 20 || true
|
|
# A renderer regression can loop without checking its cancellation flag.
|
|
kill -KILL -- -$group 2>/dev/null || true
|
|
done
|
|
zpty -d 2>/dev/null || true
|
|
return 0
|
|
}
|
|
function fail {
|
|
print -u2 -r -- "FAIL: $label: $1"
|
|
print -u2 -r -- "PTY transcript, last 4000 characters (escaped): ${(V)transcript[-4000,-1]}"
|
|
exit 1
|
|
}
|
|
function check {
|
|
"$@" || fail "$1 assertion failed"
|
|
}
|
|
function await {
|
|
local wanted=$1
|
|
local -F deadline=$(( EPOCHREALTIME + ${2:-6} ))
|
|
[[ $buffer == *"$wanted"* ]] && return 0
|
|
while (( EPOCHREALTIME < deadline )); do
|
|
if zpty -r browser chunk; then
|
|
buffer+=$chunk transcript+=$chunk
|
|
[[ $buffer == *"$wanted"* ]] && return 0
|
|
else
|
|
zselect -t 1 || true
|
|
fi
|
|
done
|
|
fail "timed out waiting for ${(V)wanted}"
|
|
}
|
|
function send {
|
|
buffer=''
|
|
zpty -w -n browser "$1"
|
|
}
|
|
function start {
|
|
label=$1 buffer='' transcript=''
|
|
local -a launch=("$commands[zsh]" -dfi "$self" --child "$2")
|
|
zpty -b browser exec "${(@q)launch}"
|
|
await 'LAZY:0:0'
|
|
await "$3"
|
|
}
|
|
function finish {
|
|
await FINISHED
|
|
# zpty -d can spend a second reaping an already-completed child. It is
|
|
# harness teardown, not browser cancellation/terminal-restoration latency.
|
|
browser_finished_at=$EPOCHREALTIME
|
|
check test "${transcript#*TTY_RESTORED}" != "$transcript"
|
|
check test "${transcript#*PARENT_UNCHANGED}" != "$transcript"
|
|
check test "${transcript#*RESULT:$1}" != "$transcript"
|
|
check test "${transcript#*$'\e[?1049l'}" != "$transcript"
|
|
check test "${transcript#*$'\e[?25h'}" != "$transcript"
|
|
check test "${transcript#*STARTUP_RAN}" = "$transcript"
|
|
zpty -d browser
|
|
[[ -z $BROWSER_TEST_TIMING ]] || print -r -- "TIMING: PTY deletion $(( EPOCHREALTIME - browser_finished_at ))s"
|
|
}
|
|
|
|
function test_completion {
|
|
start 'completion registration, commands, theme candidates, lazy loading' completion COMPLETIONS_FINISHED
|
|
check test "${transcript#*REGISTERED:_omz:omz}" != "$transcript"
|
|
local section=${${transcript#*SUBCOMMANDS}%END_SUBCOMMANDS*} action
|
|
check test "${section#*browse:Browse theme previews}" != "$section"
|
|
check test "${section#*preview:Preview a theme without applying it}" != "$section"
|
|
for action in browse preview set use; do
|
|
section=${${transcript#*CANDIDATES:$action}%END:$action*}
|
|
check test "${section#*DESCRIBE:theme}" != "$section"
|
|
for name in robbyrussell zz-omz-test-01 nested/browser-fixture; do
|
|
check test "${section#*$name}" != "$section"
|
|
done
|
|
done
|
|
check test "${transcript#*STILL_LAZY:0:0}" != "$transcript"
|
|
check test "${transcript#*STARTUP_RAN}" = "$transcript"
|
|
zpty -d browser
|
|
}
|
|
|
|
function test_rejection {
|
|
# Noninteractive rejection even with both descriptors attached to a PTY.
|
|
label=noninteractive
|
|
typeset -a launch=("$commands[zsh]" -df "$self" --child reject)
|
|
zpty -b browser exec "${(@q)launch}"
|
|
await 'REJECT:1:0:0'
|
|
check test "${transcript#*requires an interactive terminal}" != "$transcript"
|
|
zpty -d browser
|
|
for mode in stdin stdout; do
|
|
start "redirected $mode" "reject-$mode" 'REJECT:1:0:0'
|
|
check test "${transcript#*requires an interactive terminal}" != "$transcript"
|
|
zpty -d browser
|
|
done
|
|
label='noninteractive and redirected-I/O rejection'
|
|
}
|
|
|
|
function test_navigation {
|
|
start 'filter, navigation, actions/back, Esc isolation' zz-omz-test- '> zz-omz-test-01'
|
|
await FIXTURE_PREVIEW
|
|
send $'\e[B'; await '> zz-omz-test-02'
|
|
send $'\e[A'; await '> zz-omz-test-01'
|
|
send $'\x0e'; await '> zz-omz-test-02'
|
|
send $'\x10'; await '> zz-omz-test-01'
|
|
send $'\e[6~'; await '> zz-omz-test-05'
|
|
send $'\e[5~'; await '> zz-omz-test-01'
|
|
send '09'; await '> zz-omz-test-09'
|
|
send $'\x7f'; await '(9 themes)'
|
|
send $'\x15'; await 'Filter: ('
|
|
send 'ZZ-OMZ-TEST-03'; await '> zz-omz-test-03'
|
|
send $'\r'; await '[u] Use in session'
|
|
send b; await 'Enter: actions'
|
|
send $'\x15no-such-omz-fixture'; await 'No matching themes.'
|
|
send $'\r'; await 'No matching themes.'
|
|
send $'\e'
|
|
finish '0::parent-theme'
|
|
}
|
|
|
|
function test_use {
|
|
start 'explicit use dispatch' zz-omz-test-01 '> zz-omz-test-01'
|
|
send $'\r'; await '[u] Use in session'
|
|
send u
|
|
finish '0:use:zz-omz-test-01:zz-omz-test-01'
|
|
}
|
|
|
|
function test_promptsubst {
|
|
start 'literal filter/preview substitutions with parent promptsubst enabled' literal-browser '> literal-browser'
|
|
await "$preview_probe"
|
|
await literal-preview-ready
|
|
check test ! -e "$scratch/preview-executed"
|
|
send $'\x15'"$filter_probe"
|
|
await "Filter: $filter_probe"
|
|
await 'No matching themes.'
|
|
check test ! -e "$scratch/filter-executed"
|
|
send $'\x15literal-browser'
|
|
await "$preview_probe"
|
|
await literal-preview-ready
|
|
send $'\e'
|
|
finish '0::parent-theme'
|
|
check test ! -e "$scratch/filter-executed"
|
|
check test ! -e "$scratch/preview-executed"
|
|
}
|
|
|
|
function test_save {
|
|
start 'explicit save dispatch (stubbed)' zz-omz-test-02 '> zz-omz-test-02'
|
|
send $'\r'; await '[u] Use in session'
|
|
send s
|
|
finish '0:set:zz-omz-test-02:zz-omz-test-02'
|
|
}
|
|
|
|
function test_interrupt {
|
|
start 'Ctrl-C restores terminal' zz-omz-test-01 '> zz-omz-test-01'
|
|
send $'\x03'
|
|
finish '130::parent-theme'
|
|
}
|
|
|
|
function test_resize {
|
|
start 'resize small and back' zz-omz-test-01 '> zz-omz-test-01'
|
|
typeset terminal=$(<"$scratch/tty")
|
|
terminal=${terminal//$'\r'/}
|
|
buffer=''
|
|
stty rows 10 cols 30 < "$terminal"
|
|
await '40 columns x 16 rows.'
|
|
buffer=''
|
|
stty rows 24 cols 100 < "$terminal"
|
|
await '> zz-omz-test-01'
|
|
send $'\e'
|
|
finish '0::parent-theme'
|
|
}
|
|
|
|
function test_slow {
|
|
local cancellation=$1
|
|
command rm -f "$scratch/slow-pid"
|
|
start "slow preview cancellation: $cancellation" zz-omz-test-slow '> zz-omz-test-slow'
|
|
for attempt in {1..150}; do
|
|
[[ -s "$scratch/slow-pid" ]] && break
|
|
zselect -t 1 || true
|
|
done
|
|
check test -s "$scratch/slow-pid"
|
|
typeset -F started=$EPOCHREALTIME
|
|
case $cancellation in
|
|
navigation)
|
|
send $'\x15zz-omz-test-01'
|
|
await FIXTURE_PREVIEW 2
|
|
[[ -z $BROWSER_TEST_TIMING ]] || print -r -- "TIMING: navigation preview $(( EPOCHREALTIME - started ))s"
|
|
(( EPOCHREALTIME - started < 2 )) || fail 'replacement preview exceeded 2 seconds'
|
|
send $'\e'
|
|
finish '0::parent-theme'
|
|
;;
|
|
escape) send $'\e'; finish '0::parent-theme' ;;
|
|
interrupt) send $'\x03'; finish '130::parent-theme' ;;
|
|
esac
|
|
[[ -z $BROWSER_TEST_TIMING ]] || print -r -- "TIMING: browser completed $(( browser_finished_at - started ))s; including teardown $(( EPOCHREALTIME - started ))s"
|
|
(( browser_finished_at - started < 2 )) || fail "browser completion exceeded 2 seconds: $(( browser_finished_at - started ))s"
|
|
typeset child=$(<"$scratch/slow-pid") child_state
|
|
for attempt in {1..100}; do
|
|
child_state=$(command ps -o stat= -p "$child" 2>/dev/null || true)
|
|
[[ -z "${child_state//[ ZN+]/}" ]] && break
|
|
zselect -t 1 || true
|
|
done
|
|
[[ -z $BROWSER_TEST_TIMING ]] || print -r -- "TIMING: descendant $child state=${child_state:-absent}, cleanup polls=$attempt"
|
|
[[ -z "${child_state//[ ZN+]/}" ]] || fail "preview descendant $child still running: $child_state"
|
|
}
|
|
|
|
# Keep independent regressions running after a failed UI session.
|
|
unsetopt err_exit
|
|
typeset -i failures=0
|
|
for scenario in rejection completion navigation promptsubst use save interrupt resize slow-navigation slow-escape slow-interrupt; do
|
|
[[ -z $1 || $scenario == "$1" ]] || continue
|
|
(
|
|
setopt err_exit
|
|
trap 'cleanup_pty' EXIT
|
|
if [[ $scenario == slow-* ]]; then
|
|
test_slow "${scenario#slow-}"
|
|
else
|
|
test_$scenario
|
|
fi
|
|
print -r -- "PASS: $label"
|
|
)
|
|
(( $? == 0 )) || (( failures++ ))
|
|
done
|
|
typeset -a leftovers
|
|
for attempt in {1..100}; do
|
|
leftovers=("$TMPDIR"/omz-preview.*(N))
|
|
(( $#leftovers == 0 )) && break
|
|
zselect -t 1 || true
|
|
done
|
|
if (( $#leftovers )); then
|
|
print -u2 -r -- "FAIL: $#leftovers preview temporary directories remain"
|
|
(( failures++ ))
|
|
fi
|
|
print -r -- "Theme browser regression groups failed: $failures"
|
|
(( failures == 0 ))
|