Files

118 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Render a normalized test report as Markdown, HTML, and PDF."""
import argparse
import html
import json
from pathlib import Path
from fpdf import FPDF
def pdf_text(value: object) -> str:
return str(value).encode("latin-1", errors="replace").decode("latin-1")
def markdown(report: dict) -> str:
project = report["project"]
tool = report["tool"]
target = report["target"]
summary = report["summary"]
lines = [
f"# {project['name']} Test Report",
"",
"| Field | Value |",
"|---|---|",
f"| Project ID | {project['id']} |",
f"| Environment | {project['environment']} |",
f"| Tool | {tool['name']} |",
f"| Target | {target['id']} |",
f"| Run | {report['run']['id']} |",
f"| Started | {report['run']['started_at']} |",
"",
"## Summary",
"",
"| Total | Passed | Failed | Errors | Skipped | Review | Score |",
"|---:|---:|---:|---:|---:|---:|---:|",
f"| {summary['total']} | {summary['passed']} | {summary['failed']} | {summary['errors']} | {summary['skipped']} | {summary['review']} | {summary.get('score', 0):.2f}% |",
"",
"## Results",
"",
"| Status | Severity | ID | Title |",
"|---|---|---|---|",
]
for result in report["results"]:
title = str(result["title"]).replace("|", "\\|")
lines.append(f"| {result['status']} | {result['severity']} | {result['id']} | {title} |")
return "\n".join(lines) + "\n"
def html_document(markdown_text: str, report: dict) -> str:
rows = "".join(
"<tr>"
f"<td>{html.escape(result['status'])}</td>"
f"<td>{html.escape(result['severity'])}</td>"
f"<td>{html.escape(result['id'])}</td>"
f"<td>{html.escape(result['title'])}</td>"
"</tr>"
for result in report["results"]
)
summary = report["summary"]
return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>{html.escape(report['project']['name'])} test report</title>
<style>body{{font:14px sans-serif;max-width:1100px;margin:40px auto;color:#17202a}}table{{border-collapse:collapse;width:100%}}th,td{{border:1px solid #ccd1d1;padding:8px;text-align:left}}th{{background:#eaecee}}.summary{{display:flex;gap:24px;margin:24px 0}}.summary strong{{font-size:24px;display:block}}</style></head>
<body><h1>{html.escape(report['project']['name'])} Test Report</h1>
<p>{html.escape(report['tool']['name'])} against {html.escape(report['target']['id'])} at {html.escape(report['run']['started_at'])}</p>
<div class="summary"><span><strong>{summary['total']}</strong>Total</span><span><strong>{summary['passed']}</strong>Passed</span><span><strong>{summary['failed']}</strong>Failed</span><span><strong>{summary.get('score', 0):.2f}%</strong>Score</span></div>
<table><thead><tr><th>Status</th><th>Severity</th><th>ID</th><th>Title</th></tr></thead><tbody>{rows}</tbody></table>
<details><summary>Markdown source</summary><pre>{html.escape(markdown_text)}</pre></details></body></html>"""
def pdf_document(report: dict, output: Path) -> None:
pdf = FPDF()
pdf.set_auto_page_break(auto=True, margin=15)
pdf.add_page()
pdf.set_font("Helvetica", "B", 18)
pdf.multi_cell(0, 10, pdf_text(f"{report['project']['name']} Test Report"), new_x="LMARGIN", new_y="NEXT")
pdf.set_font("Helvetica", size=10)
pdf.multi_cell(
0,
6,
pdf_text(f"Tool: {report['tool']['name']}\nTarget: {report['target']['id']}\nStarted: {report['run']['started_at']}"),
new_x="LMARGIN",
new_y="NEXT",
)
summary = report["summary"]
pdf.ln(3)
pdf.set_font("Helvetica", "B", 12)
pdf.cell(0, 8, f"Total {summary['total']} Passed {summary['passed']} Failed {summary['failed']} Score {summary.get('score', 0):.2f}%", new_x="LMARGIN", new_y="NEXT")
pdf.set_font("Helvetica", size=9)
for result in report["results"]:
pdf.multi_cell(
0,
5,
pdf_text(f"[{result['status'].upper()}] [{result['severity']}] {result['id']}: {result['title']}"),
new_x="LMARGIN",
new_y="NEXT",
)
pdf.output(output)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("input", type=Path)
parser.add_argument("--output-dir", type=Path, required=True)
args = parser.parse_args()
report = json.loads(args.input.read_text(encoding="utf-8"))
args.output_dir.mkdir(parents=True, exist_ok=True)
stem = f"{report['tool']['id']}-{report['target']['id']}"
markdown_text = markdown(report)
(args.output_dir / f"{stem}.md").write_text(markdown_text, encoding="utf-8")
(args.output_dir / f"{stem}.html").write_text(html_document(markdown_text, report), encoding="utf-8")
pdf_document(report, args.output_dir / f"{stem}.pdf")
print(f"Rendered Markdown, HTML, and PDF reports in {args.output_dir}")
return 0
if __name__ == "__main__":
raise SystemExit(main())