#!/usr/bin/env python3 from __future__ import annotations import argparse import html import json import sys from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Any def normalize_report_index(report_dir: str, report_index: str | None) -> str: canonical_index = f"{report_dir}/html/index.html" if report_index is None: return canonical_index normalized_index = report_index.lstrip("/") if normalized_index in { "index.html", "html/index.html", f"{report_dir}/index.html", }: return canonical_index return normalized_index @dataclass class CoverageRun: manifest_path: Path data: dict[str, Any] generated_dt: datetime @property def branch(self) -> str: return str(self.data.get("branch", "unknown")) @property def sha(self) -> str: return str(self.data.get("sha", "")) @property def short_sha(self) -> str: return str(self.data.get("short_sha", self.sha[:12])) @property def run_id(self) -> str: return str(self.data.get("run_id", "")) @property def report_dir(self) -> str: return str(self.data.get("report_dir", self.short_sha)) @property def report_index(self) -> str: raw_report_index = self.data.get("report_index") return normalize_report_index(self.report_dir, None if raw_report_index is None else str(raw_report_index)) @property def lcov_path(self) -> str: return str(self.data.get("lcov_path", f"{self.report_dir}/lcov.info")) @property def line_rate(self) -> float: return float(self.data.get("summary", {}).get("lines", {}).get("rate", 0.0)) @property def line_hit(self) -> int: return int(self.data.get("summary", {}).get("lines", {}).get("hit", 0)) @property def line_found(self) -> int: return int(self.data.get("summary", {}).get("lines", {}).get("found", 0)) @property def function_rate(self) -> float: return float(self.data.get("summary", {}).get("functions", {}).get("rate", 0.0)) @property def function_hit(self) -> int: return int(self.data.get("summary", {}).get("functions", {}).get("hit", 0)) @property def function_found(self) -> int: return int(self.data.get("summary", {}).get("functions", {}).get("found", 0)) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--site-root", required=True) parser.add_argument("--manifest-root", required=True) parser.add_argument("--report-base-path", default="/storage/reports") parser.add_argument("--site-title", default="Cloud Storage Engine Coverage History") parser.add_argument("--github-base-url", default="https://github.com") return parser.parse_args() def load_runs(manifest_root: Path) -> list[CoverageRun]: runs: list[CoverageRun] = [] for manifest_path in manifest_root.rglob("*.manifest.json"): try: data = json.loads(manifest_path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: print(f"Warning: skipping corrupted manifest {manifest_path}: {exc}", file=sys.stderr) continue generated_at = str(data.get("generated_at", "")) try: generated_dt = datetime.strptime(generated_at, "%Y%m%dT%H%M%SZ") except ValueError as exc: print(f"Warning: skipping manifest {manifest_path} with invalid generated_at '{generated_at}': {exc}", file=sys.stderr) continue runs.append(CoverageRun(manifest_path=manifest_path, data=data, generated_dt=generated_dt)) runs.sort(key=lambda run: run.generated_dt, reverse=True) return runs def relative_href(base_path: str, relative_path: str) -> str: return f"{base_path.rstrip('/')}/{relative_path.lstrip('/')}" def write_latest_redirect(site_root: Path, latest_href: str) -> None: latest_dir = site_root / "latest" latest_dir.mkdir(parents=True, exist_ok=True) latest_dir.joinpath("index.html").write_text( """ Latest coverage report

Redirecting to the latest coverage report: {href}

""".format(href=html.escape(latest_href, quote=True)), encoding="utf-8", ) def build_rows(runs: list[CoverageRun], report_base_path: str) -> tuple[list[dict[str, Any]], str]: previous_by_branch: dict[str, CoverageRun] = {} rows: list[dict[str, Any]] = [] # Iterate chronologically (oldest first) so delta is calculated correctly for run in reversed(runs): previous_run = previous_by_branch.get(run.branch) delta = None if previous_run is None else round(run.line_rate - previous_run.line_rate, 2) report_href = relative_href(report_base_path, run.report_index) lcov_href = relative_href(report_base_path, run.lcov_path) row = { "generated_at": run.generated_dt.strftime("%Y-%m-%d %H:%M:%S UTC"), "branch": run.branch, "sha": run.sha, "short_sha": run.short_sha, "run_id": run.run_id, "repository": run.data.get("repository", ""), "line_rate": run.line_rate, "line_hit": run.line_hit, "line_found": run.line_found, "function_rate": run.function_rate, "function_hit": run.function_hit, "function_found": run.function_found, "delta_to_previous": delta, "report_href": report_href, "lcov_href": lcov_href, } rows.append(row) previous_by_branch[run.branch] = run # Reverse back to newest-first for display and capture latest_href rows.reverse() stable_latest_href = next( (row["report_href"] for row in rows if row["branch"] in ("master", "main")), None, ) latest_href = stable_latest_href if stable_latest_href is not None else (rows[0]["report_href"] if rows else "/") return rows, latest_href def render_html(title: str, rows: list[dict[str, Any]], latest_href: str, github_base_url: str) -> str: escaped_title = html.escape(title) if not rows: body = "

No coverage reports have been synced yet.

" else: table_rows = [] for row in rows: delta = row["delta_to_previous"] delta_text = "—" if delta is None else f"{delta:+.2f}%" table_rows.append( "" f"{html.escape(row['generated_at'])}" f"{html.escape(row['branch'])}" f"{html.escape(row['short_sha'])}" f"{row['line_rate']:.2f}% ({row['line_hit']}/{row['line_found']})" f"{row['function_rate']:.2f}% ({row['function_hit']}/{row['function_found']})" f"{delta_text}" f"{html.escape(row['run_id'])}" f"report" f"lcov" "" ) body = ( f'

Open latest report

' "" "" f"{''.join(table_rows)}" "
GeneratedBranchSHALine coverageFunction coverageΔ previousRunHTMLLCOV
" ) return f""" {escaped_title}

{escaped_title}

This page is generated from synced coverage manifests in object storage. Each report tarball is expanded under an immutable short-sha directory.

{body} """ def main() -> None: args = parse_args() site_root = Path(args.site_root) manifest_root = Path(args.manifest_root) site_root.mkdir(parents=True, exist_ok=True) manifest_root.mkdir(parents=True, exist_ok=True) runs = load_runs(manifest_root) rows, latest_href = build_rows(runs, args.report_base_path) site_root.joinpath("index.json").write_text( json.dumps({"title": args.site_title, "latest": latest_href, "runs": rows}, indent=2) + "\n", encoding="utf-8", ) site_root.joinpath("index.html").write_text( render_html(args.site_title, rows, latest_href, args.github_base_url), encoding="utf-8", ) write_latest_redirect(site_root, latest_href) if __name__ == "__main__": main()