|
| 1 | +#!/usr/bin/env bats |
| 2 | +# ============================================================================ |
| 3 | +# BATS tests for scripts/dream-test-functional.sh set -e resilience. |
| 4 | +# ============================================================================ |
| 5 | +# Guards against re-breakage of PR #428: |
| 6 | +# - Arithmetic expansion (TESTS_FAILED=$((TESTS_FAILED + 1))) must not |
| 7 | +# trip `set -e` on the first increment when the counter is 0. |
| 8 | +# - The summary line and the final exit code must be emitted even when |
| 9 | +# every underlying functional test fails. |
| 10 | +# - `set +e / -e` bounded around the test-function dispatch block lets |
| 11 | +# all tests run to completion before the summary. |
| 12 | +# |
| 13 | +# Note: the sentinel `__DREAM_RESULT__` is emitted by the Python streaming |
| 14 | +# endpoint (routers/setup.py), not by this shell script directly. The shell |
| 15 | +# just needs to exit with the right code so the endpoint can report it. |
| 16 | +# Sentinel-delivery itself is covered by PR-2F's Python tests. |
| 17 | + |
| 18 | +load '../bats/bats-support/load' |
| 19 | +load '../bats/bats-assert/load' |
| 20 | + |
| 21 | +setup() { |
| 22 | + export TMPDIR_TEST="$BATS_TEST_TMPDIR" |
| 23 | + export SCRIPT_SRC="$BATS_TEST_DIRNAME/../../scripts/dream-test-functional.sh" |
| 24 | +} |
| 25 | + |
| 26 | +# Build a patched copy of the script with test_*_functional() overridden. |
| 27 | +# Uses a single marker-insert to stick overrides just before the bounded |
| 28 | +# `set +e` dispatch block, so the rest of the script (strict mode, counters, |
| 29 | +# summary, exit logic) runs exactly as in production. |
| 30 | +_patch_script() { |
| 31 | + local mode="$1" # all-fail | all-pass | mixed |
| 32 | + local out="$TMPDIR_TEST/patched-${mode}.sh" |
| 33 | + local overrides_file="$TMPDIR_TEST/overrides-${mode}.sh" |
| 34 | + |
| 35 | + # Stub bodies for each mode. Every stub runs `pass` or `fail`, which |
| 36 | + # exist in the original script; those mutate the real counters. |
| 37 | + case "$mode" in |
| 38 | + all-fail) |
| 39 | + cat > "$overrides_file" <<'OV' |
| 40 | +test_llm_functional() { fail "LLM (stubbed)"; return 1; } |
| 41 | +test_tts_functional() { fail "TTS (stubbed)"; return 1; } |
| 42 | +test_embeddings_functional() { fail "Embeddings (stubbed)"; return 1; } |
| 43 | +test_whisper_functional() { fail "Whisper (stubbed)"; return 1; } |
| 44 | +OV |
| 45 | + ;; |
| 46 | + all-pass) |
| 47 | + cat > "$overrides_file" <<'OV' |
| 48 | +test_llm_functional() { pass "LLM (stubbed)"; } |
| 49 | +test_tts_functional() { pass "TTS (stubbed)"; } |
| 50 | +test_embeddings_functional() { pass "Embeddings (stubbed)"; } |
| 51 | +test_whisper_functional() { pass "Whisper (stubbed)"; } |
| 52 | +OV |
| 53 | + ;; |
| 54 | + mixed) |
| 55 | + cat > "$overrides_file" <<'OV' |
| 56 | +test_llm_functional() { pass "LLM (stubbed)"; } |
| 57 | +test_tts_functional() { fail "TTS (stubbed)"; return 1; } |
| 58 | +test_embeddings_functional() { pass "Embeddings (stubbed)"; } |
| 59 | +test_whisper_functional() { fail "Whisper (stubbed)"; return 1; } |
| 60 | +OV |
| 61 | + ;; |
| 62 | + esac |
| 63 | + |
| 64 | + # Insert overrides at the marker — the line immediately before the |
| 65 | + # bounded `set +e` dispatch block. BSD+GNU awk portable. |
| 66 | + awk -v ov_file="$overrides_file" ' |
| 67 | + BEGIN { |
| 68 | + while ((getline line < ov_file) > 0) overrides = overrides line "\n" |
| 69 | + close(ov_file) |
| 70 | + } |
| 71 | + /^# Each test returns 1 on failure/ && !inserted { |
| 72 | + printf "%s", overrides |
| 73 | + inserted = 1 |
| 74 | + } |
| 75 | + { print } |
| 76 | + ' "$SCRIPT_SRC" > "$out" |
| 77 | + |
| 78 | + # Neutralize the service-registry source block — it hard-depends on a |
| 79 | + # full install layout that does not exist in tmpdir. Strip surgically |
| 80 | + # by matching the opening `if [[ -f "$_FT_DIR/lib/service-registry.sh"` |
| 81 | + # to its closing `fi`. The `declare -A SERVICE_PORTS` line that follows |
| 82 | + # keeps the URL default-expansions safe. |
| 83 | + awk ' |
| 84 | + /^if \[\[ -f "\$_FT_DIR\/lib\/service-registry\.sh" \]\]; then/ { in_block = 1; next } |
| 85 | + in_block && /^fi$/ { in_block = 0; next } |
| 86 | + !in_block { print } |
| 87 | + ' "$out" > "$out.tmp" && mv "$out.tmp" "$out" |
| 88 | + |
| 89 | + chmod +x "$out" |
| 90 | + echo "$out" |
| 91 | +} |
| 92 | + |
| 93 | +# ── all-fail path — the core regression (PR #428) ─────────────────────────── |
| 94 | + |
| 95 | +@test "resilience: summary line prints even when every test fails" { |
| 96 | + local script |
| 97 | + script=$(_patch_script all-fail) |
| 98 | + run bash "$script" |
| 99 | + # Script must exit 1 on any failure. |
| 100 | + [ "$status" -eq 1 ] |
| 101 | + # Summary must still appear. |
| 102 | + assert_output --partial "Results: 0 passed, 4 failed" |
| 103 | + assert_output --partial "Some functional tests failed" |
| 104 | +} |
| 105 | + |
| 106 | +@test "resilience: first fail call does not trip set -e at counter=0" { |
| 107 | + # The critical regression this guards against: `((TESTS_FAILED++))` under |
| 108 | + # set -e aborts the script on the FIRST call because the pre-increment |
| 109 | + # value is 0 and compound arithmetic returns that as exit code. With the |
| 110 | + # PR #428 fix (`TESTS_FAILED=$((TESTS_FAILED+1))`), the assignment form |
| 111 | + # always returns 0. If the first fail aborts the script, we'd see "0 |
| 112 | + # passed, 1 failed" (only the first test ran). Assert we reached all 4. |
| 113 | + local script |
| 114 | + script=$(_patch_script all-fail) |
| 115 | + run bash "$script" |
| 116 | + assert_output --partial "4 failed" |
| 117 | +} |
| 118 | + |
| 119 | +# ── all-pass path ─────────────────────────────────────────────────────────── |
| 120 | + |
| 121 | +@test "resilience: all-pass exits 0 with full summary" { |
| 122 | + local script |
| 123 | + script=$(_patch_script all-pass) |
| 124 | + run bash "$script" |
| 125 | + assert_success |
| 126 | + assert_output --partial "Results: 4 passed, 0 failed" |
| 127 | + assert_output --partial "All functional tests passed" |
| 128 | +} |
| 129 | + |
| 130 | +# ── mixed path (regression guard for bounded set +e / -e) ─────────────────── |
| 131 | + |
| 132 | +@test "resilience: mixed pass/fail still runs every test and prints summary" { |
| 133 | + local script |
| 134 | + script=$(_patch_script mixed) |
| 135 | + run bash "$script" |
| 136 | + [ "$status" -eq 1 ] |
| 137 | + # All 4 test functions ran (2 pass, 2 fail). |
| 138 | + assert_output --partial "Results: 2 passed, 2 failed" |
| 139 | +} |
| 140 | + |
| 141 | +# ── static assertions on the resilience idioms in the script itself ───────── |
| 142 | + |
| 143 | +@test "resilience: script uses arithmetic-expansion assignment (not ((++)))" { |
| 144 | + # TESTS_FAILED=$((TESTS_FAILED+1)) — the set-e-safe form. |
| 145 | + run grep -E 'TESTS_FAILED=\$\(\(TESTS_FAILED[[:space:]]*\+' "$SCRIPT_SRC" |
| 146 | + assert_success |
| 147 | + # And must NOT contain the dangerous ((TESTS_FAILED++)) form. |
| 148 | + run grep -E '\(\(TESTS_FAILED\+\+\)\)' "$SCRIPT_SRC" |
| 149 | + assert_failure |
| 150 | +} |
| 151 | + |
| 152 | +@test "resilience: script has bounded 'set +e' / 'set -e' around test dispatch" { |
| 153 | + run grep -n "^set +e" "$SCRIPT_SRC" |
| 154 | + assert_success |
| 155 | + run grep -n "^set -e" "$SCRIPT_SRC" |
| 156 | + assert_success |
| 157 | +} |
| 158 | + |
| 159 | +@test "resilience: TESTS_PASSED also uses the set-e-safe assignment form" { |
| 160 | + run grep -E 'TESTS_PASSED=\$\(\(TESTS_PASSED[[:space:]]*\+' "$SCRIPT_SRC" |
| 161 | + assert_success |
| 162 | +} |
0 commit comments