All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s
- Server sends sexp source text, client (sexp.js) renders everything - SexpExpr marker class for nested sexp composition in serialize() - sexp_page() HTML shell with data-mount="body" for full page loads - sexp_response() returns text/sexp for OOB/partial responses - ~app-body layout component replaces ~app-layout (no raw!) - ~rich-text is the only component using raw! (for CMS HTML content) - Fragment endpoints return text/sexp, auto-wrapped in SexpExpr - All _*_html() helpers converted to _*_sexp() returning sexp source - Head auto-hoist: sexp.js moves meta/title/link/script[ld+json] from rendered body to document.head automatically - Unknown components render warning box instead of crashing page - Component kwargs preserve AST for lazy rendering (fixes <> in kwargs) - Fix unterminated paren in events/sexp/tickets.sexpr Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
239 lines
8.7 KiB
Python
239 lines
8.7 KiB
Python
"""Test service s-expression page components."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from datetime import datetime
|
|
|
|
from shared.sexp.jinja_bridge import load_service_components
|
|
from shared.sexp.helpers import (
|
|
sexp_call, SexpExpr,
|
|
root_header_sexp, full_page_sexp, header_child_sexp,
|
|
)
|
|
|
|
# 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")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Menu / header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_FILTER_MAP = {
|
|
"passed": "passed",
|
|
"failed": "failed",
|
|
"errors": "error",
|
|
"skipped": "skipped",
|
|
}
|
|
|
|
|
|
def _filter_tests(tests: list[dict], active_filter: str | None,
|
|
active_service: str | None) -> list[dict]:
|
|
"""Filter tests by outcome and/or service."""
|
|
from runner import _service_from_nodeid
|
|
filtered = tests
|
|
if active_filter and active_filter in _FILTER_MAP:
|
|
outcome = _FILTER_MAP[active_filter]
|
|
filtered = [t for t in filtered if t["outcome"] == outcome]
|
|
if active_service:
|
|
filtered = [t for t in filtered if _service_from_nodeid(t["nodeid"]) == active_service]
|
|
return filtered
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Results partial
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_detail_sexp(test: dict) -> str:
|
|
"""Return s-expression wire format for a test detail view."""
|
|
inner = sexp_call(
|
|
"test-detail",
|
|
nodeid=test["nodeid"],
|
|
outcome=test["outcome"],
|
|
duration=str(test["duration"]),
|
|
longrepr=test.get("longrepr", ""),
|
|
)
|
|
return (
|
|
f'(section :id "main-panel"'
|
|
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
|
|
f' {inner})'
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sexp-native versions — return sexp source (not HTML)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _test_header_sexp(ctx: dict, active_service: str | None = None) -> str:
|
|
"""Build the Tests menu-row as sexp call."""
|
|
nav = _service_nav_sexp(ctx, active_service)
|
|
return sexp_call("menu-row-sx",
|
|
id="test-row", level=1, colour="sky",
|
|
link_href="/", link_label="Tests", icon="fa fa-flask",
|
|
nav=SexpExpr(nav),
|
|
child_id="test-header-child",
|
|
)
|
|
|
|
|
|
def _service_nav_sexp(ctx: dict, active_service: str | None = None) -> str:
|
|
"""Service filter nav as sexp."""
|
|
from runner import _SERVICE_ORDER
|
|
parts = []
|
|
parts.append(sexp_call("nav-link",
|
|
href="/", label="all",
|
|
is_selected="true" if not active_service else None,
|
|
select_colours="aria-selected:bg-sky-200 aria-selected:text-sky-900",
|
|
))
|
|
for svc in _SERVICE_ORDER:
|
|
parts.append(sexp_call("nav-link",
|
|
href=f"/?service={svc}", label=svc,
|
|
is_selected="true" if active_service == svc else None,
|
|
select_colours="aria-selected:bg-sky-200 aria-selected:text-sky-900",
|
|
))
|
|
return "(<> " + " ".join(parts) + ")"
|
|
|
|
|
|
def _header_stack_sexp(ctx: dict, active_service: str | None = None) -> str:
|
|
"""Full header stack as sexp."""
|
|
hdr = root_header_sexp(ctx)
|
|
inner = _test_header_sexp(ctx, active_service)
|
|
child = header_child_sexp(inner)
|
|
return "(<> " + hdr + " " + child + ")"
|
|
|
|
|
|
def _test_rows_sexp(tests: list[dict]) -> str:
|
|
"""Render all test result rows as sexp."""
|
|
parts = []
|
|
for t in tests:
|
|
parts.append(sexp_call("test-row",
|
|
nodeid=t["nodeid"],
|
|
outcome=t["outcome"],
|
|
duration=str(t["duration"]),
|
|
longrepr=t.get("longrepr", ""),
|
|
))
|
|
return "(<> " + " ".join(parts) + ")"
|
|
|
|
|
|
def _grouped_rows_sexp(tests: list[dict]) -> str:
|
|
"""Test rows grouped by service as sexp."""
|
|
from runner import group_tests_by_service
|
|
sections = group_tests_by_service(tests)
|
|
parts = []
|
|
for sec in sections:
|
|
parts.append(sexp_call("test-service-header",
|
|
service=sec["service"],
|
|
total=str(sec["total"]),
|
|
passed=str(sec["passed"]),
|
|
failed=str(sec["failed"]),
|
|
))
|
|
parts.append(_test_rows_sexp(sec["tests"]))
|
|
return "(<> " + " ".join(parts) + ")"
|
|
|
|
|
|
def _results_partial_sexp(result: dict | None, running: bool, csrf: str,
|
|
active_filter: str | None = None,
|
|
active_service: str | None = None) -> str:
|
|
"""Results section as sexp."""
|
|
if running and not result:
|
|
summary = sexp_call("test-summary",
|
|
status="running", passed="0", failed="0", errors="0",
|
|
skipped="0", total="0", duration="...",
|
|
last_run="in progress", running=True, csrf=csrf,
|
|
active_filter=active_filter,
|
|
)
|
|
return "(<> " + summary + " " + sexp_call("test-running-indicator") + ")"
|
|
|
|
if not result:
|
|
summary = sexp_call("test-summary",
|
|
status=None, passed="0", failed="0", errors="0",
|
|
skipped="0", total="0", duration="0",
|
|
last_run="never", running=running, csrf=csrf,
|
|
active_filter=active_filter,
|
|
)
|
|
return "(<> " + summary + " " + sexp_call("test-no-results") + ")"
|
|
|
|
status = "running" if running else result["status"]
|
|
summary = sexp_call("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,
|
|
active_filter=active_filter,
|
|
)
|
|
|
|
if running:
|
|
return "(<> " + summary + " " + sexp_call("test-running-indicator") + ")"
|
|
|
|
tests = result.get("tests", [])
|
|
tests = _filter_tests(tests, active_filter, active_service)
|
|
if not tests:
|
|
return "(<> " + summary + " " + sexp_call("test-no-results") + ")"
|
|
|
|
has_failures = result["failed"] > 0 or result["errors"] > 0
|
|
rows = _grouped_rows_sexp(tests)
|
|
table = sexp_call("test-results-table",
|
|
rows=SexpExpr(rows),
|
|
has_failures=str(has_failures).lower(),
|
|
)
|
|
return "(<> " + summary + " " + table + ")"
|
|
|
|
|
|
def _wrap_results_div_sexp(inner: str, running: bool) -> str:
|
|
"""Wrap results in a div with HTMX polling (sexp)."""
|
|
attrs = ':id "test-results" :class "space-y-6 p-4"'
|
|
if running:
|
|
attrs += ' :sx-get "/results" :sx-trigger "every 2s" :sx-swap "outerHTML"'
|
|
return f'(div {attrs} {inner})'
|
|
|
|
|
|
async def render_dashboard_page_sexp(ctx: dict, result: dict | None,
|
|
running: bool, csrf: str,
|
|
active_filter: str | None = None,
|
|
active_service: str | None = None) -> str:
|
|
"""Full page: test dashboard (sexp wire format)."""
|
|
hdr = _header_stack_sexp(ctx, active_service)
|
|
inner = _results_partial_sexp(result, running, csrf, active_filter, active_service)
|
|
content = _wrap_results_div_sexp(inner, running)
|
|
return full_page_sexp(ctx, header_rows=hdr, content=content)
|
|
|
|
|
|
async def render_results_partial_sexp(result: dict | None, running: bool,
|
|
csrf: str,
|
|
active_filter: str | None = None,
|
|
active_service: str | None = None) -> str:
|
|
"""HTMX partial: results section (sexp wire format)."""
|
|
inner = _results_partial_sexp(result, running, csrf, active_filter, active_service)
|
|
return _wrap_results_div_sexp(inner, running)
|
|
|
|
|
|
async def render_test_detail_page_sexp(ctx: dict, test: dict) -> str:
|
|
"""Full page: test detail (sexp wire format)."""
|
|
root_hdr = root_header_sexp(ctx)
|
|
test_row = _test_header_sexp(ctx)
|
|
detail_row = sexp_call("menu-row-sx",
|
|
id="test-detail-row", level=2, colour="sky",
|
|
link_href=f"/test/{test['nodeid']}",
|
|
link_label=test["nodeid"].rsplit("::", 1)[-1],
|
|
)
|
|
inner = "(<> " + test_row + " " + header_child_sexp(detail_row, id="test-header-child") + ")"
|
|
hdr = "(<> " + root_hdr + " " + header_child_sexp(inner) + ")"
|
|
content = sexp_call("test-detail",
|
|
nodeid=test["nodeid"],
|
|
outcome=test["outcome"],
|
|
duration=str(test["duration"]),
|
|
longrepr=test.get("longrepr", ""),
|
|
)
|
|
return full_page_sexp(ctx, header_rows=hdr, content=content)
|