Wegwichtel/test-support/python_coverage_runner.py
Florian Zumpe 0b5c994957
Some checks failed
Sonarqube Scanner / Build and analyze (push) Failing after 27s
included coverage.py
2026-06-17 15:28:15 +02:00

144 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Erzeugt die Python-Coverage reproduzierbar ohne globale Paketinstallation."""
from __future__ import annotations
import hashlib
import os
from pathlib import Path
import shutil
import subprocess
import sys
import traceback
PROJECT_ROOT = Path(__file__).resolve().parents[1]
REQUIREMENTS_FILE = PROJECT_ROOT / "requirements-dev.txt"
DEPENDENCY_DIR = PROJECT_ROOT / ".python-test-deps"
REQUIREMENTS_MARKER = DEPENDENCY_DIR / ".requirements.sha256"
def _requirements_digest() -> str:
return hashlib.sha256(REQUIREMENTS_FILE.read_bytes()).hexdigest()
def _dependency_environment(include_local: bool) -> dict[str, str]:
env = os.environ.copy()
if include_local:
existing = env.get("PYTHONPATH", "")
paths = [str(DEPENDENCY_DIR)]
if existing:
paths.append(existing)
env["PYTHONPATH"] = os.pathsep.join(paths)
return env
def _coverage_available(env: dict[str, str]) -> bool:
check = subprocess.run(
[
sys.executable,
"-c",
"import coverage; assert hasattr(coverage, 'Coverage')",
],
cwd=PROJECT_ROOT,
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
return check.returncode == 0
def _local_dependencies_are_current() -> bool:
if not REQUIREMENTS_MARKER.is_file():
return False
return REQUIREMENTS_MARKER.read_text(encoding="ascii").strip() == _requirements_digest()
def _install_local_dependencies() -> dict[str, str]:
if DEPENDENCY_DIR.exists():
shutil.rmtree(DEPENDENCY_DIR)
DEPENDENCY_DIR.mkdir(parents=True)
subprocess.run(
[
sys.executable,
"-m",
"pip",
"install",
"--disable-pip-version-check",
"--no-warn-script-location",
"--target",
str(DEPENDENCY_DIR),
"--requirement",
str(REQUIREMENTS_FILE),
],
cwd=PROJECT_ROOT,
check=True,
)
REQUIREMENTS_MARKER.write_text(_requirements_digest() + "\n", encoding="ascii")
env = _dependency_environment(include_local=True)
if not _coverage_available(env):
raise RuntimeError("Das lokal installierte Python-Coverage-Modul kann nicht importiert werden.")
return env
def _coverage_environment() -> dict[str, str]:
system_env = _dependency_environment(include_local=False)
if _coverage_available(system_env):
return system_env
local_env = _dependency_environment(include_local=True)
if _local_dependencies_are_current() and _coverage_available(local_env):
return local_env
return _install_local_dependencies()
def _run_coverage(env: dict[str, str]) -> None:
commands = (
[sys.executable, "-m", "coverage", "erase"],
[
sys.executable,
"-m",
"coverage",
"run",
"-m",
"unittest",
"discover",
"-s",
"test/python",
"-p",
"test_*.py",
],
[
sys.executable,
"-m",
"coverage",
"xml",
"-o",
"coverage/python/coverage.xml",
],
[sys.executable, "-m", "coverage", "report"],
)
for command in commands:
subprocess.run(command, cwd=PROJECT_ROOT, env=env, check=True)
def main() -> int:
try:
_run_coverage(_coverage_environment())
except Exception as error: # noqa: BLE001 - CI-Ausgabe soll jeden unerwarteten Fehler enthalten.
print(
f"Python-Coverage fehlgeschlagen: {type(error).__name__}: {error}",
file=sys.stderr,
)
traceback.print_exc(file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())