[CI] (Test): annotate failed ctest entries with their failure lines, since the run logs need a signed-in reader

This commit is contained in:
Swung0x48
2026-10-04 07:20:04 -04:00
parent 32d27b1491
commit 2d42414667
2 changed files with 113 additions and 3 deletions
+31 -3
View File
@@ -413,6 +413,12 @@ jobs:
--discovery "${RUNNER_TEMP}/primary-inproc-lane.json" \
--junit "${RUNNER_TEMP}/primary-inproc.xml" --output "${RUNNER_TEMP}/primary-inproc-proof.json"
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
# with the lines of its output that say why, where anyone reading the run can see them.
- name: Annotate failed entries
if: failure()
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
- name: Upload integration lane evidence
if: always()
uses: actions/upload-artifact@v7
@@ -1169,6 +1175,12 @@ jobs:
# arm proof read; verify-split-logs/ is whatever the control steps left behind, which for
# twelve entries is a deliberately red re-run under the same name. Uploading only the
# second - which is what this step did before the copy above existed - loses the first.
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
# with the lines of its output that say why, where anyone reading the run can see them.
- name: Annotate failed entries
if: failure()
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
- name: Upload verify-split lane logs
if: always()
uses: actions/upload-artifact@v7
@@ -1481,7 +1493,7 @@ jobs:
working-directory: build-split
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-split.xml"
# P7 wave 2-F, PH-7 (1). THE TCP SUPERVISOR LANE NEEDS flatc, AND CI HAD NONE.
#
@@ -1648,6 +1660,12 @@ jobs:
CONTROL_TMPDIR: ${{ runner.temp }}
run: bash "${GITHUB_WORKSPACE}/scripts/ci/split_negative_controls.sh"
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
# with the lines of its output that say why, where anyone reading the run can see them.
- name: Annotate failed entries
if: failure()
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
- name: Upload split lane logs
if: always()
uses: actions/upload-artifact@v7
@@ -1737,7 +1755,7 @@ jobs:
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_IPC_STRICT_ERRORS: "1"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-strict.xml"
# ---- P5e (ra), MG_Remote/CONTRACT-P5E.md §7: ONE STEP, TWO ARMS, AND THE TREE PICKS ----
#
@@ -2246,6 +2264,12 @@ jobs:
slp.require_green(lane, sys.argv[3], names)
PY
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
# with the lines of its output that say why, where anyone reading the run can see them.
- name: Annotate failed entries
if: failure()
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
- name: Upload strict lane logs
if: always()
uses: actions/upload-artifact@v7
@@ -2751,7 +2775,11 @@ jobs:
&& [ '${{ matrix.case }}' = 'improved-transparency-minecraft-26.3' ]; then
export MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE=1
fi
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$' --output-junit "${RUNNER_TEMP}/retrace.xml"
- name: Annotate failed entries
if: failure()
run: python3 scripts/ci/junit_annotate.py --excerpts 1 "${RUNNER_TEMP}"
- name: Upload core dumps
if: failure()
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Turn the failed entries of ctest --output-junit files into GitHub error annotations.
A red lane's log needs a signed-in reader, but its annotations do not, so the names of the
failed entries - and the lines of their output that say why - are what a reader of the run
page (or of the check-runs API) can see without downloading anything. GitHub keeps at most
ten error annotations per step, so this prints one summary naming every failed entry and then
one excerpt for each of the first few.
Usage: junit_annotate.py [--excerpts N] FILE_OR_DIR... (a directory means every *.xml in it)
Exit status is always 0: this reports a failure, it is never one.
"""
import argparse
import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
INTERESTING = re.compile(
r"Failure|FAILED|Failed|error|Error|Fatal|Refuse|Expected|Which is|Value of|Actual|"
r"Abort|abort|Segmentation|SIGSEGV|signal|timed out|Timeout|assert", re.ASCII)
def escape(text):
return text.replace("%", "%25").replace("\r", "").replace("\n", "%0A")
def failed_cases(path):
try:
root = ET.parse(path).getroot()
except (ET.ParseError, OSError) as error:
print(f"junit_annotate: {path}: {error}", file=sys.stderr)
return []
out = []
for case in root.iter("testcase"):
status = case.get("status", "")
failure = case.find("failure")
error = case.find("error")
if status not in ("fail", "failed") and failure is None and error is None:
continue
text = case.findtext("system-out") or ""
if failure is not None:
text = (failure.get("message") or "") + "\n" + (failure.text or "") + "\n" + text
out.append((case.get("name", "?"), text))
return out
def excerpt(text, limit=40):
lines = [line.rstrip() for line in text.splitlines() if line.strip()]
keep = set()
for index, line in enumerate(lines):
if INTERESTING.search(line):
keep.update(range(index, min(index + 4, len(lines))))
picked = [lines[index] for index in sorted(keep)] or lines[-limit:]
return "\n".join(line[:400] for line in picked[:limit])
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--excerpts", type=int, default=8)
parser.add_argument("paths", nargs="+")
args = parser.parse_args()
files = []
for name in args.paths:
path = Path(name)
files.extend(sorted(path.glob("*.xml")) if path.is_dir() else [path])
failed = []
for path in files:
if path.is_file():
failed.extend((path.name, name, text) for name, text in failed_cases(path))
if not failed:
print("junit_annotate: no failed entries in " + ", ".join(str(f) for f in files))
return 0
names = "\n".join(f"{name} ({source})" for source, name, _ in failed)
print(f"::error title={len(failed)} failed ctest entr(y/ies)::{escape(names[:60000])}")
for source, name, text in failed[:max(args.excerpts, 0)]:
print(f"::error title={name[:200]}::{escape(excerpt(text))}")
return 0
if __name__ == "__main__":
sys.exit(main())