Add test runner dashboard service (test.rose-ash.com)
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Public Quart microservice that runs pytest against shared/tests/ and shared/sexp/tests/, serving an HTMX-powered sexp-rendered dashboard with pass/fail/running status, auto-refresh polling, and re-run button. No database — results stored in memory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
105
test/sexp/sexp_components.py
Normal file
105
test/sexp/sexp_components.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""Test service s-expression page components."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from shared.sexp.jinja_bridge import render, load_service_components
|
||||
from shared.sexp.helpers import root_header_html, full_page
|
||||
|
||||
# Load test-specific .sexpr components at import time
|
||||
load_service_components(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
|
||||
def _format_time(ts: float | None) -> str:
|
||||
"""Format a unix timestamp for display."""
|
||||
if not ts:
|
||||
return "never"
|
||||
return datetime.fromtimestamp(ts).strftime("%-d %b %Y, %H:%M:%S")
|
||||
|
||||
|
||||
def _test_rows_html(tests: list[dict]) -> str:
|
||||
"""Render all test result rows."""
|
||||
parts = []
|
||||
for t in tests:
|
||||
parts.append(render(
|
||||
"test-row",
|
||||
nodeid=t["nodeid"],
|
||||
outcome=t["outcome"],
|
||||
duration=str(t["duration"]),
|
||||
longrepr=t.get("longrepr", ""),
|
||||
))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def _results_partial_html(result: dict | None, running: bool, csrf: str) -> str:
|
||||
"""Render the results section (summary + table or running indicator)."""
|
||||
if running and not result:
|
||||
summary = render(
|
||||
"test-summary",
|
||||
status="running", passed="0", failed="0", errors="0",
|
||||
skipped="0", total="0", duration="...",
|
||||
last_run="in progress", running=True, csrf=csrf,
|
||||
)
|
||||
return summary + render("test-running-indicator")
|
||||
|
||||
if not result:
|
||||
summary = render(
|
||||
"test-summary",
|
||||
status=None, passed="0", failed="0", errors="0",
|
||||
skipped="0", total="0", duration="0",
|
||||
last_run="never", running=running, csrf=csrf,
|
||||
)
|
||||
return summary + render("test-no-results")
|
||||
|
||||
status = "running" if running else result["status"]
|
||||
summary = render(
|
||||
"test-summary",
|
||||
status=status,
|
||||
passed=str(result["passed"]),
|
||||
failed=str(result["failed"]),
|
||||
errors=str(result["errors"]),
|
||||
skipped=str(result.get("skipped", 0)),
|
||||
total=str(result["total"]),
|
||||
duration=str(result["duration"]),
|
||||
last_run=_format_time(result["finished_at"]) if not running else "in progress",
|
||||
running=running,
|
||||
csrf=csrf,
|
||||
)
|
||||
|
||||
if running:
|
||||
return summary + render("test-running-indicator")
|
||||
|
||||
tests = result.get("tests", [])
|
||||
if not tests:
|
||||
return summary + render("test-no-results")
|
||||
|
||||
has_failures = result["failed"] > 0 or result["errors"] > 0
|
||||
rows = _test_rows_html(tests)
|
||||
table = render("test-results-table", rows_html=rows,
|
||||
has_failures=str(has_failures).lower())
|
||||
return summary + table
|
||||
|
||||
|
||||
def _wrap_results_div(inner_html: str, running: bool) -> str:
|
||||
"""Wrap results in a div with HTMX polling when running."""
|
||||
attrs = 'id="test-results" class="space-y-6 p-4"'
|
||||
if running:
|
||||
attrs += ' hx-get="/results" hx-trigger="every 2s" hx-swap="outerHTML"'
|
||||
return f'<div {attrs}>{inner_html}</div>'
|
||||
|
||||
|
||||
async def render_dashboard_page(ctx: dict, result: dict | None,
|
||||
running: bool, csrf: str) -> str:
|
||||
"""Full page: test dashboard."""
|
||||
hdr = root_header_html(ctx)
|
||||
inner = _results_partial_html(result, running, csrf)
|
||||
content = _wrap_results_div(inner, running)
|
||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||
|
||||
|
||||
async def render_results_partial(result: dict | None, running: bool,
|
||||
csrf: str) -> str:
|
||||
"""HTMX partial: just the results section (wrapped in polling div)."""
|
||||
inner = _results_partial_html(result, running, csrf)
|
||||
return _wrap_results_div(inner, running)
|
||||
Reference in New Issue
Block a user