Skip to content

Commit afb3819

Browse files
ci: wire SUITE_TEST_INDEX env var for RED-zone tolerance (e7e7-38ee) (merge worktree-20260326-140938)
2 parents 6422f52 + 965357b commit afb3819

File tree

9 files changed

+900
-137
lines changed

9 files changed

+900
-137
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ jobs:
7070
name: Hook Tests
7171
runs-on: ubuntu-latest
7272
timeout-minutes: 10
73+
env:
74+
SUITE_TEST_INDEX: ${{ github.workspace }}/.test-index
7375
steps:
7476
- uses: actions/checkout@v4
7577

@@ -84,6 +86,8 @@ jobs:
8486
name: Script Tests
8587
runs-on: ubuntu-latest
8688
timeout-minutes: 15
89+
env:
90+
SUITE_TEST_INDEX: ${{ github.workspace }}/.test-index
8791
steps:
8892
- uses: actions/checkout@v4
8993

.test-index

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ plugins/dso/hooks/dispatchers/pre-bash.sh:tests/hooks/test-pre-bash-dispatcher.s
4141
plugins/dso/hooks/dispatchers/pre-edit.sh:tests/hooks/test-pre-edit-write-dispatcher.sh
4242
plugins/dso/hooks/dispatchers/pre-write.sh:tests/hooks/test-pre-edit-write-dispatcher.sh
4343
plugins/dso/hooks/lib/pre-bash-functions.sh:tests/hooks/test-commit-tracker.sh
44+
plugins/dso/hooks/lib/red-zone.sh:tests/hooks/test-red-zone.sh
4445
plugins/dso/hooks/compute-diff-hash.sh:tests/hooks/test-compute-diff-hash-tickets-tracker.sh
4546
plugins/dso/hooks/lib/review-gate-allowlist.conf:tests/hooks/test-review-gate-allowlist.sh,tests/hooks/test-compute-diff-hash-tickets-tracker.sh
4647
plugins/dso/scripts/check-local-env.sh:tests/scripts/test-check-local-env-portability.sh,tests/scripts/test-check-local-env-generic.sh
@@ -181,5 +182,6 @@ plugins/dso/scripts/purge-non-project-tickets.sh: tests/scripts/test-purge-non-p
181182
plugins/dso/scripts/runners/bash-runner.sh: tests/scripts/test-bash-runner-discovery.sh
182183
plugins/dso/scripts/test-batched.sh: tests/scripts/test-batched-state-integrity.sh, tests/scripts/test-bash-runner-discovery.sh
183184
plugins/dso/hooks/fix-bug-skill-directive.sh: tests/hooks/test-fix-bug-skill-directive.sh
185+
tests/lib/suite-engine.sh: tests/hooks/test-suite-engine-red-tolerance.sh
184186
plugins/dso/skills/debug-everything/prompts/fix-task-tdd.md: tests/skills/test_deprecated_fix_task_anti_coverup.py
185187
plugins/dso/skills/debug-everything/prompts/fix-task-mechanical.md: tests/skills/test_deprecated_fix_task_anti_coverup.py

plugins/dso/hooks/lib/red-zone.sh

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#!/usr/bin/env bash
2+
# plugins/dso/hooks/lib/red-zone.sh
3+
# Shared RED zone helpers for record-test-status.sh and test infrastructure.
4+
#
5+
# Usage (source into scripts):
6+
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/red-zone.sh"
7+
#
8+
# Provides:
9+
# get_red_zone_line_number(test_file, marker_name)
10+
# → line number of marker in test file, or -1 if not found
11+
# parse_failing_tests_from_output(output_file)
12+
# → one failing test name per line on stdout
13+
# get_test_line_number(test_file, test_name)
14+
# → line number of test function in test file, or -1 if not found
15+
# read_red_markers_by_test_file(assoc_array_name)
16+
# → populates caller's associative array: test_file_path → marker_name
17+
# (reads all entries from $REPO_ROOT/.test-index)
18+
#
19+
# Environment:
20+
# REPO_ROOT — repo root directory (defaults to ".")
21+
#
22+
# Extracted from plugins/dso/hooks/record-test-status.sh.
23+
24+
# ── RED zone helpers ──────────────────────────────────────────────────────────
25+
26+
# get_red_zone_line_number: find line number of marker in a test file.
27+
# For Python: matches 'def marker_name' or 'def marker_name('
28+
# For Bash: matches 'marker_name()' or 'marker_name (' or '# marker_name' pattern
29+
# For plain text / other: matches any line containing the marker name
30+
# Returns the line number on stdout, or -1 if not found.
31+
# Emits WARNING to stderr if marker provided but not found.
32+
get_red_zone_line_number() {
33+
local test_file="$1"
34+
local marker_name="$2"
35+
local repo_root="${REPO_ROOT:-.}"
36+
local full_path="${repo_root}/${test_file}"
37+
38+
if [[ ! -f "$full_path" ]]; then
39+
echo "-1"
40+
return 0
41+
fi
42+
43+
local line_num=0
44+
local found_line=-1
45+
# Word-boundary pattern: marker_name not adjacent to other identifier chars [a-zA-Z0-9_-]
46+
# Hyphens are included so that searching for 'test-foo' does not match 'test-foo-bar',
47+
# and searching for 'test' does not accidentally match 'test-foo'.
48+
local pat_word_boundary="(^|[^a-zA-Z0-9_-])${marker_name}([^a-zA-Z0-9_-]|\$)"
49+
while IFS= read -r line || [[ -n "$line" ]]; do
50+
(( line_num++ )) || true
51+
# Skip pure comment lines (lines starting with optional whitespace then #)
52+
# to avoid false positives from comment-only mentions
53+
[[ "$line" =~ ^[[:space:]]*# ]] && continue
54+
# Match marker_name as a word (not adjacent to other identifier chars)
55+
if [[ "$line" =~ $pat_word_boundary ]]; then
56+
found_line=$line_num
57+
break
58+
fi
59+
done < "$full_path"
60+
61+
if [[ $found_line -eq -1 ]]; then
62+
echo "WARNING: RED marker '${marker_name}' not found in test file: ${test_file}" >&2
63+
fi
64+
echo "$found_line"
65+
}
66+
67+
# parse_failing_tests_from_output: extract failing test names from test runner output.
68+
# Supports:
69+
# - Bash-style: "test_name: FAIL..." lines
70+
# - Pytest FAILED lines: "FAILED path/to/test.py::test_name"
71+
# Returns one test name per line on stdout.
72+
parse_failing_tests_from_output() {
73+
local output_file="$1"
74+
75+
if [[ ! -f "$output_file" ]]; then
76+
return 0
77+
fi
78+
79+
# Bash-style: "test_name: FAIL" (test_name is word chars + underscores/hyphens)
80+
grep -oE '^[a-zA-Z_][a-zA-Z0-9_-]*[[:space:]]*:[[:space:]]*FAIL' "$output_file" \
81+
| sed 's/[[:space:]]*:[[:space:]]*FAIL//' \
82+
|| true
83+
84+
# Bash-style (assert_pass_if_clean): "FAIL: test_name" on stderr merged into output
85+
grep -oE '^FAIL: [a-zA-Z_][a-zA-Z0-9_-]*' "$output_file" \
86+
| sed 's/^FAIL: //' \
87+
|| true
88+
89+
# Pytest-style: "FAILED path/to/test.py::test_name"
90+
grep -oE '^FAILED [^[:space:]]+::[a-zA-Z_][a-zA-Z0-9_]*' "$output_file" \
91+
| sed 's/^FAILED [^:]*:://' \
92+
|| true
93+
}
94+
95+
# parse_passing_tests_from_output: extract passing test names from test runner output.
96+
# Mirrors parse_failing_tests_from_output for the pass case.
97+
# Supports:
98+
# - Bash-style: "test_name ... PASS" or "test_name: PASS"
99+
# - Pytest PASSED lines: "PASSED path/to/test.py::test_name"
100+
# Returns one test name per line on stdout.
101+
parse_passing_tests_from_output() {
102+
local output_file="$1"
103+
104+
if [[ ! -f "$output_file" ]]; then
105+
return 0
106+
fi
107+
108+
# Bash-style: "test_name ... PASS" (with optional whitespace/dots between)
109+
grep -oE '^[a-zA-Z_][a-zA-Z0-9_-]*[[:space:]]*\.\.\..*PASS' "$output_file" \
110+
| sed 's/[[:space:]]*\.\.\..*PASS//' \
111+
|| true
112+
113+
# Bash-style: "test_name: PASS"
114+
grep -oE '^[a-zA-Z_][a-zA-Z0-9_-]*[[:space:]]*:[[:space:]]*PASS' "$output_file" \
115+
| sed 's/[[:space:]]*:[[:space:]]*PASS//' \
116+
|| true
117+
118+
# Pytest-style: "PASSED path/to/test.py::test_name"
119+
grep -oE '^PASSED [^[:space:]]+::[a-zA-Z_][a-zA-Z0-9_]*' "$output_file" \
120+
| sed 's/^PASSED [^:]*:://' \
121+
|| true
122+
}
123+
124+
# get_test_line_number: find the line number of a test function in a test file.
125+
# For Python: 'def test_name('
126+
# For Bash: 'test_name()' or any line containing test_name as a word
127+
# Returns -1 if not found.
128+
get_test_line_number() {
129+
local test_file="$1"
130+
local test_name="$2"
131+
local repo_root="${REPO_ROOT:-.}"
132+
local full_path="${repo_root}/${test_file}"
133+
134+
if [[ ! -f "$full_path" ]]; then
135+
echo "-1"
136+
return 0
137+
fi
138+
139+
local line_num=0
140+
# Word-boundary pattern: test_name not adjacent to other identifier chars [a-zA-Z0-9_-]
141+
# Hyphens are included so that searching for 'test-foo' does not match 'test-foo-bar',
142+
# and searching for 'test' does not accidentally match 'test-foo'.
143+
local pat_word_boundary="(^|[^a-zA-Z0-9_-])${test_name}([^a-zA-Z0-9_-]|\$)"
144+
while IFS= read -r line || [[ -n "$line" ]]; do
145+
(( line_num++ )) || true
146+
# Skip pure comment lines to avoid false positives
147+
[[ "$line" =~ ^[[:space:]]*# ]] && continue
148+
if [[ "$line" =~ $pat_word_boundary ]]; then
149+
echo "$line_num"
150+
return 0
151+
fi
152+
done < "$full_path"
153+
154+
echo "-1"
155+
}
156+
157+
# read_red_markers_by_test_file: scan $REPO_ROOT/.test-index and populate an
158+
# associative array mapping test_file_path → marker_name for all entries that
159+
# have a [marker] annotation.
160+
#
161+
# Usage:
162+
# declare -A my_map=()
163+
# REPO_ROOT="/path/to/repo" read_red_markers_by_test_file my_map
164+
#
165+
# Parameters:
166+
# $1 — name of caller's associative array (passed by name, populated via nameref)
167+
#
168+
# Semantics:
169+
# - Entries without a [marker] result in an empty-string value for that test file.
170+
# - When the same test file appears in multiple source entries, a non-empty marker
171+
# is never overwritten by an empty one (Bug A fix: mirrors record-test-status.sh logic).
172+
# - Comments (lines starting with #) and blank lines are skipped.
173+
# - Missing .test-index file silently produces an empty result.
174+
#
175+
# Environment:
176+
# REPO_ROOT — defaults to "."
177+
read_red_markers_by_test_file() {
178+
local -n _rrmbtf_map="$1"
179+
local repo_root="${REPO_ROOT:-.}"
180+
local index_file="${repo_root}/.test-index"
181+
182+
if [[ ! -f "$index_file" ]]; then
183+
return 0
184+
fi
185+
186+
while IFS= read -r line || [[ -n "$line" ]]; do
187+
# Skip comments and blank lines
188+
[[ -z "$line" ]] && continue
189+
[[ "$line" =~ ^[[:space:]]*# ]] && continue
190+
191+
# Split on first colon: left = source path, right = comma-separated test entries
192+
local right="${line#*:}"
193+
194+
# Parse each comma-separated test entry
195+
IFS=',' read -ra parts <<< "$right"
196+
for part in "${parts[@]}"; do
197+
# Trim leading/trailing whitespace
198+
part="${part#"${part%%[![:space:]]*}"}"
199+
part="${part%"${part##*[![:space:]]}"}"
200+
[[ -z "$part" ]] && continue
201+
202+
# Parse "test/path.ext [marker_name]" or just "test/path.ext"
203+
local parsed_path parsed_marker
204+
if [[ "$part" =~ ^(.*[^[:space:]])[[:space:]]+\[([^]]+)\]$ ]]; then
205+
parsed_path="${BASH_REMATCH[1]}"
206+
parsed_marker="${BASH_REMATCH[2]}"
207+
# Trim trailing whitespace from path
208+
parsed_path="${parsed_path%"${parsed_path##*[![:space:]]}"}"
209+
else
210+
parsed_path="$part"
211+
parsed_marker=""
212+
fi
213+
214+
# Bug A fix: non-empty marker must not be overwritten by an empty one.
215+
# Only overwrite if new marker is non-empty OR no entry exists yet.
216+
if [[ -n "$parsed_marker" ]] || [[ -z "${_rrmbtf_map[$parsed_path]:-}" ]]; then
217+
_rrmbtf_map["$parsed_path"]="$parsed_marker"
218+
fi
219+
done
220+
done < "$index_file"
221+
}

0 commit comments

Comments
 (0)