Files

221 lines
9.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""HTML report for pulumi hybrid lab suite (pulumi-vsphere + REST + SOAP)."""
from __future__ import annotations
import html
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
def _esc(value: Any) -> str:
return html.escape(str(value), quote=True)
def _section_rest(summary: dict[str, Any]) -> str:
rest = summary.get("rest") or {}
majors = rest.get("majors") or []
by_verb = rest.get("by_verb") or {}
failures = rest.get("failures") or []
coverage = rest.get("coverage_line") or f"{(rest.get('probed') or 0) - (rest.get('critical') or 0)}/{rest.get('declared') or rest.get('total') or 0}"
critical = rest.get("critical", rest.get("failed", 0))
major_rows = []
for m in majors:
buckets = m.get("buckets") or {}
deep = m.get("deep_vs_stub") or {}
major_rows.append(
"<tr>"
f"<td>{_esc(m.get('major'))}</td>"
f"<td>{_esc(m.get('version'))}</td>"
f"<td>{_esc(m.get('declared'))}</td>"
f"<td>{_esc(m.get('probed'))}</td>"
f"<td>{_esc(m.get('coverage_line') or '')}</td>"
f"<td>{_esc(m.get('critical', m.get('failed')))}</td>"
f"<td>{_esc(buckets.get('success_2xx', 0))}</td>"
f"<td>{_esc(buckets.get('client_4xx', 0))}</td>"
f"<td>{_esc(buckets.get('server_5xx', 0))}</td>"
f"<td>{_esc(deep.get('deep', 0))}/{_esc(deep.get('stub', 0))}</td>"
"</tr>"
)
verb_bits = " · ".join(f"{_esc(k)}={_esc(v)}" for k, v in sorted(by_verb.items()))
fail_rows = []
for f in failures[:40]:
fail_rows.append(
"<tr>"
f"<td>{_esc(f.get('major'))}</td>"
f"<td>{_esc(f.get('verb'))}</td>"
f"<td><code>{_esc(f.get('path'))}</code></td>"
f"<td>{_esc(f.get('kind'))}</td>"
f"<td>{_esc(f.get('status'))}</td>"
f"<td class='err'>{_esc((f.get('body') or f.get('expected') or '')[:180])}</td>"
"</tr>"
)
return f"""
<h2>REST HTTP contract matrix (Layer A)</h2>
<p class="meta"><strong>coverage {_esc(coverage)}</strong>
· critical={_esc(critical)}
· probed={_esc(rest.get("probed") or rest.get("total"))}
· declared={_esc(rest.get("declared"))}
· verbs: {verb_bits or ""}
<br/>100% = HTTP matrix (IMPLEMENTED × majors + HEAD), not pulumi-vsphere resource count.</p>
<table>
<thead><tr><th>Major</th><th>Version</th><th>Declared</th><th>Probed</th><th>Coverage</th>
<th>Critical</th><th>2xx</th><th>4xx</th><th>5xx</th><th>deep/stub</th></tr></thead>
<tbody>{"".join(major_rows) or '<tr><td colspan="10">—</td></tr>'}</tbody>
</table>
<h3>REST critical failures (sample)</h3>
<table>
<thead><tr><th>Major</th><th>Verb</th><th>Path</th><th>Kind</th><th>Status</th><th>Detail</th></tr></thead>
<tbody>{"".join(fail_rows) or '<tr><td colspan="6">none</td></tr>'}</tbody>
</table>
"""
def _section_crud(summary: dict[str, Any]) -> str:
crud = summary.get("crud") or {}
flows = crud.get("flows") or []
rows = []
for f in flows:
rows.append(
f"<tr class='{_esc(f.get('status'))}'>"
f"<td>{_esc(f.get('flow'))}</td>"
f"<td>{_esc(f.get('step'))}</td>"
f"<td>{_esc(f.get('status'))}</td>"
f"<td class='err'>{_esc(f.get('error') or '')}</td>"
f"</tr>"
)
return f"""
<h2>Deep REST CRUD</h2>
<p class="meta">total={_esc(crud.get("total"))} · failed={_esc(crud.get("failed"))}</p>
<table>
<thead><tr><th>Flow</th><th>Step</th><th>Status</th><th>Detail</th></tr></thead>
<tbody>{"".join(rows) or '<tr><td colspan="4">— (smoke skips CRUD)</td></tr>'}</tbody>
</table>
"""
def _section_soap(summary: dict[str, Any]) -> str:
soap = summary.get("soap") or {}
ops = soap.get("ops") or []
rows = []
for op in ops:
status = "passed" if op.get("ok") else "failed"
rows.append(
f"<tr class='{_esc(status)}'>"
f"<td><code>{_esc(op.get('op'))}</code></td>"
f"<td>{_esc(op.get('status'))}</td>"
f"<td>{_esc(status)}</td>"
f"<td class='err'>{_esc(op.get('error') or '')}</td>"
f"</tr>"
)
return f"""
<h2>SOAP WSDL ops</h2>
<p class="meta">total={_esc(soap.get("total"))} · failed={_esc(soap.get("failed"))}</p>
<table>
<thead><tr><th>Operation</th><th>HTTP</th><th>Status</th><th>Detail</th></tr></thead>
<tbody>{"".join(rows) or '<tr><td colspan="4">— (smoke skips SOAP)</td></tr>'}</tbody>
</table>
"""
def render_html(summary: dict[str, Any]) -> str:
generated = summary.get("generated_at") or datetime.now(UTC).isoformat()
cases = summary.get("cases") or []
total = len(cases)
passed = sum(1 for c in cases if c.get("status") == "passed")
failed = sum(1 for c in cases if c.get("status") == "failed")
skipped = sum(1 for c in cases if c.get("status") == "skipped")
rest = summary.get("rest") or {}
crud = summary.get("crud") or {}
soap = summary.get("soap") or {}
rows = []
for case in cases:
status = case.get("status") or ""
outs = case.get("outputs") or {}
out_html = "<br/>".join(f"<code>{_esc(k)}</code>={_esc(v)}" for k, v in outs.items())
err = _esc(case.get("error") or "")
rows.append(
f"<tr class='{_esc(status)}'>"
f"<td>{_esc(case.get('id'))}</td>"
f"<td>{_esc(case.get('title'))}</td>"
f"<td>{_esc(status)}</td>"
f"<td class='outs'>{out_html or ''}</td>"
f"<td class='err'>{err or ''}</td>"
f"</tr>"
)
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>pulumi hybrid lab report</title>
<style>
:root {{
--bg:#0f1419; --panel:#1a222c; --text:#e7eef7; --muted:#9aa8b8;
--ok:#3dd68c; --fail:#ff6b6b; --skip:#f0c14a; --line:#2a3542; --accent:#5b9fd4;
}}
body {{ margin:0; font-family:"IBM Plex Sans","Segoe UI",sans-serif; background:var(--bg); color:var(--text); }}
header, main {{ max-width:1200px; margin:0 auto; padding:1.5rem; }}
h1 {{ margin:0 0 .4rem; }}
h2 {{ margin:1.75rem 0 .6rem; color:var(--accent); }}
h3 {{ margin:1rem 0 .4rem; font-size:1rem; color:var(--muted); }}
.meta {{ color:var(--muted); }}
.summary {{ display:grid; grid-template-columns:repeat(4,1fr); gap:.75rem; margin:1rem 0 1.5rem; }}
.stat {{ background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:1rem; }}
.stat strong {{ display:block; font-size:1.5rem; margin-top:.25rem; }}
.ok strong {{ color:var(--ok); }} .fail strong {{ color:var(--fail); }} .skip strong {{ color:var(--skip); }}
table {{ width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--line);
border-radius:12px; overflow:hidden; margin-bottom:1rem; }}
th, td {{ padding:.55rem .65rem; border-bottom:1px solid var(--line); text-align:left; vertical-align:top; font-size:.9rem; }}
th {{ color:var(--accent); }}
tr.failed td:nth-child(3) {{ color:var(--fail); font-weight:600; }}
tr.passed td:nth-child(3) {{ color:var(--ok); }}
tr.skipped td:nth-child(3) {{ color:var(--skip); }}
code {{ font-family:ui-monospace,Menlo,monospace; font-size:.8rem; }}
.outs, .err {{ word-break:break-word; }}
.metrics {{ display:grid; grid-template-columns:repeat(3,1fr); gap:.75rem; margin-bottom:1rem; }}
</style>
</head>
<body>
<header>
<h1>pulumi hybrid lab suite</h1>
<div class="meta">Generated {_esc(generated)} · server {_esc(summary.get("vsphere_server"))}
· base {_esc(summary.get("vsphere_base"))}
· smoke={_esc(summary.get("smoke"))}</div>
</header>
<main>
<section class="summary">
<div class="stat"><span>Cases</span><strong>{total}</strong></div>
<div class="stat ok"><span>Passed</span><strong>{passed}</strong></div>
<div class="stat fail"><span>Failed</span><strong>{failed}</strong></div>
<div class="stat skip"><span>Skipped</span><strong>{skipped}</strong></div>
</section>
<section class="metrics">
<div class="stat"><span>REST failed</span><strong>{_esc(rest.get("failed", 0))}</strong>
<span class="meta"> / {_esc(rest.get("total", 0))} probes</span></div>
<div class="stat"><span>CRUD failed</span><strong>{_esc(crud.get("failed", 0))}</strong>
<span class="meta"> / {_esc(crud.get("total", 0))} flows</span></div>
<div class="stat"><span>SOAP failed</span><strong>{_esc(soap.get("failed", 0))}</strong>
<span class="meta"> / {_esc(soap.get("total", 0))} ops</span></div>
</section>
<h2>Suite cases</h2>
<table>
<thead><tr><th>ID</th><th>Title</th><th>Status</th><th>Outputs</th><th>Error</th></tr></thead>
<tbody>{"".join(rows)}</tbody>
</table>
{_section_rest(summary)}
{_section_crud(summary)}
{_section_soap(summary)}
</main>
</body>
</html>
"""
def write_report(summary: dict[str, Any], path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(render_html(summary), encoding="utf-8")