"""HTML report generator for SynapBus E2E test results.""" from __future__ import annotations import html import json import os import time from typing import Any, Dict, List, Optional from .agent_runner import TestResult, AgentRun, ToolCall def _truncate(s: str, max_len: int = 500) -> str: if len(s) <= max_len: return s return s[:max_len] + "..." def _json_pretty(data: Any, max_len: int = 500) -> str: try: raw = json.dumps(data, indent=2, default=str) except (TypeError, ValueError): raw = str(data) return html.escape(_truncate(raw, max_len)) def _status_badge(status: str) -> str: colors = { "pass": "#10b981", "fail": "#ef4444", "error": "#f59e0b", } bg = colors.get(status, "#6b7280") return '{label}'.format( bg=bg, label=html.escape(status.upper())) def _bool_icon(passed: bool) -> str: if passed: return 'PASS' return 'FAIL' def _format_duration(seconds: float) -> str: if seconds < 1: return "{:.0f}ms".format(seconds * 1000) return "{:.1f}s".format(seconds) def _format_tokens(n: int) -> str: if n >= 1000: return "{:.1f}k".format(n / 1000) return str(n) def _estimate_cost(input_tokens: int, output_tokens: int, model: str) -> float: """Estimate cost in USD. Sonnet pricing: $3/M input, $15/M output.""" if "opus" in model: return (input_tokens * 15 + output_tokens * 75) / 1_000_000 if "haiku" in model: return (input_tokens * 0.25 + output_tokens * 1.25) / 1_000_000 # Default Sonnet return (input_tokens * 3 + output_tokens * 15) / 1_000_000 def generate_report( results: List[TestResult], model: str, base_url: str, total_duration: float, output_path: str, ) -> str: """Generate a self-contained HTML report file.""" passed = sum(1 for r in results if r.status == "pass") failed = sum(1 for r in results if r.status == "fail") errored = sum(1 for r in results if r.status == "error") total_input = sum(r.total_input_tokens for r in results) total_output = sum(r.total_output_tokens for r in results) total_cost = _estimate_cost(total_input, total_output, model) total_tool_calls = sum(len(tc) for r in results for run in r.runs for tc in [run.tool_calls]) scenarios_html = [] for idx, result in enumerate(results): scenarios_html.append(_render_scenario(result, idx, model)) report_html = TEMPLATE.format( timestamp=time.strftime("%Y-%m-%d %H:%M:%S"), model=html.escape(model), base_url=html.escape(base_url), total=len(results), passed=passed, failed=failed, errored=errored, total_duration=_format_duration(total_duration), total_input=_format_tokens(total_input), total_output=_format_tokens(total_output), total_cost="{:.4f}".format(total_cost), total_tool_calls=total_tool_calls, scenarios="\n".join(scenarios_html), pass_rate="{:.0f}".format(100 * passed / len(results)) if results else "0", ) with open(output_path, "w") as f: f.write(report_html) return output_path def _render_scenario(result: TestResult, idx: int, model: str) -> str: cost = _estimate_cost(result.total_input_tokens, result.total_output_tokens, model) # Render verifications verifications_html = "" if result.verifications: rows = [] for v in result.verifications: rows.append( '
| Status | Check | Detail |
|---|
{error}
{description}
{system_prompt}
{user_prompt}
{response}
Generated {timestamp} | Model: {model} | Server: {base_url}