#!/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 = [ "
", "Datum: {now}
", f"Gesamt: {total} Tests, {passed} bestanden, {failed} fehlgeschlagen — " f"{badge_txt}
", "| Suite | Anzahl | Bestanden | " "Fehlgeschlagen | Anforderungen |
|---|---|---|---|---|
{html.escape(r['binary'])} | "
f"{r['total']} | {r['passed']} | " f"{r['failed']} | {html.escape(reqs)} |
{html.escape(r['binary'])}| # | Test | Status |
|---|---|---|
| {i} | {html.escape(name)} | " f"{html.escape(status)} |