Files
demo-epb/tools/generate_test_report.py
Stefan Lohmaier c81121c3d5
Validate / build-test (macos-latest) (push) Failing after 2s
Validate / build-test (ubuntu-latest) (push) Successful in 17s
Validate / build-test (windows-latest) (push) Failing after 18s
Validate / reports (push) Successful in 52s
feat(i18n): remaining German comments + CI strings in English
Final residual translations found in code/comments/CI:
- .doorstop.yml: config comments, traceability mapping comments
- Doxyfile: header comment
- tools/render_plantuml.py: docstring
- tools/generate_test_report.py: docstring
- tests/unit_test_framework.h: doxygen brief + body
- tests/unit/test_safety_manager.c: section comment
- src/stubs/*.h: doxygen briefs for diag/display/inclinometer/logger/service/wheel-speed
- .gitea/workflows/release.yml: release notes 'Statische Analyse' + deploy error message
2026-05-12 06:14:23 -07:00

152 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""
Generate a test-summary report from the output of our unit tests.
Reads the test-output file (build/test-output.txt) and produces:
- build/test-report.md
- build/test-report.html
Workflow:
make test > build/test-output.txt 2>&1
python3 tools/generate_test_report.py
Oder: `make test-report` macht beides.
"""
from __future__ import annotations
import datetime
import html
import re
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
BUILD = REPO / "build"
TEST_OUTPUT = BUILD / "test-output.txt"
def reqs_for(test_name: str) -> list[str]:
src = REPO / "tests" / "unit" / f"{test_name}.c"
if not src.exists():
return []
head = src.read_text()[:400]
m = re.search(r"@reqs\s+([A-Z0-9 \-,]+)", head)
return re.findall(r"[A-Z]+-\d+", m.group(1)) if m else []
def parse_output(text: str) -> list[dict]:
"""Split into suite-blocks separated by '== test_xxx ==' headers."""
suites = []
cur = None
for line in text.splitlines():
m = re.match(r"==\s+(test_\w+)\s+==", line)
if m:
if cur is not None:
suites.append(cur)
cur = {"binary": m.group(1), "tests": []}
continue
if cur is None:
continue
m = re.match(r"\s+TEST\s+(.+?)\s+\.\.\.\s+(\w+)", line)
if m:
cur["tests"].append((m.group(1).strip(), m.group(2)))
if cur is not None:
suites.append(cur)
for s in suites:
s["total"] = len(s["tests"])
s["failed"] = sum(1 for _, st in s["tests"] if st.lower() != "ok")
s["passed"] = s["total"] - s["failed"]
return suites
def main() -> int:
if not TEST_OUTPUT.exists():
print(f"ERROR: {TEST_OUTPUT} fehlt. Bitte zuerst `make test > {TEST_OUTPUT.relative_to(REPO)} 2>&1` ausfuehren.")
return 1
output = TEST_OUTPUT.read_text()
results = parse_output(output)
if not results:
print("ERROR: no test suite found in the output.")
return 1
total = sum(r["total"] for r in results)
failed = sum(r["failed"] for r in results)
passed = total - failed
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
# Markdown
md = [f"# demo-epb — Test Summary Report\n\n",
f"**Date:** {now}\n\n",
f"**Total:** {total} tests, {passed} passed, {failed} failed\n\n",
f"**Status:** {'PASS' if failed == 0 else 'FAIL'}\n\n",
"## Per Test Suite\n\n",
"| Suite | Count | Passed | Failed | Requirements |\n",
"|-------|--------|-----------|-----------------|---------------|\n"]
for r in results:
reqs = ", ".join(reqs_for(r["binary"])) or ""
md.append(f"| `{r['binary']}` | {r['total']} | {r['passed']} | "
f"{r['failed']} | {reqs} |\n")
md.append("\n## Details\n\n")
for r in results:
md.append(f"### `{r['binary']}`\n\n")
md.append("| # | Test | Status |\n|---|------|--------|\n")
for i, (name, status) in enumerate(r["tests"], 1):
md.append(f"| {i} | {name} | {status} |\n")
md.append("\n")
(BUILD / "test-report.md").write_text("".join(md))
# HTML
badge_cls = "pass-badge" if failed == 0 else "fail-badge"
badge_txt = "PASS" if failed == 0 else "FAIL"
h = [
"<!doctype html><html lang='de'><head>",
"<meta charset='utf-8'><title>demo-epb Test Report</title>",
"<style>",
"body{font-family:-apple-system,Segoe UI,sans-serif;padding:20px}",
"h1{color:#1f3864}h2{color:#1f3864;margin-top:30px}",
"table{border-collapse:collapse;width:100%;font-size:14px;margin:10px 0}",
"th,td{border:1px solid #ccc;padding:6px 10px;text-align:left}",
"th{background:#f0f0f0}",
".pass{color:#0a0;font-weight:bold}.fail{color:#c00;font-weight:bold}",
".badge{display:inline-block;padding:4px 10px;border-radius:4px;color:#fff;font-weight:bold}",
".pass-badge{background:#0a0}.fail-badge{background:#c00}",
"</style></head><body>",
"<h1>demo-epb — Test Summary Report</h1>",
f"<p><strong>Date:</strong> {now}</p>",
f"<p><strong>Total:</strong> {total} tests, {passed} passed, {failed} failed — "
f"<span class='badge {badge_cls}'>{badge_txt}</span></p>",
"<h2>Per Test Suite</h2>",
"<table><tr><th>Suite</th><th>Count</th><th>Passed</th>"
"<th>Failed</th><th>Requirements</th></tr>",
]
for r in results:
reqs = ", ".join(reqs_for(r["binary"])) or ""
h.append(
f"<tr><td><code>{html.escape(r['binary'])}</code></td>"
f"<td>{r['total']}</td><td>{r['passed']}</td>"
f"<td>{r['failed']}</td><td>{html.escape(reqs)}</td></tr>"
)
h.append("</table>")
for r in results:
h.append(f"<h2><code>{html.escape(r['binary'])}</code></h2>")
h.append("<table><tr><th>#</th><th>Test</th><th>Status</th></tr>")
for i, (name, status) in enumerate(r["tests"], 1):
cls = "pass" if status.lower() == "ok" else "fail"
h.append(
f"<tr><td>{i}</td><td>{html.escape(name)}</td>"
f"<td class='{cls}'>{html.escape(status)}</td></tr>"
)
h.append("</table>")
h.append("</body></html>")
(BUILD / "test-report.html").write_text("\n".join(h))
print(f"Wrote {BUILD / 'test-report.md'}")
print(f"Wrote {BUILD / 'test-report.html'}")
print(f"\n{total} tests: {passed} passed, {failed} failed.")
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())