mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
240 lines
11 KiB
Bash
Executable File
240 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# suite-failure-report.sh — what a red suite's log has to say, and which logs
|
|
# to keep. The ONE implementation behind the parallel harness's end-of-run
|
|
# failure summary (scripts/run-tests-parallel.sh sources this file), so every
|
|
# venue that reaches the harness through scripts/test.sh prints the same thing:
|
|
# hosted CI, the Linux container leg and the Windows VM leg.
|
|
#
|
|
# Why it is a file of its own: tests/test_failure_report_contract.sh drives
|
|
# these functions with fabricated suite logs. Inline in the harness they could
|
|
# only be exercised by a real red run, which is how the gap below went unseen.
|
|
#
|
|
# The gap: the summary used to grep a failing suite's log for "FAIL" and then
|
|
# show its last 15 lines. A sanitizer report contains no "FAIL", and its useful
|
|
# part (error kind, access size, top frames) is at the START of the report,
|
|
# while the last 15 lines of an AddressSanitizer abort are the shadow-byte
|
|
# legend. A Windows sanitizer job therefore went red with an empty "every
|
|
# failure site" section and a legend, and the cause had to be found by reading
|
|
# code. A trap-mode UBSan build (Windows on ARM) is worse still: it dies on an
|
|
# illegal instruction and prints nothing at all, so the only evidence in the
|
|
# log is which test was running.
|
|
|
|
suite_failure_report_usage() {
|
|
cat <<'EOF'
|
|
Usage: scripts/suite-failure-report.sh summary <suite> <suite-log>
|
|
scripts/suite-failure-report.sh collect <log-dir> <results-file> <slice-file>
|
|
|
|
Internal helper of scripts/run-tests-parallel.sh (reached through
|
|
scripts/test.sh); the two modes exist so the contract test can drive the real
|
|
functions with fabricated logs.
|
|
|
|
summary Print the failure summary of one suite log:
|
|
1. every "FAIL" site with context (at most 120 lines);
|
|
2. the sanitizer / crash report, only when the log holds one:
|
|
the test that was running, the first sanitizer report up to
|
|
its own SUMMARY line (at most 40 lines), every further
|
|
"runtime error:" / sanitizer header / SUMMARY line (at most
|
|
10), and signal or timeout lines (at most 5);
|
|
3. the last 15 lines.
|
|
collect Copy the log of every suite of <slice-file> that did not record
|
|
rc=0 in <results-file> into <log-dir>/failed/, together with the
|
|
results file. Creates nothing when every suite is green. CI uploads
|
|
that directory when a test job fails.
|
|
|
|
Exit codes: 0 success · 2 usage.
|
|
EOF
|
|
}
|
|
|
|
# The sanitizer / crash section of the summary, header included. Prints
|
|
# NOTHING for a log that has no sanitizer report, no signal line and a
|
|
# completion summary, so an ordinary assertion failure reads exactly as it did
|
|
# before this section existed.
|
|
#
|
|
# awk rather than grep because three things have to be known together: where
|
|
# the first report starts, which test line precedes it, and where the report
|
|
# stops being useful. Portable awk only (BSD awk on macOS, mawk on Ubuntu,
|
|
# gawk under MSYS2): no interval expressions, no character classes, no gensub.
|
|
#
|
|
# The 40-line cap: a report normally ends before it, at its own SUMMARY line
|
|
# (error line, access line, the faulting stack, then what the address belongs
|
|
# to). The cap is for the report that does not end soon — a use-after-free
|
|
# carries three stacks, a recursion overflow hundreds of frames — and 40 lines
|
|
# keep the error line, the access line and the top of the faulting stack, which
|
|
# is the part that names the code. It is a third of the 120 lines the
|
|
# failure-site section may print, so one suite's whole summary stays under 200
|
|
# lines. A SUMMARY line beyond the cut is still printed, under "further
|
|
# sanitizer lines".
|
|
suite_crash_report() {
|
|
local suite="$1" log="$2"
|
|
[ -r "$log" ] || return 0
|
|
awk -v header="──── $suite: sanitizer / crash report ────" \
|
|
-v block_max=40 -v other_max=10 -v signal_max=5 '
|
|
# A test line is what RUN_TEST prints: two spaces, then the test name
|
|
# left-justified in 55 columns (" %-55s"), flushed BEFORE the test body
|
|
# runs. That flush is why the last test line ahead of a report names the
|
|
# test that was running: nothing the test itself buffers can precede it.
|
|
function test_name(s, head, rest) {
|
|
if (substr(s, 1, 2) != " ") return ""
|
|
head = substr(s, 3, 55)
|
|
if (length(head) < 55) return ""
|
|
if (head !~ /^[A-Za-z_][A-Za-z0-9_]* *$/) return ""
|
|
if (head ~ / $/) {
|
|
sub(/ +$/, "", head)
|
|
return head
|
|
}
|
|
# A name of 55 characters or more runs straight into what follows it.
|
|
rest = substr(s, 3)
|
|
match(rest, /^[A-Za-z_][A-Za-z0-9_]*/)
|
|
head = substr(rest, 1, RLENGTH)
|
|
sub(/(PASS|SKIP)$/, "", head)
|
|
return head
|
|
}
|
|
{
|
|
line = $0
|
|
sub(/\r$/, "", line) # Windows suite logs are CRLF
|
|
if (line ~ /^ [0-9]+ passed/) has_summary = 1
|
|
is_header = (line ~ /ERROR: (Address|Leak|Thread|Memory|UndefinedBehavior)Sanitizer/ || line ~ /WARNING: (Thread|Memory)Sanitizer/)
|
|
is_start = (is_header || line ~ /runtime error:/)
|
|
captured = 0
|
|
|
|
if (!started) {
|
|
name = test_name(line)
|
|
if (name != "") {
|
|
current = name
|
|
current_done = 0
|
|
}
|
|
# The start line never finishes a test: a diagnostic appended to
|
|
# the name line is printed while that test is still running.
|
|
if (!is_start && current != "" && (line ~ /PASS$/ || line ~ /SKIP \(/ || line ~ /FAIL /))
|
|
current_done = 1
|
|
}
|
|
|
|
if (is_start && !started) {
|
|
started = 1
|
|
# A recoverable UBSan diagnostic is one line plus, at most, its
|
|
# stack; every other report runs to its own SUMMARY line.
|
|
ubsan_line = !is_header
|
|
block[++block_count] = line
|
|
captured = 1
|
|
} else if (started && !closed) {
|
|
if (ubsan_line) {
|
|
if (line ~ /^[ \t]+#[0-9]+ / || line ~ /SUMMARY: UndefinedBehaviorSanitizer/) {
|
|
block[++block_count] = line
|
|
captured = 1
|
|
} else {
|
|
closed = 1
|
|
}
|
|
} else if (line ~ /^Shadow bytes around/ || line ~ /==ABORTING/) {
|
|
closed = 1 # the shadow dump and legend explain nothing
|
|
} else {
|
|
block[++block_count] = line
|
|
captured = 1
|
|
if (line ~ /SUMMARY: [A-Za-z]*Sanitizer/) closed = 1
|
|
}
|
|
if (!closed && block_count >= block_max) {
|
|
closed = 1
|
|
cut = 1
|
|
}
|
|
}
|
|
|
|
if (!captured) {
|
|
if (is_start || line ~ /SUMMARY: [A-Za-z]*Sanitizer/) {
|
|
other_total++
|
|
if (other_total <= other_max) other[other_total] = line
|
|
} else if (line ~ /Segmentation fault|Abort trap|killed by signal|timed out/ && line !~ /FAIL/) {
|
|
# FAIL lines are already shown by the failure-site section.
|
|
signal_total++
|
|
if (signal_total <= signal_max) signals[signal_total] = line
|
|
}
|
|
}
|
|
}
|
|
END {
|
|
died_silently = (!started && !has_summary && current != "")
|
|
if (!started && other_total == 0 && signal_total == 0 && !died_silently) exit 0
|
|
print header
|
|
if (started && current == "")
|
|
print "running test: none (the report precedes the first test line)"
|
|
else if (started && current_done)
|
|
print "last finished test: " current " (the report comes after it)"
|
|
else if (started)
|
|
print "running test: " current
|
|
else if (died_silently && current_done)
|
|
print "last finished test: " current " (the log ends without a completion summary)"
|
|
else if (died_silently)
|
|
print "running test: " current " (the log ends without a completion summary)"
|
|
for (i = 1; i <= block_count; i++) print block[i]
|
|
if (cut) print "[report cut at " block_max " lines; the suite log has the rest]"
|
|
if (other_total > 0) {
|
|
print "further sanitizer lines:"
|
|
for (i = 1; i <= other_total && i <= other_max; i++) print other[i]
|
|
if (other_total > other_max) print "[" (other_total - other_max) " more not shown]"
|
|
}
|
|
if (signal_total > 0) {
|
|
print "signal / timeout lines:"
|
|
for (i = 1; i <= signal_total && i <= signal_max; i++) print signals[i]
|
|
if (signal_total > signal_max) print "[" (signal_total - signal_max) " more not shown]"
|
|
}
|
|
}' "$log"
|
|
}
|
|
|
|
# The failure summary of one suite. Sections 1 and 3 are byte-for-byte what
|
|
# the harness printed before; section 2 appears only when it has content.
|
|
suite_failure_summary() {
|
|
local suite="$1" log="$2"
|
|
echo "──── $suite: every failure site ────"
|
|
grep -B2 -A8 "FAIL" "$log" | head -120
|
|
suite_crash_report "$suite" "$log"
|
|
echo "──── $suite: last 15 lines ────"
|
|
tail -15 "$log"
|
|
return 0
|
|
}
|
|
|
|
# Keep the logs a red run needs: every suite of this shard's slice that did
|
|
# not record rc=0 — failed, crashed, timed out, or still in flight when the
|
|
# scheduler itself gave up (a log but no result line). One awk decides the
|
|
# set, so a green run costs a single process and creates no directory.
|
|
collect_failed_suite_logs() {
|
|
local logdir="$1" results="$2" slice="$3"
|
|
local dest="$logdir/failed" suite kept=0
|
|
# An existing directory only: an empty or wrong <log-dir> must never turn
|
|
# the rm below into a removal somewhere else.
|
|
[ -d "$logdir" ] || return 0
|
|
rm -rf "$dest"
|
|
if [ ! -f "$results" ] || [ ! -f "$slice" ]; then
|
|
return 0
|
|
fi
|
|
while IFS= read -r suite; do
|
|
[ -f "$logdir/$suite.log" ] || continue
|
|
mkdir -p "$dest" && cp "$logdir/$suite.log" "$dest/$suite.log" && kept=$((kept + 1))
|
|
done < <(awk 'FILENAME == ARGV[1] { if ($2 == "rc=0") green[$1] = 1; next }
|
|
NF && !($1 in green) { print $1 }' "$results" "$slice")
|
|
if [ "$kept" -gt 0 ]; then
|
|
cp "$results" "$dest/results.txt"
|
|
echo "failing-suite logs kept in $dest ($kept suite log(s) + results.txt)"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Executed (not sourced): the contract test's entry.
|
|
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
set -uo pipefail
|
|
case "${1:-}" in
|
|
-h | --help)
|
|
suite_failure_report_usage
|
|
exit 0
|
|
;;
|
|
summary)
|
|
[ $# -eq 3 ] || { echo "suite-failure-report: summary needs <suite> <suite-log>. Please consult --help." >&2; exit 2; }
|
|
suite_failure_summary "$2" "$3"
|
|
;;
|
|
collect)
|
|
[ $# -eq 4 ] || { echo "suite-failure-report: collect needs <log-dir> <results-file> <slice-file>. Please consult --help." >&2; exit 2; }
|
|
collect_failed_suite_logs "$2" "$3" "$4"
|
|
;;
|
|
*)
|
|
echo "suite-failure-report: unknown mode '${1:-}'. Please consult --help." >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
fi
|