nnn

mirror of https://github.com/jarun/nnn
Log | Files | Refs | README | LICENSE

nnn_runner.sh (12741B)


      1 #!/bin/sh
      2 #
      3 # POSIX test wrapper for nnn (no tmux dependency)
      4 #
      5 # Runs nnn inside a pty (via the `script` command) and feeds it keystrokes
      6 # from an input file, one input per line. Captures terminal output for
      7 # verification.
      8 #
      9 # Dependencies: script (from util-linux / POSIX), mkfifo
     10 #
     11 # Usage:
     12 #   ./misc/test/nnn_runner.sh [OPTIONS] <input-file> [nnn-args ...]
     13 #
     14 # Options:
     15 #   -e <path>      Path to nnn binary (default: ./nnn)
     16 #   -d <dir>       Working directory for nnn (default: temporary dir)
     17 #   -o <dir>       Output directory for snapshots (default: /tmp/nnn-test.<pid>)
     18 #   -t <ms>        Delay in milliseconds between inputs (default: 200)
     19 #   -v             Verbose: print each input as it is sent
     20 #   -h             Show this help
     21 #
     22 # Input file format:
     23 #   One input per line. Blank lines and lines starting with '#' are skipped.
     24 #   Lines starting with '@' are directives:
     25 #     @sleep <ms>         Extra delay in milliseconds
     26 #     @snapshot <label>   Capture a named snapshot
     27 #     @assert <string>    Fail if <string> is NOT in terminal output (full history)
     28 #     @refute <string>    Fail if <string> IS in output since last @mark
     29 #     @mark               Set checkpoint; subsequent @refute checks only output
     30 #                         produced after this point
     31 #
     32 #   Key encoding:
     33 #     >text         Send literal text (no newline appended)
     34 #     Enter         Send carriage return (\r)
     35 #     Escape        Send ESC (\033)
     36 #     Tab           Send tab (\t)
     37 #     Space         Send space
     38 #     Up            Send arrow up    (\033[A)
     39 #     Down          Send arrow down  (\033[B)
     40 #     Right         Send arrow right (\033[C)
     41 #     Left          Send arrow left  (\033[D)
     42 #     BSpace        Send backspace (\177)
     43 #     C-<x>         Send Ctrl+<x>  (e.g. C-a sends \001)
     44 #     Any other single character is sent as-is (e.g. j, k, q, /, d)
     45 #
     46 # Example input file:
     47 #   # Navigate down and open a file
     48 #   j
     49 #   j
     50 #   Enter
     51 #   @sleep 500
     52 #   @snapshot after-open
     53 #   q
     54 #
     55 # Exit codes:
     56 #   0   All assertions passed
     57 #   1   Usage error / missing dependencies
     58 #   2   Assertion failure
     59 
     60 set -e
     61 
     62 # ---------- defaults ----------
     63 NNN_BIN="./nnn"
     64 WORK_DIR=""
     65 OUT_DIR=""
     66 DELAY_MS=200
     67 VERBOSE=0
     68 PASS_COUNT=0
     69 FAIL_COUNT=0
     70 
     71 # ---------- helpers ----------
     72 usage() {
     73     sed -n '2,/^$/{ s/^#//; s/^ //; p; }' "$0"
     74     exit 1
     75 }
     76 
     77 die() {
     78     printf "error: %s\n" "$1" >&2
     79     exit 1
     80 }
     81 
     82 cleanup() {
     83     # Kill nnn/script if still running
     84     if [ -n "$SCRIPT_PID" ] && kill -0 "$SCRIPT_PID" 2>/dev/null; then
     85         kill "$SCRIPT_PID" 2>/dev/null || true
     86         wait "$SCRIPT_PID" 2>/dev/null || true
     87     fi
     88     # Close the input pipe fd
     89     exec 3>&- 2>/dev/null || true
     90     # Remove temporary working directory
     91     if [ -n "$TMPWORK" ] && [ -d "$TMPWORK" ]; then
     92         rm -rf "$TMPWORK"
     93     fi
     94     # Remove the FIFO
     95     if [ -n "$INPUT_FIFO" ] && [ -p "$INPUT_FIFO" ]; then
     96         rm -f "$INPUT_FIFO"
     97     fi
     98 }
     99 
    100 msleep() {
    101     # Portable millisecond sleep: try fractional seconds, fall back to 1s.
    102     _s=$(( $1 / 1000 ))
    103     _ms=$(( $1 % 1000 ))
    104     if [ "$_ms" -gt 0 ]; then
    105         sleep "${_s}.$(printf '%03d' "$_ms")" 2>/dev/null || sleep $(( _s + 1 ))
    106     elif [ "$_s" -gt 0 ]; then
    107         sleep "$_s"
    108     fi
    109 }
    110 
    111 # Map a key name to a raw byte sequence
    112 map_key() {
    113     case "$1" in
    114         Enter)      printf '\r' ;;
    115         Escape)     printf '\033' ;;
    116         Tab)        printf '\t' ;;
    117         Space)      printf ' ' ;;
    118         BSpace)     printf '\177' ;;
    119         Up)         printf '\033[A' ;;
    120         Down)       printf '\033[B' ;;
    121         Right)      printf '\033[C' ;;
    122         Left)       printf '\033[D' ;;
    123         Home)       printf '\033[H' ;;
    124         End)        printf '\033[F' ;;
    125         PgUp)       printf '\033[5~' ;;
    126         PgDn)       printf '\033[6~' ;;
    127         Delete)     printf '\033[3~' ;;
    128         Insert)     printf '\033[2~' ;;
    129         F1)         printf '\033OP' ;;
    130         F2)         printf '\033OQ' ;;
    131         F3)         printf '\033OR' ;;
    132         F4)         printf '\033OS' ;;
    133         F5)         printf '\033[15~' ;;
    134         F6)         printf '\033[17~' ;;
    135         F7)         printf '\033[18~' ;;
    136         F8)         printf '\033[19~' ;;
    137         F9)         printf '\033[20~' ;;
    138         F10)        printf '\033[21~' ;;
    139         F11)        printf '\033[23~' ;;
    140         F12)        printf '\033[24~' ;;
    141         C-?)
    142             # Ctrl+letter: convert to control character (1-26 for a-z)
    143             _letter="$(printf '%s' "$1" | cut -c3)"
    144             _upper="$(printf '%s' "$_letter" | tr 'a-z' 'A-Z')"
    145             _ord=$(printf '%d' "'$_upper")
    146             _ctrl=$(( _ord - 64 ))
    147             printf "\\$(printf '%03o' "$_ctrl")"
    148             ;;
    149         ?)
    150             # Single character — send as-is
    151             printf '%s' "$1"
    152             ;;
    153         *)
    154             # Multi-char: send as literal keystrokes
    155             printf '%s' "$1"
    156             ;;
    157     esac
    158 }
    159 
    160 # Send raw bytes into the nnn pty via the FIFO
    161 send_key() {
    162     map_key "$1" >&3
    163 }
    164 
    165 send_literal() {
    166     printf '%s' "$1" >&3
    167 }
    168 
    169 # Strip ANSI escape sequences and control chars to get plain content
    170 strip_ansi() {
    171     # Use printf to get a literal ESC character for portable sed
    172     _esc="$(printf '\033')"
    173     _cr="$(printf '\r')"
    174     _bs="$(printf '\010')"
    175     # 1) CSI sequences:  ESC [ <params> <letter>
    176     # 2) OSC sequences:  ESC ] ... (until ESC or end)
    177     # 3) Charset select: ESC ( <char>, ESC ) <char>
    178     # 4) Other 2-byte:   ESC <char> (for =, >, <, etc.)
    179     # 5) Lone ESC
    180     # 6) char + backspace pairs (tty echo artifacts)
    181     # 7) Carriage returns
    182     sed "s/${_esc}\[[^a-zA-Z]*[a-zA-Z]//g
    183          s/${_esc}\][^${_esc}]*//g
    184          s/${_esc}[()].\{0,1\}//g
    185          s/${_esc}.//g
    186          s/${_esc}//g
    187          s/.${_bs}//g
    188          s/${_cr}//g"
    189 }
    190 
    191 # Get a clean snapshot of the current terminal output (full history)
    192 get_screen() {
    193     if [ -f "$TYPESCRIPT" ]; then
    194         tail -c 16384 "$TYPESCRIPT" | strip_ansi
    195     fi
    196 }
    197 
    198 # Get only the terminal output produced since the last checkpoint
    199 # This is what assertions run against, so they test the current screen.
    200 LAST_OFFSET=0
    201 get_recent_screen() {
    202     if [ -f "$TYPESCRIPT" ]; then
    203         _size="$(wc -c < "$TYPESCRIPT")"
    204         _new=$(( _size - LAST_OFFSET ))
    205         if [ "$_new" -gt 0 ]; then
    206             tail -c "$_new" "$TYPESCRIPT" | strip_ansi
    207         fi
    208     fi
    209 }
    210 
    211 # Advance the checkpoint to the current end of the typescript
    212 advance_offset() {
    213     if [ -f "$TYPESCRIPT" ]; then
    214         LAST_OFFSET="$(wc -c < "$TYPESCRIPT")"
    215     fi
    216 }
    217 
    218 snapshot() {
    219     _label="$1"
    220     _file="${OUT_DIR}/${_label}.txt"
    221     get_screen > "$_file"
    222     [ "$VERBOSE" -eq 1 ] && printf "  snapshot: %s\n" "$_file"
    223 }
    224 
    225 assert_screen() {
    226     _pattern="$1"
    227     _content="$(get_screen)"
    228     if printf '%s' "$_content" | grep -qF "$_pattern"; then
    229         PASS_COUNT=$(( PASS_COUNT + 1 ))
    230         [ "$VERBOSE" -eq 1 ] && printf "  PASS: assert '%s'\n" "$_pattern"
    231     else
    232         FAIL_COUNT=$(( FAIL_COUNT + 1 ))
    233         printf "  FAIL: expected '%s' on screen\n" "$_pattern" >&2
    234         printf "  --- screen (last 2000 chars) ---\n" >&2
    235         printf '%s' "$_content" | tail -c 2000 >&2
    236         printf "\n  --- end ---\n" >&2
    237     fi
    238 }
    239 
    240 refute_screen() {
    241     _pattern="$1"
    242     _content="$(get_recent_screen)"
    243     if printf '%s' "$_content" | grep -qF "$_pattern"; then
    244         FAIL_COUNT=$(( FAIL_COUNT + 1 ))
    245         printf "  FAIL: unexpected '%s' on screen\n" "$_pattern" >&2
    246         printf "  --- screen (last 2000 chars) ---\n" >&2
    247         printf '%s' "$_content" | tail -c 2000 >&2
    248         printf "\n  --- end ---\n" >&2
    249     else
    250         PASS_COUNT=$(( PASS_COUNT + 1 ))
    251         [ "$VERBOSE" -eq 1 ] && printf "  PASS: refute '%s'\n" "$_pattern"
    252     fi
    253 }
    254 
    255 # ---------- parse options ----------
    256 while getopts "e:d:o:t:vh" opt; do
    257     case "$opt" in
    258         e) NNN_BIN="$OPTARG" ;;
    259         d) WORK_DIR="$OPTARG" ;;
    260         o) OUT_DIR="$OPTARG" ;;
    261         t) DELAY_MS="$OPTARG" ;;
    262         v) VERBOSE=1 ;;
    263         h) usage ;;
    264         *) usage ;;
    265     esac
    266 done
    267 shift $(( OPTIND - 1 ))
    268 
    269 INPUT_FILE="${1:?input file required}"
    270 shift
    271 NNN_EXTRA_ARGS="$*"
    272 
    273 [ -f "$INPUT_FILE" ] || die "input file not found: $INPUT_FILE"
    274 command -v script >/dev/null 2>&1 || die "'script' command is required but not found"
    275 
    276 # Resolve nnn binary
    277 case "$NNN_BIN" in
    278     /*) ;;  # absolute
    279     *)  NNN_BIN="$(cd "$(dirname "$NNN_BIN")" && pwd)/$(basename "$NNN_BIN")" ;;
    280 esac
    281 [ -x "$NNN_BIN" ] || die "nnn binary not found or not executable: $NNN_BIN"
    282 
    283 # ---------- set up environment ----------
    284 TMPWORK=""
    285 if [ -z "$WORK_DIR" ]; then
    286     TMPWORK="$(mktemp -d "${TMPDIR:-/tmp}/nnn-test-work.XXXXXX")"
    287     WORK_DIR="$TMPWORK"
    288     # populate with a few test entries
    289     mkdir -p "$WORK_DIR/dir1" "$WORK_DIR/dir2"
    290     printf 'hello\n' > "$WORK_DIR/file1.txt"
    291     printf 'world\n' > "$WORK_DIR/file2.txt"
    292 fi
    293 
    294 if [ -z "$OUT_DIR" ]; then
    295     OUT_DIR="/tmp/nnn-test.$$"
    296 fi
    297 mkdir -p "$OUT_DIR"
    298 
    299 TYPESCRIPT="${OUT_DIR}/typescript.raw"
    300 INPUT_FIFO="${OUT_DIR}/input.fifo"
    301 SCRIPT_PID=""
    302 
    303 trap cleanup EXIT INT TERM
    304 
    305 # Create a FIFO for feeding input to nnn
    306 rm -f "$INPUT_FIFO"
    307 mkfifo "$INPUT_FIFO"
    308 
    309 # ---------- start nnn inside script(1) pty ----------
    310 # The `script` command allocates a real pty so nnn's isatty() check passes.
    311 # We feed keystrokes through the FIFO connected to script's stdin.
    312 # Terminal output is captured to the typescript file.
    313 
    314 # Detect script flavour (GNU coreutils vs BSD)
    315 if script --version 2>&1 | grep -q 'util-linux'; then
    316     # GNU/Linux: script -q -f -c <cmd> <outfile>
    317     script -q -f -c "cd '${WORK_DIR}' && exec '${NNN_BIN}' ${NNN_EXTRA_ARGS}" "$TYPESCRIPT" \
    318         < "$INPUT_FIFO" > /dev/null 2>&1 &
    319 else
    320     # BSD / macOS: script -q -F <outfile> <cmd>  (-F = flush on BSD)
    321     script -q -F "$TYPESCRIPT" sh -c "cd '${WORK_DIR}' && exec '${NNN_BIN}' ${NNN_EXTRA_ARGS}" \
    322         < "$INPUT_FIFO" > /dev/null 2>&1 &
    323 fi
    324 SCRIPT_PID=$!
    325 
    326 # Open the FIFO for writing on fd 3 (keeps the pipe open so nnn doesn't get EOF)
    327 exec 3>"$INPUT_FIFO"
    328 
    329 # Give nnn time to start and render its first frame
    330 msleep 500
    331 
    332 [ "$VERBOSE" -eq 1 ] && printf "nnn started (pid %s)\n" "$SCRIPT_PID"
    333 
    334 # Take an initial snapshot
    335 snapshot "00-init"
    336 
    337 # ---------- process input file ----------
    338 STEP=1
    339 while IFS= read -r line || [ -n "$line" ]; do
    340     # Skip blank lines and comments
    341     case "$line" in
    342         ""|\#*) continue ;;
    343     esac
    344 
    345     PADSTEP="$(printf '%02d' "$STEP")"
    346 
    347     # Handle directives
    348     case "$line" in
    349         @sleep\ *)
    350             _val="${line#@sleep }"
    351             [ "$VERBOSE" -eq 1 ] && printf "[%s] sleep %s ms\n" "$PADSTEP" "$_val"
    352             msleep "$_val"
    353             ;;
    354         @snapshot\ *)
    355             _label="${line#@snapshot }"
    356             [ "$VERBOSE" -eq 1 ] && printf "[%s] snapshot '%s'\n" "$PADSTEP" "$_label"
    357             snapshot "${PADSTEP}-${_label}"
    358             ;;
    359         @assert\ *)
    360             _pat="${line#@assert }"
    361             [ "$VERBOSE" -eq 1 ] && printf "[%s] assert '%s'\n" "$PADSTEP" "$_pat"
    362             assert_screen "$_pat"
    363             ;;
    364         @refute\ *)
    365             _pat="${line#@refute }"
    366             [ "$VERBOSE" -eq 1 ] && printf "[%s] refute '%s'\n" "$PADSTEP" "$_pat"
    367             refute_screen "$_pat"
    368             ;;
    369         @mark)
    370             [ "$VERBOSE" -eq 1 ] && printf "[%s] mark offset\n" "$PADSTEP"
    371             advance_offset
    372             ;;
    373         \>*)
    374             # Literal text (strip leading '>')
    375             _text="${line#>}"
    376             [ "$VERBOSE" -eq 1 ] && printf "[%s] literal: %s\n" "$PADSTEP" "$_text"
    377             send_literal "$_text"
    378             msleep "$DELAY_MS"
    379             ;;
    380         *)
    381             # Key name — translate and send
    382             [ "$VERBOSE" -eq 1 ] && printf "[%s] key: %s\n" "$PADSTEP" "$line"
    383             send_key "$line"
    384             msleep "$DELAY_MS"
    385             ;;
    386     esac
    387 
    388     # Auto-snapshot after each step
    389     snapshot "${PADSTEP}-auto"
    390 
    391     STEP=$(( STEP + 1 ))
    392 done < "$INPUT_FILE"
    393 
    394 # Final snapshot
    395 snapshot "final"
    396 
    397 # Close the input fd — this causes nnn to see EOF and exit
    398 exec 3>&-
    399 
    400 # Wait for script/nnn to finish (with a timeout)
    401 _waited=0
    402 while kill -0 "$SCRIPT_PID" 2>/dev/null && [ "$_waited" -lt 3 ]; do
    403     msleep 500
    404     _waited=$(( _waited + 1 ))
    405 done
    406 if kill -0 "$SCRIPT_PID" 2>/dev/null; then
    407     kill "$SCRIPT_PID" 2>/dev/null || true
    408 fi
    409 wait "$SCRIPT_PID" 2>/dev/null || true
    410 SCRIPT_PID=""
    411 
    412 # ---------- summary ----------
    413 TOTAL=$(( PASS_COUNT + FAIL_COUNT ))
    414 if [ "$TOTAL" -gt 0 ]; then
    415     printf "\n=== Results: %d passed, %d failed (of %d) ===\n" \
    416         "$PASS_COUNT" "$FAIL_COUNT" "$TOTAL"
    417 fi
    418 
    419 printf "Snapshots saved to: %s\n" "$OUT_DIR"
    420 printf "Raw typescript: %s\n" "$TYPESCRIPT"
    421 
    422 if [ "$FAIL_COUNT" -gt 0 ]; then
    423     exit 2
    424 fi
    425 
    426 exit 0