Skip to content

Commit cc7744d

Browse files
committed
updatig rich reports
1 parent daa4c7b commit cc7744d

1 file changed

Lines changed: 94 additions & 27 deletions

File tree

scripts/generate_pages_report.py

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,39 @@
88
import html
99
import json
1010
from pathlib import Path
11+
import re
1112
import textwrap
1213
import xml.etree.ElementTree as ET
1314

1415

16+
def _infer_file_from_classname(classname: str) -> str:
17+
if not classname:
18+
return ""
19+
return f"{classname.replace('.', '/')}.py"
20+
21+
22+
def _infer_source_from_detail(detail: str, fallback_file: str, fallback_line: str) -> tuple[str, str]:
23+
match = re.search(r"(tests/[^:\s]+\.py):(\d+)", detail)
24+
if match:
25+
return match.group(1), match.group(2)
26+
return fallback_file, fallback_line
27+
28+
1529
def _parse_junit(junit_path: Path) -> tuple[list[dict], dict]:
30+
if not junit_path.exists():
31+
return [
32+
{
33+
"id": "workflow::pytest-results",
34+
"classname": "workflow",
35+
"name": "pytest-results",
36+
"status": "error",
37+
"file": "",
38+
"line": "",
39+
"duration": 0.0,
40+
"detail": f"JUnit XML was not found at {junit_path}. The test command may not have started or may have failed before pytest wrote results.",
41+
}
42+
], {"total": 1, "passed": 0, "failed": 0, "error": 1, "skipped": 0}
43+
1644
tree = ET.parse(junit_path)
1745
root = tree.getroot()
1846

@@ -22,7 +50,7 @@ def _parse_junit(junit_path: Path) -> tuple[list[dict], dict]:
2250
for case in root.iter("testcase"):
2351
classname = case.attrib.get("classname", "").strip()
2452
name = case.attrib.get("name", "").strip()
25-
file_path = case.attrib.get("file", "").strip()
53+
file_path = case.attrib.get("file", "").strip() or _infer_file_from_classname(classname)
2654
line = case.attrib.get("line", "").strip()
2755
duration = float(case.attrib.get("time", "0") or 0)
2856

@@ -44,6 +72,8 @@ def _parse_junit(junit_path: Path) -> tuple[list[dict], dict]:
4472
status = "skipped"
4573
detail = (skipped.text or skipped.attrib.get("message", "")).strip()
4674

75+
file_path, line = _infer_source_from_detail(detail, file_path, line)
76+
4777
summary["total"] += 1
4878
summary[status] += 1
4979

@@ -131,10 +161,19 @@ def _render_run_page(
131161
"</section>"
132162
)
133163

134-
pytest_html_link = (
135-
f"<a class='button' href='{html.escape(pytest_report_relpath)}'>Open rich pytest-html report</a>"
164+
rich_report_panel = (
165+
"<details class='card rich-report' open>"
166+
"<summary><span>Rich pytest report</span><small>Click to collapse</small></summary>"
167+
f"<iframe src='{html.escape(pytest_report_relpath)}' title='Rich pytest-html report'></iframe>"
168+
f"<p><a class='button' href='{html.escape(pytest_report_relpath)}'>Open rich report in a new tab</a></p>"
169+
"</details>"
136170
if pytest_report_relpath
137-
else ""
171+
else (
172+
"<section class='card rich-report empty'>"
173+
"<h2>Rich pytest report unavailable</h2>"
174+
"<p>The pytest-html artifact was not produced for this run.</p>"
175+
"</section>"
176+
)
138177
)
139178

140179
fail_list = "".join(
@@ -152,24 +191,39 @@ def _render_run_page(
152191
<meta name="viewport" content="width=device-width, initial-scale=1" />
153192
<title>Test Run {html.escape(run_meta["run_id"])} - {html.escape(run_meta["workflow"])}</title>
154193
<style>
155-
body {{ font-family: Inter, ui-sans-serif, system-ui, sans-serif; margin: 0; background:#0b1020; color:#e6edf3; }}
156-
.wrap {{ max-width: 1200px; margin: 0 auto; padding: 24px; }}
157-
.grid {{ display:grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 12px; }}
158-
.card {{ background:#111827; border:1px solid #334155; border-radius:12px; padding:16px; margin-top: 14px; }}
159-
.badge {{ font-size: 12px; border-radius: 999px; padding: 3px 10px; font-weight: 600; text-transform: uppercase; }}
194+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap');
195+
:root {{ color-scheme: dark; --bg:#070b16; --panel:rgba(15,23,42,.74); --panel-strong:rgba(15,23,42,.92); --border:rgba(148,163,184,.22); --text:#e5eefb; --muted:#9fb0c6; --accent:#7dd3fc; --accent-strong:#2563eb; }}
196+
* {{ box-sizing: border-box; }}
197+
body {{ font-family: Inter, ui-sans-serif, system-ui, sans-serif; margin: 0; background: radial-gradient(circle at 15% 0%, rgba(59,130,246,.24), transparent 32rem), radial-gradient(circle at 85% 10%, rgba(14,165,233,.18), transparent 28rem), linear-gradient(180deg, #020617, var(--bg)); color:var(--text); }}
198+
.wrap {{ max-width: 1320px; margin: 0 auto; padding: 28px; }}
199+
h1 {{ font-size: clamp(2rem, 4vw, 4rem); letter-spacing: -.055em; margin: 10px 0 4px; }}
200+
h2 {{ font-size: 1.05rem; letter-spacing: -.02em; }}
201+
.subnav {{ color: var(--muted); margin-bottom: 18px; }}
202+
.grid {{ display:grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 14px; }}
203+
.card {{ background:linear-gradient(180deg, var(--panel), rgba(15,23,42,.56)); border:1px solid var(--border); border-radius:18px; padding:18px; margin-top: 16px; box-shadow: 0 18px 55px rgba(2,6,23,.32); backdrop-filter: blur(18px); }}
204+
.grid .card p {{ font-size: 2rem; font-weight: 800; margin: 0; letter-spacing: -.05em; }}
205+
.badge {{ font-size: 11px; border-radius: 999px; padding: 4px 10px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }}
160206
table {{ width:100%; border-collapse: collapse; }}
161-
th, td {{ text-align:left; border-bottom:1px solid #334155; padding: 8px; font-size: 14px; vertical-align: top; }}
162-
a {{ color:#93c5fd; text-decoration: none; }}
207+
th, td {{ text-align:left; border-bottom:1px solid rgba(148,163,184,.18); padding: 10px 8px; font-size: 14px; vertical-align: top; }}
208+
th {{ color:#b8c7dc; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }}
209+
a {{ color:var(--accent); text-decoration: none; }}
163210
a:hover {{ text-decoration: underline; }}
164-
.button {{ display:inline-block; margin: 8px 0 0; background:#1d4ed8; color:white; padding:8px 12px; border-radius:8px; }}
165-
pre {{ white-space: pre-wrap; background:#020617; border-radius:8px; padding:12px; border:1px solid #334155; overflow-x:auto; }}
211+
.button {{ display:inline-block; margin: 8px 0 0; background:linear-gradient(135deg, var(--accent-strong), #0891b2); color:white; padding:10px 14px; border-radius:12px; font-weight:700; box-shadow: 0 10px 30px rgba(37,99,235,.26); }}
212+
pre {{ font-family:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; background:#020617; border-radius:14px; padding:14px; border:1px solid var(--border); overflow-x:auto; }}
213+
.rich-report {{ padding: 0; overflow: hidden; }}
214+
.rich-report summary {{ cursor:pointer; list-style:none; display:flex; align-items:center; justify-content:space-between; gap: 16px; padding: 16px 18px; border-bottom:1px solid var(--border); font-weight:800; letter-spacing:-.02em; }}
215+
.rich-report summary::-webkit-details-marker {{ display:none; }}
216+
.rich-report summary small {{ color:var(--muted); font-weight:600; letter-spacing:0; }}
217+
.rich-report iframe {{ display:block; width:100%; min-height: 780px; border:0; background:#fff; }}
218+
.rich-report p {{ margin: 12px 18px 18px; }}
219+
.empty {{ padding: 18px; }}
166220
</style>
167221
</head>
168222
<body>
169223
<div class="wrap">
170224
<h1>Run #{html.escape(str(run_meta["run_number"]))} (attempt {html.escape(str(run_meta["run_attempt"]))})</h1>
171-
<p><a href="../../index.html">Back to dashboard</a> · <a href="{html.escape(run_meta["run_url"])}">GitHub Actions run</a></p>
172-
{pytest_html_link}
225+
<p class="subnav"><a href="../../index.html">Back to dashboard</a> · <a href="{html.escape(run_meta["run_url"])}">GitHub Actions run</a></p>
226+
{rich_report_panel}
173227
<div class="grid">
174228
<div class="card"><h2>Total</h2><p>{run_meta["summary"]["total"]}</p></div>
175229
<div class="card"><h2>Passed</h2><p>{by_status["passed"]}</p></div>
@@ -275,20 +329,24 @@ def _render_dashboard(site_dir: Path, runs: list[dict]) -> None:
275329
<meta name="viewport" content="width=device-width, initial-scale=1" />
276330
<title>tbsim validation dashboard</title>
277331
<style>
278-
:root {{ color-scheme: dark; }}
279-
body {{ font-family: Inter, ui-sans-serif, system-ui, sans-serif; margin: 0; background: linear-gradient(180deg, #030712, #0f172a); color:#e2e8f0; }}
280-
.wrap {{ max-width: 1400px; margin: 0 auto; padding: 24px; }}
281-
.hero {{ background: rgba(15, 23, 42, 0.7); border:1px solid #334155; border-radius: 14px; padding: 20px; }}
282-
h1, h2 {{ margin: 8px 0 12px; }}
283-
p {{ color:#cbd5e1; }}
284-
.badge {{ font-size: 12px; border-radius: 999px; padding: 3px 10px; font-weight: 600; text-transform: uppercase; }}
285-
.card {{ background: rgba(15, 23, 42, 0.72); border:1px solid #334155; border-radius: 12px; padding: 16px; margin-top: 14px; }}
332+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap');
333+
:root {{ color-scheme: dark; --bg:#070b16; --panel:rgba(15,23,42,.74); --border:rgba(148,163,184,.22); --text:#e5eefb; --muted:#9fb0c6; --accent:#7dd3fc; }}
334+
* {{ box-sizing: border-box; }}
335+
body {{ font-family: Inter, ui-sans-serif, system-ui, sans-serif; margin: 0; background: radial-gradient(circle at 15% 0%, rgba(59,130,246,.24), transparent 32rem), radial-gradient(circle at 85% 10%, rgba(14,165,233,.18), transparent 28rem), linear-gradient(180deg, #020617, var(--bg)); color:var(--text); }}
336+
.wrap {{ max-width: 1440px; margin: 0 auto; padding: 28px; }}
337+
.hero {{ background:linear-gradient(135deg, rgba(30,41,59,.88), rgba(15,23,42,.72)); border:1px solid var(--border); border-radius: 22px; padding: 26px; box-shadow: 0 24px 70px rgba(2,6,23,.36); backdrop-filter: blur(18px); }}
338+
h1 {{ font-size: clamp(2.2rem, 4vw, 4.6rem); letter-spacing: -.06em; margin: 6px 0 10px; }}
339+
h2 {{ margin: 8px 0 12px; letter-spacing: -.025em; }}
340+
p {{ color:var(--muted); }}
341+
.badge {{ font-size: 11px; border-radius: 999px; padding: 4px 10px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }}
342+
.card {{ background:linear-gradient(180deg, var(--panel), rgba(15,23,42,.56)); border:1px solid var(--border); border-radius: 18px; padding: 18px; margin-top: 16px; box-shadow: 0 18px 55px rgba(2,6,23,.32); backdrop-filter: blur(18px); }}
286343
table {{ width:100%; border-collapse: collapse; }}
287-
th, td {{ text-align:left; border-bottom:1px solid #334155; padding: 8px; font-size: 13px; }}
288-
a {{ color:#93c5fd; text-decoration: none; }}
344+
th, td {{ text-align:left; border-bottom:1px solid rgba(148,163,184,.18); padding: 10px 8px; font-size: 13px; }}
345+
th {{ color:#b8c7dc; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }}
346+
a {{ color:var(--accent); text-decoration: none; }}
289347
a:hover {{ text-decoration: underline; }}
290348
.matrix {{ overflow-x: auto; }}
291-
.dot {{ width: 11px; height: 11px; border-radius: 999px; display: inline-block; }}
349+
.dot {{ width: 12px; height: 12px; border-radius: 999px; display: inline-block; box-shadow: 0 0 0 3px rgba(255,255,255,.05); }}
292350
.small th, .small td {{ padding: 6px; font-size: 12px; }}
293351
</style>
294352
</head>
@@ -351,7 +409,16 @@ def main() -> None:
351409

352410
# Copy raw artifacts for direct inspection.
353411
junit_target = run_dir / "junit.xml"
354-
junit_target.write_text(args.junit.read_text(encoding="utf-8"), encoding="utf-8")
412+
if args.junit.exists():
413+
junit_target.write_text(args.junit.read_text(encoding="utf-8"), encoding="utf-8")
414+
else:
415+
junit_target.write_text(
416+
"<testsuites><testsuite errors='1' failures='0' skipped='0' tests='1'>"
417+
"<testcase classname='workflow' name='pytest-results'>"
418+
"<error message='JUnit XML was not produced'>The pytest JUnit XML artifact was not found.</error>"
419+
"</testcase></testsuite></testsuites>\n",
420+
encoding="utf-8",
421+
)
355422

356423
pytest_html_relpath = None
357424
if args.pytest_html and args.pytest_html.exists():

0 commit comments

Comments
 (0)