Publish EUnit/CT HTML reports to ci-reports host.
Refs EventHub/EventHubBack#62
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user