#!/usr/bin/env bash set -euo pipefail # This script is invoked by smoketest.sh when the diff command is run: # ./smoketest.sh diff # # It provides a simple interactive TUI for reviewing snapshot diffs. # Path to the folder containing this script. script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Get current working directory cwd="$(pwd)" # If script_dir is the same as cwd, set script_rel_path to "./$(basename "$0")" if [[ "$script_dir" == "$cwd" ]]; then main_script_rel_path="./smoketest.sh" else main_script_rel_path="${script_dir#"$cwd"/}/smoketest.sh" fi usage() { cat < all snapshot base names. declare -A LOCALE_FAILED_COUNT=() # Maps: "locale" -> integer # of .error.html. declare -A LOCALE_TOTAL_COUNT=() # Maps: "locale" -> total # of snapshots. # If a locale was passed as an argument, only show snapshots for that locale. # Otherwise, show all locales. locale_dirs=() for_single_locale=false if [ $# -eq 1 ]; then # If a locale was passed, only show snapshots for that locale. locale_dirs=( "$SNAPSHOT_DIR/$1" ) for_single_locale=true # Check if the locale directory exists. If it doesn't, print an error and exit. if [ ! -d "${locale_dirs[0]}" ]; then echo "No snapshot directory found for locale '$1' in $SNAPSHOT_DIR." >&2 exit 1 fi else # Otherwise, gather a list of possible locale subdirectories. Only consider # directories, ignoring anything else. locale_dirs=( $(find "$SNAPSHOT_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort) ) # If none found, just say so. if [ ${#locale_dirs[@]} -eq 0 ]; then echo "No snapshot directories found in $SNAPSHOT_DIR." >&2 exit 0 fi fi for dir in "${locale_dirs[@]}"; do loc="$(basename "$dir")" # Gather all *.html inside that directory. Separates expected and actual # snapshots, and counts the total number of snapshots. html_files=( $(find "$dir" -maxdepth 1 -type f -name '*.html' 2>/dev/null | sort) ) if [ ${#html_files[@]} -eq 0 ]; then LOCALE_SNAPSHOTS["$loc"]="" LOCALE_FAILED_COUNT["$loc"]=0 LOCALE_TOTAL_COUNT["$loc"]=0 continue fi total_count=0 failed_count=0 found_bases=() failed_idx=0 for file in "${html_files[@]}"; do base="$(basename "$file")" if [[ "$base" == *".error.html" ]]; then short="${base%.error.html}" failed_count=$((failed_count + 1)) # Ensure we only add the base once. if [[ ! " ${found_bases[*]} " =~ " $short " ]]; then # Insert failed snapshots at the beginning. found_bases=( "${found_bases[@]:0:$failed_idx}" "$short" "${found_bases[@]:$failed_idx}" ) failed_idx=$((failed_idx + 1)) total_count=$((total_count + 1)) fi else short="${base%.html}" if [[ ! " ${found_bases[*]} " =~ " $short " ]]; then found_bases+=( "$short" ) total_count=$((total_count + 1)) fi fi done LOCALE_SNAPSHOTS["$loc"]="${found_bases[*]}" LOCALE_FAILED_COUNT["$loc"]=$failed_count LOCALE_TOTAL_COUNT["$loc"]=$total_count done locales_sorted=( $(printf "%s\n" "${!LOCALE_SNAPSHOTS[@]}" | sort) ) # Hide cursor for a less jumpy interface. tput civis cleanup() { # Re-enable cursor on exit. tput cnorm } trap cleanup EXIT # Reads one keypress (arrow keys, enter, esc, pgup, pgdn, etc.) in raw mode. # Returns a code: up/down/pgup/pgdn/enter/esc/backspace/ctrl_c/other # # Common escape sequences: # ↑: ESC [ A # ↓: ESC [ B # PageUp: ESC [ 5 ~ # PageDown: ESC [ 6 ~ read_keypress() { IFS= read -rsn1 key 2>/dev/null || true case "$key" in $'\x1b') # ESC # Try reading up to two (or three) more bytes to detect special keys read -rsn2 -t 0.001 rest 2>/dev/null || true case "$rest" in '[A') echo "up" ;; '[B') echo "down" ;; '[5') # Possibly PageUp if next byte is '~'. read -rsn1 -t 0.001 t3 2>/dev/null || true if [[ "$t3" == "~" ]]; then echo "pgup" else echo "esc" fi ;; '[6') # Possibly PageDown if next byte is '~'. read -rsn1 -t 0.001 t3 2>/dev/null || true if [[ "$t3" == "~" ]]; then echo "pgdn" else echo "esc" fi ;; *) # If it's not recognized, treat as ESC. echo "esc" ;; esac ;; '') # Enter sends empty string. echo "enter" ;; $'\x7f') # Backspace. echo "backspace" ;; q) # Treat q as backspace. echo "backspace" ;; *) # Could be ctrl_c, or random keys. # Check for ^C specifically. if [[ $key == $'\x03' ]]; then echo "ctrl_c" else echo "other" fi ;; esac } # print_menu <items[@]> <highlightIndex> # # Supports paging/scrolling so that long lists don't overflow the screen. # PageUp/PageDown will jump the highlight by max_items at a time. print_menu() { local title="$1" shift local -a items=("${!1}") # Indirect expansion: if you passed lines[@], this captures its elements shift local highlightIndex="$1" # How many total items? local count="${#items[@]}" # Clear screen. clear # Print header. echo -e "${CLR_CYAN}${title}${CLR_RESET}" echo "Use ↑/↓ or PgUp/PgDn to navigate, Enter to select, ESC/Backspace/q to go back, CTRL+C to quit." echo # Get terminal height (number of rows). local term_height term_height=$(tput lines) # We want to leave some rows for the title/instructions at top, and maybe 1 # row of margin at the bottom. title+instructions so far = 3 lines, so: local menu_start=4 local max_items=$(( term_height - menu_start - 2 )) if (( max_items < 1 )); then # In case the terminal is very small. max_items=1 fi # Make sure highlightIndex is in range. if (( highlightIndex < 0 )); then highlightIndex=0 elif (( highlightIndex >= count )); then highlightIndex=$(( count - 1 )) fi # Center the highlight in the menu if possible. # First guess: start printing from highlightIndex - half the window. local half=$(( max_items / 2 )) local start=$(( highlightIndex - half )) if (( start < 0 )); then start=0 fi local end=$(( start + max_items - 1 )) if (( end >= count )); then end=$(( count - 1 )) fi # If highlightIndex is below start or above end, adjust so it's visible. if (( highlightIndex < start )); then start=$highlightIndex elif (( highlightIndex > end )); then start=$(( highlightIndex - max_items + 1 )) fi if (( start < 0 )); then start=0 fi end=$(( start + max_items - 1 )) if (( end >= count )); then end=$(( count - 1 )) fi # If there are items outside this window, show an indicator. if (( start > 0 )); then echo -e "${CLR_DIM} ↑ ... more above ...${CLR_RESET}" else echo fi # Print only items[start..end]. for (( i = start; i <= end; i++ )); do local line="${items[$i]}" if (( i == highlightIndex )); then # Highlighted. echo -e " ${CLR_YELLOW}▶ $line${CLR_RESET}" else # Normal or dim. if [[ "$line" == *"[No snapshots]"* ]]; then echo -e " ${CLR_DIM}$line${CLR_RESET}" else echo " $line" fi fi done if (( end < count - 1 )); then echo -e "${CLR_DIM} ↓ ... more below ...${CLR_RESET}" fi } # Locale selection menu. choose_locale() { local idx=0 while true; do # Build an array of menu lines. local lines=() for loc in "${locales_sorted[@]}"; do local total="${LOCALE_TOTAL_COUNT["$loc"]}" local fails="${LOCALE_FAILED_COUNT["$loc"]}" if [ "$total" -eq 0 ]; then lines+=("$loc [No snapshots]") else if [ "$fails" -gt 0 ]; then lines+=("$loc ($fails failed / $total total)") else lines+=("$loc (all passed / $total total)") fi fi done # Show the menu. print_menu "Locales with Snapshots" lines[@] "$idx" >&2 # Wait for a key. key="$(read_keypress)" case "$key" in up) # Decrement idx carefully. if [ "$idx" -le 0 ]; then idx=$(( ${#lines[@]} - 1 )) else idx=$((idx - 1)) fi ;; down) # Increment idx carefully. if [ "$idx" -ge $(( ${#lines[@]} - 1 )) ]; then idx=0 else idx=$((idx + 1)) fi ;; pgup) # Jump up by one page. idx=$(( idx - ( $(tput lines) - 5 ) )) if (( idx < 0 )); then idx=0 fi ;; pgdn) # Jump down by one page. idx=$(( idx + ( $(tput lines) - 5 ) )) if (( idx >= ${#lines[@]} )); then idx=$(( ${#lines[@]} - 1 )) fi ;; enter) local line="${lines[$idx]}" # If it's "[No snapshots]", ignore selection. if [[ "$line" == *"[No snapshots]"* ]]; then continue fi # Otherwise proceed. echo "${locales_sorted[$idx]}" return 0 ;; esc|backspace|ctrl_c) # Return empty to signal user wants to quit. echo "" return 0 ;; esac done } # Snapshot selection menu. choose_snapshot() { local loc="$1" # Reconstruct the snapshot base names. IFS=' ' read -r -a bases <<< "${LOCALE_SNAPSHOTS[$loc]}" local idx=0 while true; do # Build lines[] for display. local lines=() for base in "${bases[@]}"; do local expected="${SNAPSHOT_DIR}/${loc}/${base}.html" local actual="${SNAPSHOT_DIR}/${loc}/${base}.error.html" # URL Decode any %2F, %3A, etc. for display. base="$(echo -e "${base//%/\\x}")" if [ -f "$actual" ]; then lines+=("❌ $base") else lines+=("✅ $base") fi done print_menu "Snapshots in locale '$loc'" lines[@] "$idx" >&2 key="$(read_keypress)" case "$key" in up) if [ "$idx" -le 0 ]; then idx=$(( ${#lines[@]} - 1 )) else idx=$((idx - 1)) fi ;; down) if [ "$idx" -ge $(( ${#lines[@]} - 1 )) ]; then idx=0 else idx=$((idx + 1)) fi ;; pgup) idx=$(( idx - ( $(tput lines) - 5 ) )) if (( idx < 0 )); then idx=0 fi ;; pgdn) idx=$(( idx + ( $(tput lines) - 5 ) )) if (( idx >= ${#lines[@]} )); then idx=$(( ${#lines[@]} - 1 )) fi ;; enter) local base="${bases[$idx]}" local expected="${SNAPSHOT_DIR}/${loc}/${base}.html" local actual="${SNAPSHOT_DIR}/${loc}/${base}.error.html" # URL Decode any %2F, %3A, etc. for display. base="$(echo -e "${base//%/\\x}")" if [ ! -f "$actual" ]; then # Passed snapshot. clear echo -e "${CLR_GREEN}Snapshot '$base' PASSED. No .error.html to compare.${CLR_RESET}" >&2 echo "Press any key to go back..." >&2 read -rsn1 else # Compare with git diff. clear echo "Running diff on '$expected' vs '$actual'..." >&2 tput cnorm git diff --no-index --color=always --color-words "$expected" "$actual" | less -R || true # Hide cursor again on return. tput civis fi ;; esc|backspace|ctrl_c) return 0 ;; esac done } # Main interactive flow # If we're only showing snapshots for a single locale, skip the locale menu. if [ "$for_single_locale" = true ]; then choose_snapshot "${locale_dirs[0]##*/}" # Exit if user pressed ESC/back at the snapshot menu. clear exit 0 fi # Otherwise, show the locale menu first. while true; do chosen_locale="$(choose_locale)" if [ -z "$chosen_locale" ]; then # Exit if user pressed ESC/back at the locale menu. clear exit 0 fi choose_snapshot "$chosen_locale" done