mirror of
https://github.com/jarun/nnn.git
synced 2026-10-02 14:56:57 +02:00
427 lines
12 KiB
Bash
Executable File
427 lines
12 KiB
Bash
Executable File
#!/bin/sh
|
|
#
|
|
# POSIX test wrapper for nnn (no tmux dependency)
|
|
#
|
|
# Runs nnn inside a pty (via the `script` command) and feeds it keystrokes
|
|
# from an input file, one input per line. Captures terminal output for
|
|
# verification.
|
|
#
|
|
# Dependencies: script (from util-linux / POSIX), mkfifo
|
|
#
|
|
# Usage:
|
|
# ./misc/test/nnn_runner.sh [OPTIONS] <input-file> [nnn-args ...]
|
|
#
|
|
# Options:
|
|
# -e <path> Path to nnn binary (default: ./nnn)
|
|
# -d <dir> Working directory for nnn (default: temporary dir)
|
|
# -o <dir> Output directory for snapshots (default: /tmp/nnn-test.<pid>)
|
|
# -t <ms> Delay in milliseconds between inputs (default: 200)
|
|
# -v Verbose: print each input as it is sent
|
|
# -h Show this help
|
|
#
|
|
# Input file format:
|
|
# One input per line. Blank lines and lines starting with '#' are skipped.
|
|
# Lines starting with '@' are directives:
|
|
# @sleep <ms> Extra delay in milliseconds
|
|
# @snapshot <label> Capture a named snapshot
|
|
# @assert <string> Fail if <string> is NOT in terminal output (full history)
|
|
# @refute <string> Fail if <string> IS in output since last @mark
|
|
# @mark Set checkpoint; subsequent @refute checks only output
|
|
# produced after this point
|
|
#
|
|
# Key encoding:
|
|
# >text Send literal text (no newline appended)
|
|
# Enter Send carriage return (\r)
|
|
# Escape Send ESC (\033)
|
|
# Tab Send tab (\t)
|
|
# Space Send space
|
|
# Up Send arrow up (\033[A)
|
|
# Down Send arrow down (\033[B)
|
|
# Right Send arrow right (\033[C)
|
|
# Left Send arrow left (\033[D)
|
|
# BSpace Send backspace (\177)
|
|
# C-<x> Send Ctrl+<x> (e.g. C-a sends \001)
|
|
# Any other single character is sent as-is (e.g. j, k, q, /, d)
|
|
#
|
|
# Example input file:
|
|
# # Navigate down and open a file
|
|
# j
|
|
# j
|
|
# Enter
|
|
# @sleep 500
|
|
# @snapshot after-open
|
|
# q
|
|
#
|
|
# Exit codes:
|
|
# 0 All assertions passed
|
|
# 1 Usage error / missing dependencies
|
|
# 2 Assertion failure
|
|
|
|
set -e
|
|
|
|
# ---------- defaults ----------
|
|
NNN_BIN="./nnn"
|
|
WORK_DIR=""
|
|
OUT_DIR=""
|
|
DELAY_MS=200
|
|
VERBOSE=0
|
|
PASS_COUNT=0
|
|
FAIL_COUNT=0
|
|
|
|
# ---------- helpers ----------
|
|
usage() {
|
|
sed -n '2,/^$/{ s/^#//; s/^ //; p; }' "$0"
|
|
exit 1
|
|
}
|
|
|
|
die() {
|
|
printf "error: %s\n" "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
cleanup() {
|
|
# Kill nnn/script if still running
|
|
if [ -n "$SCRIPT_PID" ] && kill -0 "$SCRIPT_PID" 2>/dev/null; then
|
|
kill "$SCRIPT_PID" 2>/dev/null || true
|
|
wait "$SCRIPT_PID" 2>/dev/null || true
|
|
fi
|
|
# Close the input pipe fd
|
|
exec 3>&- 2>/dev/null || true
|
|
# Remove temporary working directory
|
|
if [ -n "$TMPWORK" ] && [ -d "$TMPWORK" ]; then
|
|
rm -rf "$TMPWORK"
|
|
fi
|
|
# Remove the FIFO
|
|
if [ -n "$INPUT_FIFO" ] && [ -p "$INPUT_FIFO" ]; then
|
|
rm -f "$INPUT_FIFO"
|
|
fi
|
|
}
|
|
|
|
msleep() {
|
|
# Portable millisecond sleep: try fractional seconds, fall back to 1s.
|
|
_s=$(( $1 / 1000 ))
|
|
_ms=$(( $1 % 1000 ))
|
|
if [ "$_ms" -gt 0 ]; then
|
|
sleep "${_s}.$(printf '%03d' "$_ms")" 2>/dev/null || sleep $(( _s + 1 ))
|
|
elif [ "$_s" -gt 0 ]; then
|
|
sleep "$_s"
|
|
fi
|
|
}
|
|
|
|
# Map a key name to a raw byte sequence
|
|
map_key() {
|
|
case "$1" in
|
|
Enter) printf '\r' ;;
|
|
Escape) printf '\033' ;;
|
|
Tab) printf '\t' ;;
|
|
Space) printf ' ' ;;
|
|
BSpace) printf '\177' ;;
|
|
Up) printf '\033[A' ;;
|
|
Down) printf '\033[B' ;;
|
|
Right) printf '\033[C' ;;
|
|
Left) printf '\033[D' ;;
|
|
Home) printf '\033[H' ;;
|
|
End) printf '\033[F' ;;
|
|
PgUp) printf '\033[5~' ;;
|
|
PgDn) printf '\033[6~' ;;
|
|
Delete) printf '\033[3~' ;;
|
|
Insert) printf '\033[2~' ;;
|
|
F1) printf '\033OP' ;;
|
|
F2) printf '\033OQ' ;;
|
|
F3) printf '\033OR' ;;
|
|
F4) printf '\033OS' ;;
|
|
F5) printf '\033[15~' ;;
|
|
F6) printf '\033[17~' ;;
|
|
F7) printf '\033[18~' ;;
|
|
F8) printf '\033[19~' ;;
|
|
F9) printf '\033[20~' ;;
|
|
F10) printf '\033[21~' ;;
|
|
F11) printf '\033[23~' ;;
|
|
F12) printf '\033[24~' ;;
|
|
C-?)
|
|
# Ctrl+letter: convert to control character (1-26 for a-z)
|
|
_letter="$(printf '%s' "$1" | cut -c3)"
|
|
_upper="$(printf '%s' "$_letter" | tr 'a-z' 'A-Z')"
|
|
_ord=$(printf '%d' "'$_upper")
|
|
_ctrl=$(( _ord - 64 ))
|
|
printf "\\$(printf '%03o' "$_ctrl")"
|
|
;;
|
|
?)
|
|
# Single character — send as-is
|
|
printf '%s' "$1"
|
|
;;
|
|
*)
|
|
# Multi-char: send as literal keystrokes
|
|
printf '%s' "$1"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Send raw bytes into the nnn pty via the FIFO
|
|
send_key() {
|
|
map_key "$1" >&3
|
|
}
|
|
|
|
send_literal() {
|
|
printf '%s' "$1" >&3
|
|
}
|
|
|
|
# Strip ANSI escape sequences and control chars to get plain content
|
|
strip_ansi() {
|
|
# Use printf to get a literal ESC character for portable sed
|
|
_esc="$(printf '\033')"
|
|
_cr="$(printf '\r')"
|
|
_bs="$(printf '\010')"
|
|
# 1) CSI sequences: ESC [ <params> <letter>
|
|
# 2) OSC sequences: ESC ] ... (until ESC or end)
|
|
# 3) Charset select: ESC ( <char>, ESC ) <char>
|
|
# 4) Other 2-byte: ESC <char> (for =, >, <, etc.)
|
|
# 5) Lone ESC
|
|
# 6) char + backspace pairs (tty echo artifacts)
|
|
# 7) Carriage returns
|
|
sed "s/${_esc}\[[^a-zA-Z]*[a-zA-Z]//g
|
|
s/${_esc}\][^${_esc}]*//g
|
|
s/${_esc}[()].\{0,1\}//g
|
|
s/${_esc}.//g
|
|
s/${_esc}//g
|
|
s/.${_bs}//g
|
|
s/${_cr}//g"
|
|
}
|
|
|
|
# Get a clean snapshot of the current terminal output (full history)
|
|
get_screen() {
|
|
if [ -f "$TYPESCRIPT" ]; then
|
|
tail -c 16384 "$TYPESCRIPT" | strip_ansi
|
|
fi
|
|
}
|
|
|
|
# Get only the terminal output produced since the last checkpoint
|
|
# This is what assertions run against, so they test the current screen.
|
|
LAST_OFFSET=0
|
|
get_recent_screen() {
|
|
if [ -f "$TYPESCRIPT" ]; then
|
|
_size="$(wc -c < "$TYPESCRIPT")"
|
|
_new=$(( _size - LAST_OFFSET ))
|
|
if [ "$_new" -gt 0 ]; then
|
|
tail -c "$_new" "$TYPESCRIPT" | strip_ansi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Advance the checkpoint to the current end of the typescript
|
|
advance_offset() {
|
|
if [ -f "$TYPESCRIPT" ]; then
|
|
LAST_OFFSET="$(wc -c < "$TYPESCRIPT")"
|
|
fi
|
|
}
|
|
|
|
snapshot() {
|
|
_label="$1"
|
|
_file="${OUT_DIR}/${_label}.txt"
|
|
get_screen > "$_file"
|
|
[ "$VERBOSE" -eq 1 ] && printf " snapshot: %s\n" "$_file"
|
|
}
|
|
|
|
assert_screen() {
|
|
_pattern="$1"
|
|
_content="$(get_screen)"
|
|
if printf '%s' "$_content" | grep -qF "$_pattern"; then
|
|
PASS_COUNT=$(( PASS_COUNT + 1 ))
|
|
[ "$VERBOSE" -eq 1 ] && printf " PASS: assert '%s'\n" "$_pattern"
|
|
else
|
|
FAIL_COUNT=$(( FAIL_COUNT + 1 ))
|
|
printf " FAIL: expected '%s' on screen\n" "$_pattern" >&2
|
|
printf " --- screen (last 2000 chars) ---\n" >&2
|
|
printf '%s' "$_content" | tail -c 2000 >&2
|
|
printf "\n --- end ---\n" >&2
|
|
fi
|
|
}
|
|
|
|
refute_screen() {
|
|
_pattern="$1"
|
|
_content="$(get_recent_screen)"
|
|
if printf '%s' "$_content" | grep -qF "$_pattern"; then
|
|
FAIL_COUNT=$(( FAIL_COUNT + 1 ))
|
|
printf " FAIL: unexpected '%s' on screen\n" "$_pattern" >&2
|
|
printf " --- screen (last 2000 chars) ---\n" >&2
|
|
printf '%s' "$_content" | tail -c 2000 >&2
|
|
printf "\n --- end ---\n" >&2
|
|
else
|
|
PASS_COUNT=$(( PASS_COUNT + 1 ))
|
|
[ "$VERBOSE" -eq 1 ] && printf " PASS: refute '%s'\n" "$_pattern"
|
|
fi
|
|
}
|
|
|
|
# ---------- parse options ----------
|
|
while getopts "e:d:o:t:vh" opt; do
|
|
case "$opt" in
|
|
e) NNN_BIN="$OPTARG" ;;
|
|
d) WORK_DIR="$OPTARG" ;;
|
|
o) OUT_DIR="$OPTARG" ;;
|
|
t) DELAY_MS="$OPTARG" ;;
|
|
v) VERBOSE=1 ;;
|
|
h) usage ;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
shift $(( OPTIND - 1 ))
|
|
|
|
INPUT_FILE="${1:?input file required}"
|
|
shift
|
|
NNN_EXTRA_ARGS="$*"
|
|
|
|
[ -f "$INPUT_FILE" ] || die "input file not found: $INPUT_FILE"
|
|
command -v script >/dev/null 2>&1 || die "'script' command is required but not found"
|
|
|
|
# Resolve nnn binary
|
|
case "$NNN_BIN" in
|
|
/*) ;; # absolute
|
|
*) NNN_BIN="$(cd "$(dirname "$NNN_BIN")" && pwd)/$(basename "$NNN_BIN")" ;;
|
|
esac
|
|
[ -x "$NNN_BIN" ] || die "nnn binary not found or not executable: $NNN_BIN"
|
|
|
|
# ---------- set up environment ----------
|
|
TMPWORK=""
|
|
if [ -z "$WORK_DIR" ]; then
|
|
TMPWORK="$(mktemp -d "${TMPDIR:-/tmp}/nnn-test-work.XXXXXX")"
|
|
WORK_DIR="$TMPWORK"
|
|
# populate with a few test entries
|
|
mkdir -p "$WORK_DIR/dir1" "$WORK_DIR/dir2"
|
|
printf 'hello\n' > "$WORK_DIR/file1.txt"
|
|
printf 'world\n' > "$WORK_DIR/file2.txt"
|
|
fi
|
|
|
|
if [ -z "$OUT_DIR" ]; then
|
|
OUT_DIR="/tmp/nnn-test.$$"
|
|
fi
|
|
mkdir -p "$OUT_DIR"
|
|
|
|
TYPESCRIPT="${OUT_DIR}/typescript.raw"
|
|
INPUT_FIFO="${OUT_DIR}/input.fifo"
|
|
SCRIPT_PID=""
|
|
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# Create a FIFO for feeding input to nnn
|
|
rm -f "$INPUT_FIFO"
|
|
mkfifo "$INPUT_FIFO"
|
|
|
|
# ---------- start nnn inside script(1) pty ----------
|
|
# The `script` command allocates a real pty so nnn's isatty() check passes.
|
|
# We feed keystrokes through the FIFO connected to script's stdin.
|
|
# Terminal output is captured to the typescript file.
|
|
|
|
# Detect script flavour (GNU coreutils vs BSD)
|
|
if script --version 2>&1 | grep -q 'util-linux'; then
|
|
# GNU/Linux: script -q -f -c <cmd> <outfile>
|
|
script -q -f -c "cd '${WORK_DIR}' && exec '${NNN_BIN}' ${NNN_EXTRA_ARGS}" "$TYPESCRIPT" \
|
|
< "$INPUT_FIFO" > /dev/null 2>&1 &
|
|
else
|
|
# BSD / macOS: script -q -F <outfile> <cmd> (-F = flush on BSD)
|
|
script -q -F "$TYPESCRIPT" sh -c "cd '${WORK_DIR}' && exec '${NNN_BIN}' ${NNN_EXTRA_ARGS}" \
|
|
< "$INPUT_FIFO" > /dev/null 2>&1 &
|
|
fi
|
|
SCRIPT_PID=$!
|
|
|
|
# Open the FIFO for writing on fd 3 (keeps the pipe open so nnn doesn't get EOF)
|
|
exec 3>"$INPUT_FIFO"
|
|
|
|
# Give nnn time to start and render its first frame
|
|
msleep 500
|
|
|
|
[ "$VERBOSE" -eq 1 ] && printf "nnn started (pid %s)\n" "$SCRIPT_PID"
|
|
|
|
# Take an initial snapshot
|
|
snapshot "00-init"
|
|
|
|
# ---------- process input file ----------
|
|
STEP=1
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
# Skip blank lines and comments
|
|
case "$line" in
|
|
""|\#*) continue ;;
|
|
esac
|
|
|
|
PADSTEP="$(printf '%02d' "$STEP")"
|
|
|
|
# Handle directives
|
|
case "$line" in
|
|
@sleep\ *)
|
|
_val="${line#@sleep }"
|
|
[ "$VERBOSE" -eq 1 ] && printf "[%s] sleep %s ms\n" "$PADSTEP" "$_val"
|
|
msleep "$_val"
|
|
;;
|
|
@snapshot\ *)
|
|
_label="${line#@snapshot }"
|
|
[ "$VERBOSE" -eq 1 ] && printf "[%s] snapshot '%s'\n" "$PADSTEP" "$_label"
|
|
snapshot "${PADSTEP}-${_label}"
|
|
;;
|
|
@assert\ *)
|
|
_pat="${line#@assert }"
|
|
[ "$VERBOSE" -eq 1 ] && printf "[%s] assert '%s'\n" "$PADSTEP" "$_pat"
|
|
assert_screen "$_pat"
|
|
;;
|
|
@refute\ *)
|
|
_pat="${line#@refute }"
|
|
[ "$VERBOSE" -eq 1 ] && printf "[%s] refute '%s'\n" "$PADSTEP" "$_pat"
|
|
refute_screen "$_pat"
|
|
;;
|
|
@mark)
|
|
[ "$VERBOSE" -eq 1 ] && printf "[%s] mark offset\n" "$PADSTEP"
|
|
advance_offset
|
|
;;
|
|
\>*)
|
|
# Literal text (strip leading '>')
|
|
_text="${line#>}"
|
|
[ "$VERBOSE" -eq 1 ] && printf "[%s] literal: %s\n" "$PADSTEP" "$_text"
|
|
send_literal "$_text"
|
|
msleep "$DELAY_MS"
|
|
;;
|
|
*)
|
|
# Key name — translate and send
|
|
[ "$VERBOSE" -eq 1 ] && printf "[%s] key: %s\n" "$PADSTEP" "$line"
|
|
send_key "$line"
|
|
msleep "$DELAY_MS"
|
|
;;
|
|
esac
|
|
|
|
# Auto-snapshot after each step
|
|
snapshot "${PADSTEP}-auto"
|
|
|
|
STEP=$(( STEP + 1 ))
|
|
done < "$INPUT_FILE"
|
|
|
|
# Final snapshot
|
|
snapshot "final"
|
|
|
|
# Close the input fd — this causes nnn to see EOF and exit
|
|
exec 3>&-
|
|
|
|
# Wait for script/nnn to finish (with a timeout)
|
|
_waited=0
|
|
while kill -0 "$SCRIPT_PID" 2>/dev/null && [ "$_waited" -lt 3 ]; do
|
|
msleep 500
|
|
_waited=$(( _waited + 1 ))
|
|
done
|
|
if kill -0 "$SCRIPT_PID" 2>/dev/null; then
|
|
kill "$SCRIPT_PID" 2>/dev/null || true
|
|
fi
|
|
wait "$SCRIPT_PID" 2>/dev/null || true
|
|
SCRIPT_PID=""
|
|
|
|
# ---------- summary ----------
|
|
TOTAL=$(( PASS_COUNT + FAIL_COUNT ))
|
|
if [ "$TOTAL" -gt 0 ]; then
|
|
printf "\n=== Results: %d passed, %d failed (of %d) ===\n" \
|
|
"$PASS_COUNT" "$FAIL_COUNT" "$TOTAL"
|
|
fi
|
|
|
|
printf "Snapshots saved to: %s\n" "$OUT_DIR"
|
|
printf "Raw typescript: %s\n" "$TYPESCRIPT"
|
|
|
|
if [ "$FAIL_COUNT" -gt 0 ]; then
|
|
exit 2
|
|
fi
|
|
|
|
exit 0
|