118 lines
4.9 KiB
Python
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()) |