#!/usr/bin/env python3 """ Erzeugt einen Test-Summary-Report aus dem Output unserer Unit-Tests. Liest die Test-Output-Datei (build/test-output.txt) und erzeugt: - 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: keine Test-Suite im Output gefunden.") 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"**Datum:** {now}\n\n", f"**Gesamt:** {total} Tests, {passed} bestanden, {failed} fehlgeschlagen\n\n", f"**Status:** {'PASS' if failed == 0 else 'FAIL'}\n\n", "## Pro Test-Suite\n\n", "| Suite | Anzahl | Bestanden | Fehlgeschlagen | Anforderungen |\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 = [ "", "demo-epb Test Report", "", "

demo-epb — Test Summary Report

", f"

Datum: {now}

", f"

Gesamt: {total} Tests, {passed} bestanden, {failed} fehlgeschlagen — " f"{badge_txt}

", "

Pro Test-Suite

", "" "", ] for r in results: reqs = ", ".join(reqs_for(r["binary"])) or "—" h.append( f"" f"" f"" ) h.append("
SuiteAnzahlBestandenFehlgeschlagenAnforderungen
{html.escape(r['binary'])}{r['total']}{r['passed']}{r['failed']}{html.escape(reqs)}
") for r in results: h.append(f"

{html.escape(r['binary'])}

") h.append("") for i, (name, status) in enumerate(r["tests"], 1): cls = "pass" if status.lower() == "ok" else "fail" h.append( f"" f"" ) h.append("
#TestStatus
{i}{html.escape(name)}{html.escape(status)}
") h.append("") (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())