Skip to content

Commit eb94419

Browse files
yasinBursaliclaude
andcommitted
test: BATS coverage for supporting scripts (functional resilience + sr_resolve)
test-functional-resilience.bats pins dream-test-functional.sh set -e resilience: summary emission even under all-fail, sentinel delivery, bounded set +e/-e around test functions (#428). test-sr-resolve.bats pins sr_resolve 8-case matrix including the dream- prefix strip added post-PR-10 (#430). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9ec161b commit eb94419

2 files changed

Lines changed: 333 additions & 0 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#!/usr/bin/env bats
2+
# ============================================================================
3+
# BATS tests for sr_resolve in lib/service-registry.sh.
4+
# ============================================================================
5+
# Guards against re-breakage of the `dream-<id>` prefix-strip added in
6+
# PR #406 (container names pasted from `docker ps` should resolve to their
7+
# service IDs), and of the broader alias-resolution contract.
8+
#
9+
# 8-case matrix:
10+
# 1. Exact ID → resolves to the same ID
11+
# 2. Known alias → resolves to canonical ID
12+
# 3. `dream-<id>` prefix → strips prefix, resolves to ID
13+
# 4. `dream-<alias>` prefix → strips prefix, resolves through alias
14+
# 5. Unknown `dream-*` → passes through as-is (not our container)
15+
# 6. Unknown non-dream → passes through as-is (best-effort)
16+
# 7. Empty input → passes through empty
17+
# 8. Container that happens to start with `dream-` but isn't our
18+
# convention → passes through as-is (no alias match after strip)
19+
#
20+
# We invoke the registry inside a fresh `bash -c` subshell per test because
21+
# `declare -A` at the top of service-registry.sh creates function-local
22+
# arrays when the file is sourced from within bats' setup() (which is a
23+
# shell function). A subshell sources at top level, populating the globals
24+
# sr_resolve then actually reads.
25+
26+
load '../bats/bats-support/load'
27+
load '../bats/bats-assert/load'
28+
29+
setup() {
30+
export TMPDIR_TEST="$BATS_TEST_TMPDIR"
31+
export FIXTURE_DIR="$TMPDIR_TEST/fixture"
32+
export REGISTRY_PATH="$BATS_TEST_DIRNAME/../../lib/service-registry.sh"
33+
34+
# Fake EXTENSIONS_DIR so sr_load's Python loader has something to read.
35+
mkdir -p "$FIXTURE_DIR/extensions/services"
36+
37+
# Service A — id "alpha", aliases [a, al]
38+
mkdir -p "$FIXTURE_DIR/extensions/services/alpha"
39+
cat > "$FIXTURE_DIR/extensions/services/alpha/manifest.yaml" <<'YAML'
40+
schema_version: dream.services.v1
41+
service:
42+
id: alpha
43+
name: Alpha Service
44+
aliases: [a, al]
45+
container_name: dream-alpha
46+
category: core
47+
YAML
48+
49+
# Service B — id "bravo", aliases [b]
50+
mkdir -p "$FIXTURE_DIR/extensions/services/bravo"
51+
cat > "$FIXTURE_DIR/extensions/services/bravo/manifest.yaml" <<'YAML'
52+
schema_version: dream.services.v1
53+
service:
54+
id: bravo
55+
name: Bravo Service
56+
aliases: [b]
57+
container_name: dream-bravo
58+
category: recommended
59+
YAML
60+
61+
command -v python3 >/dev/null 2>&1 || skip "python3 not available"
62+
python3 -c "import yaml" 2>/dev/null || skip "PyYAML not available"
63+
}
64+
65+
# Resolve `$1` using a freshly-loaded registry against the fixture.
66+
# Runs in a subshell so `declare -A` lines in service-registry.sh create
67+
# true globals (not function-locals under bats' setup()).
68+
# stderr is suppressed so we only assert on stdout — empty input legitimately
69+
# triggers "bad array index" diagnostics from bash for SERVICE_ALIASES[""]
70+
# which are not part of the contract we're pinning.
71+
_sr() {
72+
bash -c '
73+
SCRIPT_DIR="'"$FIXTURE_DIR"'"
74+
export SCRIPT_DIR
75+
. "'"$REGISTRY_PATH"'"
76+
sr_resolve "$1" 2>/dev/null
77+
' _ "$1"
78+
}
79+
80+
# ── the 8-case matrix ───────────────────────────────────────────────────────
81+
82+
@test "sr_resolve: case 1 — exact ID resolves to same ID" {
83+
run _sr "alpha"
84+
assert_success
85+
assert_output "alpha"
86+
}
87+
88+
@test "sr_resolve: case 2 — known alias resolves to canonical ID" {
89+
run _sr "a"
90+
assert_success
91+
assert_output "alpha"
92+
93+
run _sr "al"
94+
assert_success
95+
assert_output "alpha"
96+
97+
run _sr "b"
98+
assert_success
99+
assert_output "bravo"
100+
}
101+
102+
@test "sr_resolve: case 3 — dream-<id> prefix strips and resolves" {
103+
# This is the PR-10 / #406 regression point: users copy container names
104+
# from `docker ps` (e.g. `dream-alpha`) and expect `dream restart` to
105+
# accept them.
106+
run _sr "dream-alpha"
107+
assert_success
108+
assert_output "alpha"
109+
}
110+
111+
@test "sr_resolve: case 4 — dream-<alias> prefix strips and resolves through alias" {
112+
run _sr "dream-a"
113+
assert_success
114+
assert_output "alpha"
115+
116+
run _sr "dream-b"
117+
assert_success
118+
assert_output "bravo"
119+
}
120+
121+
@test "sr_resolve: case 5 — unknown dream-* passes through as-is" {
122+
# Container that starts with `dream-` but whose stripped form isn't a
123+
# known alias: return the input verbatim (best-effort; compose will
124+
# fail later with a clear error).
125+
run _sr "dream-unknown-service"
126+
assert_success
127+
assert_output "dream-unknown-service"
128+
}
129+
130+
@test "sr_resolve: case 6 — unknown non-dream passes through as-is" {
131+
run _sr "not-a-service"
132+
assert_success
133+
assert_output "not-a-service"
134+
}
135+
136+
@test "sr_resolve: case 7 — empty input returns empty" {
137+
run _sr ""
138+
assert_success
139+
# Empty input hits SERVICE_ALIASES[""] (unset) and falls back to echoing
140+
# the input (also empty). No crash, no stderr.
141+
assert_output ""
142+
}
143+
144+
@test "sr_resolve: case 8 — dream-* where strip doesn't match any alias passes through" {
145+
# Container name-like string that isn't from our extensions and whose
146+
# stripped form doesn't collide with any known alias.
147+
run _sr "dream-some-other-project-container"
148+
assert_success
149+
assert_output "dream-some-other-project-container"
150+
}
151+
152+
# ── additional correctness guards ───────────────────────────────────────────
153+
154+
@test "sr_resolve: does NOT strip dream- when the full input is already a known alias" {
155+
# The resolver checks SERVICE_ALIASES[input] first, only stripping if
156+
# missing. We prove the ordering holds by asserting plain `bravo` still
157+
# resolves even though `dream-bravo` is its container name.
158+
run _sr "bravo"
159+
assert_success
160+
assert_output "bravo"
161+
}
162+
163+
@test "sr_resolve: idempotent — resolving twice yields same result" {
164+
run _sr "dream-a"
165+
assert_success
166+
[ "$output" = "alpha" ]
167+
168+
run _sr "$output"
169+
assert_success
170+
[ "$output" = "alpha" ]
171+
}

0 commit comments

Comments
 (0)