Files
Stirling-PDF/scripts/playwright-coverage-summary.py
T
Ludy87 c835f2da06 Update Ruff and lint cleanup
Bump Python tooling to Ruff 0.16.2 and datamodel-code-generator 0.72.3, add the py313 target in pre-commit, and refresh the engine lockfile. This also narrows broad exception handling and applies small Python cleanups across the engine and helper scripts to satisfy newer lint rules without changing runtime behavior.
2026-08-19 00:26:55 +02:00

169 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Aggregate per-test V8 coverage dumps from Playwright into one summary.
Playwright's test-base fixture writes raw `page.coverage.stopJSCoverage()`
output to a directory (one JSON file per test, when `PW_COVERAGE=1`).
This script walks that directory and produces:
- coverage-summary.json (vitest-shaped, so the existing
scripts/coverage-summary.py renderer can consume it without changes)
Method:
- V8 reports per-function coverage as a list of ranges; the first
range covers the whole function body, and a non-zero `count` on
that outer range means the function was entered at least once.
- We deduplicate functions across the merged dumps by (script-url,
startOffset, endOffset). This avoids double-counting a function
that ran in many tests.
- Per-file line coverage requires source maps + a `v8-to-istanbul`
style walker, which is out of scope here - we report
Lines/Statements as 0/0 (the helper renders that as "not computed",
matching how it handles vitest's known v8+SWC degradation).
Filtering:
- URLs not on the local dev server (e.g. CDN assets, blob: URLs,
chrome-extension: pages) are skipped.
- URLs containing `node_modules` or vite's HMR client are skipped so
the percentage reflects app code, not framework noise.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
# URLs we deliberately don't count: vite client, hot-reload runtime,
# anything served out of node_modules, and any non-http(s) scheme.
SKIP_URL_FRAGMENTS = (
"/@vite/",
"/@react-refresh",
"/node_modules/",
"/__vite_ping",
"/__open-in-editor",
)
def _is_app_url(url: str) -> bool:
if not url:
return False
if not (url.startswith(("http://", "https://"))):
return False
return not any(frag in url for frag in SKIP_URL_FRAGMENTS)
def aggregate(dump_dir: Path) -> dict:
files = sorted(dump_dir.glob("*.json"))
if not files:
return {
"tests": 0,
"scripts": 0,
"functions_total": 0,
"functions_covered": 0,
}
# (url, start, end) -> covered? Set semantics dedup the same function
# appearing in many test dumps.
seen: dict[tuple[str, int, int], bool] = {}
scripts_seen: set[str] = set()
for path in files:
try:
entries = json.loads(path.read_text())
except (OSError, json.JSONDecodeError):
continue
for entry in entries:
url = entry.get("url", "")
if not _is_app_url(url):
continue
scripts_seen.add(url)
for fn in entry.get("functions", []):
ranges = fn.get("ranges") or []
if not ranges:
continue
outer = ranges[0]
key = (
url,
int(outer.get("startOffset", 0)),
int(outer.get("endOffset", 0)),
)
covered = int(outer.get("count", 0)) > 0
# Promote to covered if any dump exercised it.
seen[key] = seen.get(key, False) or covered
return {
"tests": len(files),
"scripts": len(scripts_seen),
"functions_total": len(seen),
"functions_covered": sum(1 for v in seen.values() if v),
}
def write_vitest_summary(stats: dict, out_path: Path) -> None:
total = stats["functions_total"]
covered = stats["functions_covered"]
pct = 100.0 * covered / total if total else 0.0
# vitest coverage-summary.json schema. We report the function metric
# under both `functions` and `branches` so the helper script's
# "trust functions/branches" line still applies. Lines/Statements
# left at 0/0 so the renderer's degradation note kicks in.
payload = {
"total": {
"functions": {
"covered": covered,
"total": total,
"pct": pct,
"skipped": 0,
},
"branches": {
"covered": covered,
"total": total,
"pct": pct,
"skipped": 0,
},
"lines": {"covered": 0, "total": 0, "pct": 0.0, "skipped": 0},
"statements": {"covered": 0, "total": 0, "pct": 0.0, "skipped": 0},
}
}
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(payload, indent=2))
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"dump_dir",
type=Path,
help="Directory containing per-test V8 JSON dumps from the Playwright fixture.",
)
parser.add_argument(
"--out",
type=Path,
required=True,
help="Path to write the vitest-shaped coverage-summary.json",
)
args = parser.parse_args(argv)
if not args.dump_dir.exists():
print(
f"::warning::No Playwright coverage dump dir at {args.dump_dir}",
file=sys.stderr,
)
write_vitest_summary({"functions_total": 0, "functions_covered": 0}, args.out)
return 0
stats = aggregate(args.dump_dir)
write_vitest_summary(stats, args.out)
pct = 100.0 * stats["functions_covered"] / stats["functions_total"] if stats["functions_total"] else 0.0
print(
f"Aggregated {stats['tests']} tests / {stats['scripts']} scripts: "
f"{stats['functions_covered']}/{stats['functions_total']} functions "
f"({pct:.1f}%)"
)
return 0
if __name__ == "__main__":
sys.exit(main())