mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
175 lines
5.6 KiB
Python
175 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://") or url.startswith("https://")):
|
|
return False
|
|
if any(frag in url for frag in SKIP_URL_FRAGMENTS):
|
|
return False
|
|
return True
|
|
|
|
|
|
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())
|