Files
proxmox-api-simulator/pulumi-tests/pulumi/report.py
T
Sergey Antropoff 48df10b17e Prepare 0.1.0 for lab release: durable handlers, HTTP Compose, CI, and pulumi-tests.
- Harden DB-backed handlers and seed profiles; align client wire shapes for
  cluster resources, QEMU config, and node SSL fields
- Serve plain HTTP on Compose :8006; keep TLS optional (--profile tls) and
  terminate HTTPS at Kubernetes Ingress
- Add pulumi-tests (full contract surface majors 6–9 + BPG lifecycle) and
  make pulumi-tests
- Ship bilingual docs, CHANGELOG, SECURITY, CONTRIBUTING, and GitHub Actions
  (make ci + Compose/Helm validation)
2026-07-18 04:18:05 +03:00

266 lines
10 KiB
Python
Raw 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.
"""Render HTML and optional JUnit reports for the Pulumi HX suite."""
from __future__ import annotations
import html
import json
from pathlib import Path
from typing import Any
from xml.etree.ElementTree import Element, ElementTree, SubElement
def write_json(payload: dict[str, Any], path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
# Drop per-method lists from majors for a lighter default JSON? Keep full for debugging.
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
def write_junit(payload: dict[str, Any], path: Path) -> None:
surface = payload.get("surface") or []
scenarios = payload.get("scenarios") or []
cases: list[dict[str, Any]] = []
for major in surface:
cases.append(
{
"classname": "surface",
"name": f"PVE {major.get('version')} major={major.get('major')}",
"time": major.get("time") or 0,
"ok": bool(major.get("ok")),
"error": _surface_error(major),
}
)
for item in scenarios:
cases.append(
{
"classname": "lifecycle",
"name": item.get("id") or item.get("name") or "lifecycle",
"time": item.get("time") or 0,
"ok": bool(item.get("ok")),
"error": item.get("error") or "",
}
)
suite = Element(
"testsuite",
name="pulumi-hx",
tests=str(len(cases)),
failures=str(sum(1 for c in cases if not c["ok"])),
time=f"{sum(float(c['time']) for c in cases):.3f}",
)
for item in cases:
case = SubElement(
suite,
"testcase",
classname=str(item["classname"]),
name=str(item["name"]),
time=f"{float(item['time']):.3f}",
)
if not item["ok"]:
failure = SubElement(case, "failure", message=str(item["error"])[:500])
failure.text = str(item["error"])
path.parent.mkdir(parents=True, exist_ok=True)
ElementTree(suite).write(path, encoding="utf-8", xml_declaration=True)
def _surface_error(major: dict[str, Any]) -> str:
fails = major.get("failures") or []
if not fails:
return ""
parts = [
f"{f.get('verb')} {f.get('path')} -> {f.get('bucket')} {f.get('status', '')}"
for f in fails[:20]
]
return f"{len(fails)} critical: " + "; ".join(parts)
def write_html(payload: dict[str, Any], path: Path) -> None:
surface = payload.get("surface") or []
scenarios = payload.get("scenarios") or []
coverage = payload.get("coverage") or {}
ok = bool(payload.get("ok"))
elapsed = float(payload.get("elapsed") or 0)
coverage_by_major = coverage.get("by_major") or []
coverage_rows = []
for item in coverage_by_major:
complete = bool(item.get("complete"))
coverage_rows.append(
"<tr>"
f"<td>{html.escape(str(item.get('major')))}</td>"
f"<td>{html.escape(str(item.get('version')))}</td>"
f"<td>{html.escape(str(item.get('declared')))}</td>"
f"<td>{html.escape(str(item.get('probed')))}</td>"
f"<td>{html.escape(str(item.get('critical')))}</td>"
f"<td class='{'ok' if complete else 'fail'}'>"
f"{'yes' if complete else 'no'}</td>"
"</tr>"
)
declared_total = int(coverage.get("declared_total") or 0)
probed_total = int(coverage.get("probed_total") or 0)
critical_total = int(coverage.get("critical_total") or 0)
coverage_ok = bool(coverage.get("ok")) if coverage_by_major else True
majs = coverage.get("majors") or []
if len(majs) >= 2:
majors_label = f"{majs[0]}{majs[-1]}"
elif majs:
majors_label = str(majs[0])
else:
majors_label = ""
surface_rows = []
failure_rows = []
for major in surface:
surface_rows.append(
"<tr>"
f"<td>{html.escape(str(major.get('major')))}</td>"
f"<td>{html.escape(str(major.get('version')))}</td>"
f"<td>{html.escape(str(major.get('declared')))}</td>"
f"<td>{html.escape(str(major.get('probed')))}</td>"
f"<td>{html.escape(str(major.get('success_2xx')))}</td>"
f"<td>{html.escape(str(major.get('client_4xx')))}</td>"
f"<td class='{'ok' if major.get('ok') else 'fail'}'>"
f"{html.escape(str(major.get('failure_count')))}</td>"
f"<td>{float(major.get('time') or 0):.1f}s</td>"
"</tr>"
)
for fail in major.get("failures") or []:
failure_rows.append(
"<tr>"
f"<td>{html.escape(str(major.get('version')))}</td>"
f"<td>{html.escape(str(fail.get('verb')))}</td>"
f"<td><code>{html.escape(str(fail.get('path')))}</code></td>"
f"<td>{html.escape(str(fail.get('bucket')))}</td>"
f"<td>{html.escape(str(fail.get('status', fail.get('error', ''))))}</td>"
f"<td><code>{html.escape(str(fail.get('body', ''))[:200])}</code></td>"
"</tr>"
)
scenario_rows = []
for item in scenarios:
scenario_rows.append(
"<tr>"
f"<td>{html.escape(str(item.get('id') or item.get('name')))}</td>"
f"<td class='{'ok' if item.get('ok') else 'fail'}'>"
f"{'PASS' if item.get('ok') else 'FAIL'}</td>"
f"<td>{float(item.get('time') or 0):.2f}s</td>"
f"<td><code>{html.escape(str(item.get('error') or ''))}</code></td>"
"</tr>"
)
status = "PASS" if ok else "FAIL"
status_class = "ok" if ok else "fail"
coverage_status = "complete" if coverage_ok else "incomplete"
coverage_class = "ok" if coverage_ok else "fail"
doc = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Pulumi HX suite report</title>
<style>
:root {{
--bg: #0f1419; --panel: #1a222c; --text: #e7ecf1; --muted: #9aa7b5;
--ok: #3dd68c; --fail: #ff6b6b; --line: #2a3542;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--sans: "Segoe UI", system-ui, sans-serif;
}}
body {{ margin: 0; font-family: var(--sans); background: var(--bg); color: var(--text); }}
main {{ max-width: 1100px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }}
h1 {{ font-size: 1.6rem; margin: 0 0 .4rem; }}
h2 {{ font-size: 1.15rem; margin: 2rem 0 .75rem; }}
p.lead {{ color: var(--muted); margin: 0 0 1.5rem; }}
.badge {{ display: inline-block; padding: .2rem .6rem; border-radius: .35rem;
font-weight: 700; letter-spacing: .02em; }}
.badge.ok {{ background: color-mix(in srgb, var(--ok) 25%, transparent); color: var(--ok); }}
.badge.fail {{ background: color-mix(in srgb, var(--fail) 25%, transparent); color: var(--fail); }}
.cards {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: .75rem; margin: 1rem 0 1.5rem; }}
.card {{ background: var(--panel); border: 1px solid var(--line); border-radius: .5rem;
padding: .9rem 1rem; }}
.card .label {{ color: var(--muted); font-size: .8rem; }}
.card .value {{ font-size: 1.35rem; font-weight: 700; margin-top: .25rem; }}
table {{ width: 100%; border-collapse: collapse; background: var(--panel);
border: 1px solid var(--line); border-radius: .5rem; overflow: hidden; }}
th, td {{ text-align: left; padding: .55rem .7rem; border-bottom: 1px solid var(--line);
vertical-align: top; font-size: .92rem; }}
th {{ color: var(--muted); font-weight: 600; }}
tr:last-child td {{ border-bottom: 0; }}
td.ok {{ color: var(--ok); font-weight: 600; }}
td.fail {{ color: var(--fail); font-weight: 600; }}
code {{ font-family: var(--mono); font-size: .85em; }}
.empty {{ color: var(--muted); font-style: italic; }}
</style>
</head>
<body>
<main>
<h1>Pulumi HX suite report</h1>
<p class="lead">Contract surface probe (PVE majors 69) + lifecycle scenarios</p>
<span class="badge {status_class}">{status}</span>
<div class="cards">
<div class="card"><div class="label">Elapsed</div>
<div class="value">{elapsed:.1f}s</div></div>
<div class="card"><div class="label">Majors</div>
<div class="value">{len(surface)}</div></div>
<div class="card"><div class="label">Critical surface fails</div>
<div class="value">{sum(int(m.get('failure_count') or 0) for m in surface)}</div></div>
<div class="card"><div class="label">Scenarios</div>
<div class="value">{sum(1 for s in scenarios if s.get('ok'))}/{len(scenarios)}</div></div>
</div>
<h2>Full contract coverage</h2>
<p class="lead">
{probed_total}/{declared_total} methods across majors {html.escape(majors_label)}
(critical={critical_total}) —
<span class="{coverage_class}">{coverage_status}</span>
</p>
<table>
<thead><tr>
<th>Major</th><th>Version</th><th>Declared</th><th>Probed</th>
<th>Critical</th><th>declared==probed</th>
</tr></thead>
<tbody>
{''.join(coverage_rows) or '<tr><td colspan="6" class="empty">No coverage data</td></tr>'}
<tr>
<td colspan="2"><strong>Total</strong></td>
<td><strong>{declared_total}</strong></td>
<td><strong>{probed_total}</strong></td>
<td><strong>{critical_total}</strong></td>
<td class="{coverage_class}"><strong>{'yes' if coverage_ok else 'no'}</strong></td>
</tr>
</tbody>
</table>
<h2>Surface by major</h2>
<table>
<thead><tr>
<th>Major</th><th>Version</th><th>Declared</th><th>Probed</th>
<th>2xx</th><th>4xx/auth</th><th>Critical</th><th>Time</th>
</tr></thead>
<tbody>
{''.join(surface_rows) or '<tr><td colspan="8" class="empty">No surface results</td></tr>'}
</tbody>
</table>
<h2>Critical surface failures</h2>
<table>
<thead><tr>
<th>Version</th><th>Verb</th><th>Path</th><th>Bucket</th><th>Status</th><th>Body</th>
</tr></thead>
<tbody>
{''.join(failure_rows) or '<tr><td colspan="6" class="empty">None</td></tr>'}
</tbody>
</table>
<h2>Lifecycle scenarios</h2>
<table>
<thead><tr><th>ID</th><th>Result</th><th>Time</th><th>Error</th></tr></thead>
<tbody>
{''.join(scenario_rows) or '<tr><td colspan="4" class="empty">No scenarios</td></tr>'}
</tbody>
</table>
</main>
</body>
</html>
"""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(doc, encoding="utf-8")