mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Add CI coverage summaries and aggregate JaCoCo report (#6451)
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user