| 1 | #!/bin/sh |
| 2 | # ===================================== |
| 3 | # Shared test harness for builtin tests |
| 4 | # ===================================== |
| 5 | # Source this file from individual builtin test scripts. |
| 6 | # Each test file must set TEST_PREFIX before sourcing. |
| 7 | |
| 8 | # Portable timeout: macOS has gtimeout (from coreutils) or no timeout |
| 9 | if command -v timeout >/dev/null 2>&1; then |
| 10 | run_with_timeout() { timeout "$@"; } |
| 11 | elif command -v gtimeout >/dev/null 2>&1; then |
| 12 | run_with_timeout() { gtimeout "$@"; } |
| 13 | else |
| 14 | run_with_timeout() { |
| 15 | _rwt_secs="$1"; shift |
| 16 | _rwt_tmp=$(mktemp /tmp/fortsh_timeout.XXXXXX) |
| 17 | ( "$@" ) > "$_rwt_tmp" 2>&1 & |
| 18 | _rwt_pid=$! |
| 19 | ( sleep "$_rwt_secs"; kill "$_rwt_pid" 2>/dev/null ) & |
| 20 | _rwt_wd=$! |
| 21 | wait "$_rwt_pid" 2>/dev/null |
| 22 | _rwt_rc=$? |
| 23 | kill "$_rwt_wd" 2>/dev/null |
| 24 | wait "$_rwt_wd" 2>/dev/null |
| 25 | cat "$_rwt_tmp" |
| 26 | rm -f "$_rwt_tmp" |
| 27 | return $_rwt_rc |
| 28 | } |
| 29 | fi |
| 30 | |
| 31 | # Colors (POSIX-compliant way) |
| 32 | RED='\033[0;31m' |
| 33 | GREEN='\033[0;32m' |
| 34 | YELLOW='\033[1;33m' |
| 35 | BLUE='\033[0;34m' |
| 36 | NC='\033[0m' |
| 37 | |
| 38 | CURRENT_SECTION="" |
| 39 | TEST_NUM=0 |
| 40 | |
| 41 | PASSED=0 |
| 42 | FAILED=0 |
| 43 | SKIPPED=0 |
| 44 | FAILED_TESTS_LIST="" |
| 45 | |
| 46 | # Get script directory (POSIX way) |
| 47 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) |
| 48 | FORTSH_BIN="${FORTSH_BIN:-$SCRIPT_DIR/../../bin/fortsh}" |
| 49 | |
| 50 | # Reference bash for expected output — must be bash 4+ for assoc arrays, case transforms |
| 51 | # macOS ships bash 3.2 which lacks these; override with BASH_REF=/opt/homebrew/bin/bash |
| 52 | BASH_REF="${BASH_REF:-bash}" |
| 53 | |
| 54 | # Check if fortsh exists |
| 55 | if [ ! -x "$FORTSH_BIN" ]; then |
| 56 | printf "${RED}ERROR${NC}: fortsh binary not found at $FORTSH_BIN\n" |
| 57 | printf "Please run 'make' first or set FORTSH_BIN environment variable\n" |
| 58 | exit 1 |
| 59 | fi |
| 60 | |
| 61 | # Temp directory for test files |
| 62 | TEST_TMPDIR=$(mktemp -d) |
| 63 | cleanup() { rm -rf "$TEST_TMPDIR" 2>/dev/null; } |
| 64 | trap cleanup EXIT INT TERM |
| 65 | |
| 66 | pass() { |
| 67 | TEST_NUM=$((TEST_NUM + 1)) |
| 68 | printf "${GREEN}✓ PASS${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1" |
| 69 | PASSED=$((PASSED + 1)) |
| 70 | } |
| 71 | |
| 72 | fail() { |
| 73 | TEST_NUM=$((TEST_NUM + 1)) |
| 74 | TEST_ID="${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}" |
| 75 | printf "${RED}✗ FAIL${NC} ${TEST_ID}: %s\n" "$1" |
| 76 | FAILED_TESTS_LIST="${FAILED_TESTS_LIST} ${TEST_ID}: $1\n" |
| 77 | if [ -n "$2" ]; then printf " expected: %s\n" "$2"; fi |
| 78 | if [ -n "$3" ]; then printf " got: %s\n" "$3"; fi |
| 79 | FAILED=$((FAILED + 1)) |
| 80 | } |
| 81 | |
| 82 | skip() { |
| 83 | TEST_NUM=$((TEST_NUM + 1)) |
| 84 | printf "${YELLOW}⊘ SKIP${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s - %s\n" "$1" "$2" |
| 85 | SKIPPED=$((SKIPPED + 1)) |
| 86 | } |
| 87 | |
| 88 | section() { |
| 89 | CURRENT_SECTION=$(echo "$1" | grep -oE '^[0-9]+' || echo "0") |
| 90 | TEST_NUM=0 |
| 91 | printf "\n${BLUE}==========================================\n%s\n==========================================${NC}\n" "$1" |
| 92 | } |
| 93 | |
| 94 | # Normalize shell name prefixes in error messages for comparison |
| 95 | normalize_shell_name() { |
| 96 | printf '%s\n' "$1" | sed -e 's/bash: line [0-9]*: /SHELL: /g' \ |
| 97 | -e 's/fortsh: line [0-9]*: /SHELL: /g' \ |
| 98 | -e 's/bash: /SHELL: /g' \ |
| 99 | -e 's/fortsh: /SHELL: /g' |
| 100 | } |
| 101 | |
| 102 | # Per-test timeout (seconds) — prevents hanging tests from blocking CI |
| 103 | TEST_TIMEOUT="${TEST_TIMEOUT:-10}" |
| 104 | |
| 105 | # Helper: compare output against bash |
| 106 | compare_output() { |
| 107 | test_name="$1"; command="$2" |
| 108 | expected=$(run_with_timeout "$TEST_TIMEOUT" "$BASH_REF" -c "$command" 2>&1) |
| 109 | actual=$(run_with_timeout "$TEST_TIMEOUT" "$FORTSH_BIN" -c "$command" 2>&1) |
| 110 | norm_expected=$(normalize_shell_name "$expected") |
| 111 | norm_actual=$(normalize_shell_name "$actual") |
| 112 | if [ "$norm_expected" = "$norm_actual" ]; then pass "$test_name" |
| 113 | else fail "$test_name" "$expected" "$actual"; fi |
| 114 | } |
| 115 | |
| 116 | # Helper: compare exit code only |
| 117 | compare_exit() { |
| 118 | test_name="$1"; command="$2" |
| 119 | run_with_timeout "$TEST_TIMEOUT" "$BASH_REF" -c "$command" >/dev/null 2>&1; expected=$? |
| 120 | run_with_timeout "$TEST_TIMEOUT" "$FORTSH_BIN" -c "$command" >/dev/null 2>&1; actual=$? |
| 121 | if [ "$expected" = "$actual" ]; then pass "$test_name" |
| 122 | else fail "$test_name" "exit $expected" "exit $actual"; fi |
| 123 | } |
| 124 | |
| 125 | # Helper: compare both output and exit code |
| 126 | compare_both() { |
| 127 | test_name="$1"; command="$2" |
| 128 | expected_out=$(run_with_timeout "$TEST_TIMEOUT" "$BASH_REF" -c "$command" 2>&1); expected_exit=$? |
| 129 | actual_out=$(run_with_timeout "$TEST_TIMEOUT" "$FORTSH_BIN" -c "$command" 2>&1); actual_exit=$? |
| 130 | norm_expected=$(normalize_shell_name "$expected_out") |
| 131 | norm_actual=$(normalize_shell_name "$actual_out") |
| 132 | if [ "$norm_expected" = "$norm_actual" ] && [ "$expected_exit" = "$actual_exit" ]; then |
| 133 | pass "$test_name" |
| 134 | else |
| 135 | fail "$test_name" "out='$expected_out' exit=$expected_exit" "out='$actual_out' exit=$actual_exit" |
| 136 | fi |
| 137 | } |
| 138 | |
| 139 | # Helper: check fortsh output matches expected string |
| 140 | check_output() { |
| 141 | test_name="$1"; command="$2"; expected="$3" |
| 142 | actual=$(timeout "$TEST_TIMEOUT" "$FORTSH_BIN" -c "$command" 2>&1) |
| 143 | if [ "$actual" = "$expected" ]; then pass "$test_name" |
| 144 | else fail "$test_name" "$expected" "$actual"; fi |
| 145 | } |
| 146 | |
| 147 | # Helper: check fortsh exit code matches expected |
| 148 | check_exit() { |
| 149 | test_name="$1"; command="$2"; expected="$3" |
| 150 | timeout "$TEST_TIMEOUT" "$FORTSH_BIN" -c "$command" >/dev/null 2>&1; actual=$? |
| 151 | if [ "$actual" = "$expected" ]; then pass "$test_name" |
| 152 | else fail "$test_name" "exit $expected" "exit $actual"; fi |
| 153 | } |
| 154 | |
| 155 | # Print summary — call at end of each test file |
| 156 | print_summary() { |
| 157 | printf "\n==========================================\n" |
| 158 | printf "%s TEST RESULTS\n" "$TEST_PREFIX" |
| 159 | printf "==========================================\n" |
| 160 | printf "Passed: %d\n" "$PASSED" |
| 161 | printf "Failed: %d\n" "$FAILED" |
| 162 | printf "Skipped: %d\n" "$SKIPPED" |
| 163 | printf "Total: %d\n" "$((PASSED + FAILED + SKIPPED))" |
| 164 | printf "==========================================\n" |
| 165 | |
| 166 | if [ $((PASSED + FAILED)) -gt 0 ]; then |
| 167 | PASS_RATE=$((PASSED * 100 / (PASSED + FAILED))) |
| 168 | printf "Pass rate: %d%%\n" "$PASS_RATE" |
| 169 | fi |
| 170 | |
| 171 | if [ "$FAILED" -gt 0 ]; then |
| 172 | printf "\nFailed tests:\n" |
| 173 | printf "%b" "$FAILED_TESTS_LIST" |
| 174 | printf "==========================================\n" |
| 175 | fi |
| 176 | |
| 177 | if [ "$FAILED" -eq 0 ]; then |
| 178 | printf "ALL %s TESTS PASSED!\n" "$TEST_PREFIX" |
| 179 | exit 0 |
| 180 | else |
| 181 | exit 1 |
| 182 | fi |
| 183 | } |