mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
# Description of Changes This PR modernizes the project's Python tooling across GitHub Actions by migrating CI workflows from pip-based dependency management to `uv` and aligning Python execution with the engine project's managed environment. ### What was changed - Replaced `actions/setup-python` and ad-hoc `pip install` steps with `astral-sh/setup-uv` across CI workflows. - Configured shared `uv` dependency caching using `engine/pyproject.toml` and `engine/uv.lock`. - Updated Python script execution to use `uv run --project engine --locked` for a consistent runtime environment. - Replaced package installation steps with `uv sync` for the required dependency groups (e.g. `tools` and `cucumber`). - Added Docker image build validation for both production and development AI engine images. - Updated workflow cache configuration and Docker build context where required. - Removed obsolete Python requirements files that are no longer needed after the migration. - Applied minor Python code modernizations, including import cleanup, modern built-in generic type annotations (`list[...]`, `tuple[...]`, `float | None`), and small style improvements. - Removed unnecessary Python formatter/linter extensions from the development container configuration. ### Why the change was made - Standardize Python dependency management across the repository. - Reduce duplicated dependency installation logic in CI. - Improve workflow performance through shared dependency caching. - Ensure all Python utilities execute against the same locked dependency set managed by the engine project. - Simplify long-term maintenance by eliminating legacy requirements files and pip-specific workflow steps. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --------- Signed-off-by: Carsten Drewes <c.drewes@stud.uni-hannover.de> Co-authored-by: albanobattistella <34811668+albanobattistella@users.noreply.github.com> Co-authored-by: kastenherri <116314318+kastenherri@users.noreply.github.com> Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: James Brunton <jbrunton96@gmail.com>
171 lines
5.6 KiB
Python
171 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())
|