Publish EUnit/CT HTML reports to ci-reports host.

Refs EventHub/EventHubBack#62
This commit is contained in:
2026-07-27 22:48:35 +03:00
parent cd619db4a2
commit 2bfce06dbf
10 changed files with 361 additions and 4 deletions
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""Convert Surefire/JUnit XML (eunit_surefire) into a simple HTML report."""
from __future__ import annotations
import argparse
import html
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
def load_suites(paths: list[Path]) -> list[ET.Element]:
suites: list[ET.Element] = []
for path in paths:
try:
root = ET.parse(path).getroot()
except ET.ParseError as exc:
print(f"skip {path}: {exc}", file=sys.stderr)
continue
if root.tag == "testsuite":
suites.append(root)
elif root.tag == "testsuites":
suites.extend(list(root.findall("testsuite")))
return suites
def render(suites: list[ET.Element], title: str) -> str:
total = failures = errors = skipped = 0
rows: list[str] = []
for suite in suites:
s_name = suite.attrib.get("name", "suite")
total += int(suite.attrib.get("tests", "0") or 0)
failures += int(suite.attrib.get("failures", "0") or 0)
errors += int(suite.attrib.get("errors", "0") or 0)
skipped += int(suite.attrib.get("skipped", "0") or 0)
for case in suite.findall("testcase"):
name = case.attrib.get("name", "?")
classname = case.attrib.get("classname", s_name)
time_s = case.attrib.get("time", "")
fail = case.find("failure")
err = case.find("error")
skip = case.find("skipped")
if fail is not None:
status, detail = "FAIL", fail.attrib.get("message") or (fail.text or "")
elif err is not None:
status, detail = "ERROR", err.attrib.get("message") or (err.text or "")
elif skip is not None:
status, detail = "SKIP", skip.attrib.get("message") or ""
else:
status, detail = "PASS", ""
cls = status.lower()
rows.append(
"<tr class=\"{cls}\"><td>{status}</td><td>{suite}</td><td>{case}</td>"
"<td>{time}</td><td><pre>{detail}</pre></td></tr>".format(
cls=html.escape(cls),
status=html.escape(status),
suite=html.escape(classname),
case=html.escape(name),
time=html.escape(time_s),
detail=html.escape(detail.strip()[:4000]),
)
)
ok = total - failures - errors - skipped
return f"""<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8"/>
<title>{html.escape(title)}</title>
<style>
body {{ font-family: system-ui, sans-serif; margin: 1.5rem; }}
.summary span {{ margin-right: 1rem; }}
.pass {{ color: #0a7; }}
.fail, .error {{ color: #c22; }}
.skip {{ color: #a80; }}
table {{ border-collapse: collapse; width: 100%; margin-top: 1rem; }}
th, td {{ border: 1px solid #ddd; padding: .4rem .6rem; vertical-align: top; text-align: left; }}
th {{ background: #f4f4f4; }}
pre {{ white-space: pre-wrap; margin: 0; font-size: 12px; }}
tr.fail, tr.error {{ background: #fff5f5; }}
tr.pass {{ background: #f5fff8; }}
</style>
</head>
<body>
<h1>{html.escape(title)}</h1>
<p class="summary">
<span>tests: <b>{total}</b></span>
<span class="pass">pass: <b>{ok}</b></span>
<span class="fail">fail: <b>{failures}</b></span>
<span class="error">error: <b>{errors}</b></span>
<span class="skip">skip: <b>{skipped}</b></span>
</p>
<table>
<thead><tr><th>Status</th><th>Suite</th><th>Case</th><th>Time</th><th>Detail</th></tr></thead>
<tbody>
{''.join(rows) if rows else '<tr><td colspan="5">No testcases</td></tr>'}
</tbody>
</table>
</body>
</html>
"""
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("xml_dir", type=Path, help="Directory with TEST-*.xml")
ap.add_argument("-o", "--out", type=Path, required=True, help="Output HTML file or dir")
ap.add_argument("--title", default="EUnit report")
args = ap.parse_args()
xml_files = sorted(args.xml_dir.glob("TEST-*.xml")) + sorted(args.xml_dir.glob("*.xml"))
# de-dupe
seen = set()
unique: list[Path] = []
for p in xml_files:
if p.resolve() in seen:
continue
seen.add(p.resolve())
unique.append(p)
suites = load_suites(unique)
out = args.out
if out.suffix.lower() != ".html":
out.mkdir(parents=True, exist_ok=True)
out = out / "index.html"
else:
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(render(suites, args.title), encoding="utf-8")
print(f"wrote {out} ({len(suites)} suites, {len(unique)} xml files)")
return 0
if __name__ == "__main__":
raise SystemExit(main())