In windows opened by netssh multi, prefix S toggles broadcast for all panes and prefix e excludes the current pane (a pane-level synchronize-panes off, shown as [SOLO] in the border) or includes it again. netssh sync --pane does the same from a shell. S and e are unbound in a default tmux (E would be "spread evenly"). netssh writes the bindings to a small tmux config and sources it, binds a key only if the user's tmux.conf does not use it (netssh's own earlier binding is refreshed), and the bindings act only in windows marked @netssh. multi.sync_key and multi.solo_key change the keys or turn them off (none); also in the configurator. zellij syncs whole tabs only, so excluding a pane is tmux-only. testenv: the render test presses the keys through the xterm client (send-keys -K) and checks [SOLO], that broadcast skips the excluded pane, that S toggles, and that other windows are unaffected. The client gets GNU grep: BusyBox grep -oF counted only one match per line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
155 lines
4.9 KiB
Bash
155 lines
4.9 KiB
Bash
# Helpers for the netssh test environment. Sourced by run.sh, the tests, and
|
|
# shell.sh inside the client container.
|
|
|
|
# shellcheck disable=SC1091
|
|
. /state/env
|
|
RESULTS=${RESULTS:-/tmp/netssh-results}
|
|
|
|
# new_home gives each test file its own netssh config, cache, notes, and rc files.
|
|
new_home() {
|
|
HOME=$(mktemp -d /tmp/home.XXXXXX)
|
|
export HOME XDG_CONFIG_HOME="$HOME/.config" XDG_CACHE_HOME="$HOME/.cache" SHELL=/bin/bash
|
|
mkdir -p "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME"
|
|
unset TMUX ZELLIJ NETSSH_NETBOX_URL NETSSH_NETBOX_TOKEN
|
|
cd "$HOME" || exit 1
|
|
}
|
|
|
|
# base_config prints both NetBox instances followed by stdin.
|
|
base_config() { cat /state/netssh.yaml -; }
|
|
|
|
# config writes the netssh config: both NetBox instances plus stdin.
|
|
config() { base_config >"$XDG_CONFIG_HOME/netssh.yaml"; }
|
|
|
|
# config_only writes stdin as the whole config (no NetBox).
|
|
config_only() { cat >"$XDG_CONFIG_HOME/netssh.yaml"; }
|
|
|
|
ok() {
|
|
echo "ok $CURRENT: $1" >>"$RESULTS"
|
|
printf ' \033[32mok\033[0m %s\n' "$1"
|
|
}
|
|
bad() {
|
|
echo "FAIL $CURRENT: $1" >>"$RESULTS"
|
|
printf ' \033[31mFAIL\033[0m %s\n' "$1"
|
|
printf '%s\n' "$2" | sed 's/^/ /'
|
|
}
|
|
|
|
# check NAME CMD... — CMD must succeed.
|
|
check() {
|
|
local name=$1 out
|
|
shift
|
|
if out=$("$@" 2>&1); then ok "$name"; else bad "$name" "exit $?: $out"; fi
|
|
}
|
|
|
|
# expect_out NAME WANT CMD... — CMD must succeed and print WANT (substring).
|
|
expect_out() {
|
|
local name=$1 want=$2 out rc
|
|
shift 2
|
|
out=$("$@" 2>&1) && rc=0 || rc=$?
|
|
if [ "$rc" = 0 ] && [[ $out == *"$want"* ]]; then ok "$name"; else bad "$name" "want: $want
|
|
got (exit $rc): $out"; fi
|
|
}
|
|
|
|
# expect_eq NAME WANT CMD... — CMD must succeed and print exactly WANT.
|
|
expect_eq() {
|
|
local name=$1 want=$2 out rc
|
|
shift 2
|
|
out=$("$@" 2>&1) && rc=0 || rc=$?
|
|
if [ "$rc" = 0 ] && [ "$out" = "$want" ]; then ok "$name"; else bad "$name" "want: $want
|
|
got (exit $rc): $out"; fi
|
|
}
|
|
|
|
# expect_fail NAME WANT CMD... — CMD must fail and print WANT (substring).
|
|
expect_fail() {
|
|
local name=$1 want=$2 out rc
|
|
shift 2
|
|
out=$("$@" 2>&1) && rc=0 || rc=$?
|
|
if [ "$rc" != 0 ] && [[ $out == *"$want"* ]]; then ok "$name"; else bad "$name" "want failure with: $want
|
|
got (exit $rc): $out"; fi
|
|
}
|
|
|
|
# wait_for TIMEOUT CMD... — retry CMD every 0.2 s until it succeeds.
|
|
wait_for() {
|
|
local deadline=$((SECONDS + $1))
|
|
shift
|
|
until "$@" >/dev/null 2>&1; do
|
|
[ $SECONDS -ge $deadline ] && return 1
|
|
sleep 0.2
|
|
done
|
|
}
|
|
|
|
# --- rendering in a real terminal emulator (tests/16-render.sh) ---
|
|
# Xvfb provides a screen, xterm renders the terminal, `import` takes a
|
|
# screenshot, and tesseract reads it back, so checks see what a user sees.
|
|
|
|
start_screen() {
|
|
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp >/dev/null 2>&1 &
|
|
XVFB_PID=$!
|
|
export DISPLAY=:99
|
|
wait_for 10 import -window root -crop 1x1+0+0 null:
|
|
}
|
|
|
|
stop_screen() {
|
|
pkill -u "$(id -u)" xterm 2>/dev/null
|
|
kill "$XVFB_PID" 2>/dev/null
|
|
wait "$XVFB_PID" 2>/dev/null
|
|
}
|
|
|
|
# term CMD... — run CMD in a full-screen xterm (UTF-8, DejaVu Sans Mono).
|
|
term() {
|
|
pkill -u "$(id -u)" xterm 2>/dev/null
|
|
xterm -u8 -geometry 190x52+0+0 -fa 'DejaVu Sans Mono' -fs 11 -bg '#1e1e1e' -fg '#d4d4d4' \
|
|
-T netssh -e "$@" >/dev/null 2>&1 &
|
|
sleep 1
|
|
}
|
|
|
|
# shot NAME — screenshot to /artifacts/NAME.png.
|
|
shot() { import -window root "/artifacts/$1.png"; }
|
|
|
|
# OCR preprocessing variants: no single one reads everything (dim hint lines,
|
|
# red/green borders): plain luminance, stretched dark range for dim gray, and
|
|
# the brightest color channel for colored text.
|
|
OCR_VARIANTS=("-colorspace gray" "-colorspace gray -level 10%,40%"
|
|
"-channel RGB -separate -evaluate-sequence max -level 10%,60%")
|
|
|
|
# ocr_variant NAME I — OCR of screenshot NAME with preprocessing variant I.
|
|
ocr_variant() {
|
|
# shellcheck disable=SC2086
|
|
magick "/artifacts/$1.png" ${OCR_VARIANTS[$2]} -negate -resize 300% png:- |
|
|
tesseract stdin stdout --psm 6 2>/dev/null
|
|
}
|
|
|
|
# screen_text NAME — the text of all variants combined.
|
|
screen_text() {
|
|
local i
|
|
for i in "${!OCR_VARIANTS[@]}"; do ocr_variant "$1" "$i"; done
|
|
}
|
|
|
|
# ocr_norm maps characters OCR confuses in DejaVu Sans Mono (1/l/I/|, C/( , 0/O,
|
|
# typographic dashes for "-") to one form; expected text gets the same mapping,
|
|
# so comparisons stay strict about everything else.
|
|
ocr_norm() { sed 's/[‐‑‒–—―−]/-/g' | tr 'lI|(O' '111C0'; }
|
|
|
|
# screen_shows NAME TEXT... — take a screenshot and check that OCR finds every TEXT.
|
|
screen_shows() {
|
|
local name=$1 text want
|
|
shift
|
|
shot "$name"
|
|
text=$(screen_text "$name" | ocr_norm)
|
|
for want in "$@"; do
|
|
want=$(printf '%s' "$want" | ocr_norm)
|
|
[[ $text == *"$want"* ]] || return 1
|
|
done
|
|
}
|
|
|
|
# screen_count NAME TEXT — how often TEXT is on screenshot NAME: the highest
|
|
# count of any OCR variant (they read the same screen, so they are not added).
|
|
screen_count() {
|
|
local want i n max=0
|
|
want=$(printf '%s' "$2" | ocr_norm)
|
|
for i in "${!OCR_VARIANTS[@]}"; do
|
|
n=$(ocr_variant "$1" "$i" | ocr_norm | grep -oF -- "$want" | wc -l)
|
|
[ "$n" -gt "$max" ] && max=$n
|
|
done
|
|
echo "$max"
|
|
}
|