#!/usr/bin/env python3
"""Generate a markdown summary from JUnit XML test results."""
from __future__ import annotations
import argparse
import sys
import textwrap
import xml.etree.ElementTree as ET
def generate_summary(xml_path: str) -> str:
"""Parse JUnit XML and generate markdown summary."""
lines: list[str] = []
try:
tree = ET.parse(xml_path)
root = tree.getroot()
testsuite = root.find("testsuite") if root.tag != "testsuite" else root
if testsuite is None:
testsuite = root
tests = int(testsuite.get("tests", 0))
failures = int(testsuite.get("failures", 0))
errors = int(testsuite.get("errors", 0))
skipped = int(testsuite.get("skipped", 0))
time_taken = float(testsuite.get("time", 0))
passed = tests - failures - errors - skipped
if failures == 0 and errors == 0:
status = "✓ All tests passed"
else:
status = "✗ Some tests failed"
lines.append("## STT Test Results\n")
lines.append(f"**Status:** {status}\n")
lines.append("| Metric | Count |")
lines.append("|--------|-------|")
lines.append(f"| ✓ Passed | {passed} |")
lines.append(f"| ✗ Failed | {failures} |")
lines.append(f"| × Errors | {errors} |")
lines.append(f"| → Skipped | {skipped} |")
lines.append(f"| ▣ Total | {tests} |")
lines.append(f"| ⏱ Duration | {time_taken:.1f}s |")
lines.append("")
if failures > 0 or errors > 0:
lines.append("")
lines.append("Failed Tests
\n")
for testcase in testsuite.iter("testcase"):
failure = testcase.find("failure")
error = testcase.find("error")
if failure is not None or error is not None:
name = testcase.get("name", "unknown")
classname = testcase.get("classname", "")
elem = failure if failure is not None else error
msg = elem.text if elem.text else elem.get("message", "")
msg = msg[:2000]
indented_msg = textwrap.indent(msg.strip(), " ")
lines.append(f"- **{classname}::{name}**")
lines.append(" ```")
lines.append(indented_msg)
lines.append(" ```")
lines.append(" ")
skipped_tests = [tc for tc in testsuite.iter("testcase") if tc.find("skipped") is not None]
if skipped_tests:
lines.append("")
lines.append("Skipped Tests
\n")
lines.append("| Test | Reason |")
lines.append("|------|--------|")
for testcase in skipped_tests[:20]:
name = testcase.get("name", "unknown")
classname = testcase.get("classname", "")
skip = testcase.find("skipped")
reason = skip.get("message", "") if skip is not None else ""
test_name = f"{classname}::{name}".replace("|", "\\|")
reason_escaped = reason.strip().replace("|", "\\|").replace("\n", " ")
lines.append(f"| `{test_name}` | {reason_escaped} |")
if len(skipped_tests) > 20:
lines.append(f"| ... | ... and {len(skipped_tests) - 20} more |")
lines.append(" ")
except Exception as e:
lines.append("## STT Test Results\n")
lines.append(f"⚠ Could not parse test results: {e}")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description="Generate test summary from JUnit XML")
parser.add_argument(
"xml_path",
nargs="?",
default="test-results.xml",
help="Path to JUnit XML file (default: test-results.xml)",
)
parser.add_argument(
"-o",
"--output",
default=None,
help="Output file path (default: stdout)",
)
args = parser.parse_args()
summary = generate_summary(args.xml_path)
if args.output:
with open(args.output, "w") as f:
f.write(summary)
else:
print(summary)
return 0
if __name__ == "__main__":
sys.exit(main())