mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
feat: unify pdm test defaults and verbosity controls (#836)
This commit is contained in:
165
scripts/run_tests.py
Normal file
165
scripts/run_tests.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
"""Unified pytest runner for public and CI test execution.
|
||||
|
||||
This module invokes pytest via ``pytest.main()``. Programmatic callers should
|
||||
avoid repeated in-process invocations because Python's import cache can retain
|
||||
test module state between runs. CLI usage via ``pdm run`` is unaffected because
|
||||
each invocation runs in a fresh process.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT:Final = Path(__file__).resolve().parent.parent
|
||||
TEMP:Final = ROOT / ".temp"
|
||||
|
||||
# Most tests are currently unmarked, so utest intentionally uses negative markers
|
||||
# to select the default "unit-like" population while excluding integration/smoke.
|
||||
PROFILE_CONFIGS:Final[dict[str, tuple[str | None, str]]] = {
|
||||
"test": (None, "auto"),
|
||||
"utest": ("not itest and not smoke", "auto"),
|
||||
"itest": ("itest and not smoke", "0"),
|
||||
"smoke": ("smoke", "auto"),
|
||||
}
|
||||
|
||||
|
||||
def _append_verbosity(pytest_args:list[str], verbosity:int) -> None:
|
||||
if verbosity == 0:
|
||||
pytest_args.append("-q")
|
||||
else:
|
||||
pytest_args.append("-" + ("v" * verbosity))
|
||||
pytest_args.extend([
|
||||
"--durations=25",
|
||||
"--durations-min=0.5",
|
||||
])
|
||||
|
||||
|
||||
def _pytest_base_args(*, workers:str, verbosity:int) -> list[str]:
|
||||
# Stable pytest defaults (strict markers, doctest, coverage) live in pyproject addopts.
|
||||
# This runner only adds dynamic execution policy (workers and verbosity).
|
||||
pytest_args = [
|
||||
"-n",
|
||||
workers,
|
||||
]
|
||||
_append_verbosity(pytest_args, verbosity)
|
||||
return pytest_args
|
||||
|
||||
|
||||
def _resolve_path(path:Path) -> Path:
|
||||
if path.is_absolute():
|
||||
return path
|
||||
return ROOT / path
|
||||
|
||||
|
||||
def _display_path(path:Path) -> str:
|
||||
try:
|
||||
return str(path.relative_to(ROOT))
|
||||
except ValueError:
|
||||
return str(path)
|
||||
|
||||
|
||||
def _cleanup_coverage_artifacts() -> None:
|
||||
TEMP.mkdir(parents = True, exist_ok = True)
|
||||
for pattern in ("coverage-*.xml", ".coverage-*.sqlite"):
|
||||
for stale_file in TEMP.glob(pattern):
|
||||
stale_file.unlink(missing_ok = True)
|
||||
|
||||
for stale_path in (TEMP / "coverage.sqlite", ROOT / ".coverage"):
|
||||
stale_path.unlink(missing_ok = True)
|
||||
|
||||
|
||||
def _run_profile(*, profile:str, verbosity:int, passthrough:list[str]) -> int:
|
||||
marker, workers = PROFILE_CONFIGS[profile]
|
||||
pytest_args = _pytest_base_args(workers = workers, verbosity = verbosity)
|
||||
|
||||
if marker is not None:
|
||||
pytest_args.extend(["-m", marker])
|
||||
|
||||
pytest_args.extend(passthrough)
|
||||
return pytest.main(pytest_args)
|
||||
|
||||
|
||||
def _run_ci(*, marker:str, coverage_file:Path, xml_file:Path, workers:str, verbosity:int, passthrough:list[str]) -> int:
|
||||
resolved_coverage_file = _resolve_path(coverage_file)
|
||||
resolved_xml_file = _resolve_path(xml_file)
|
||||
resolved_coverage_file.parent.mkdir(parents = True, exist_ok = True)
|
||||
resolved_xml_file.parent.mkdir(parents = True, exist_ok = True)
|
||||
|
||||
previous_coverage_file = os.environ.get("COVERAGE_FILE")
|
||||
os.environ["COVERAGE_FILE"] = str(resolved_coverage_file)
|
||||
|
||||
pytest_args = _pytest_base_args(workers = workers, verbosity = verbosity)
|
||||
pytest_args.extend([
|
||||
"-m",
|
||||
marker,
|
||||
f"--cov-report=xml:{_display_path(resolved_xml_file)}",
|
||||
])
|
||||
pytest_args.extend(passthrough)
|
||||
try:
|
||||
return pytest.main(pytest_args)
|
||||
finally:
|
||||
if previous_coverage_file is None:
|
||||
os.environ.pop("COVERAGE_FILE", None)
|
||||
else:
|
||||
os.environ["COVERAGE_FILE"] = previous_coverage_file
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description = "Run project tests")
|
||||
subparsers = parser.add_subparsers(dest = "command", required = True)
|
||||
|
||||
run_parser = subparsers.add_parser("run", help = "Run tests for a predefined profile")
|
||||
run_parser.add_argument("profile", choices = sorted(PROFILE_CONFIGS))
|
||||
run_parser.add_argument("-v", "--verbose", action = "count", default = 0)
|
||||
|
||||
subparsers.add_parser("ci-prepare", help = "Clean stale coverage artifacts")
|
||||
|
||||
ci_run_parser = subparsers.add_parser("ci-run", help = "Run tests with explicit coverage outputs")
|
||||
ci_run_parser.add_argument("--marker", required = True)
|
||||
ci_run_parser.add_argument("--coverage-file", type = Path, required = True)
|
||||
ci_run_parser.add_argument("--xml-file", type = Path, required = True)
|
||||
ci_run_parser.add_argument("-n", "--workers", default = "auto")
|
||||
ci_run_parser.add_argument("-v", "--verbose", action = "count", default = 0)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv:list[str] | None = None) -> int:
|
||||
os.chdir(ROOT)
|
||||
effective_argv = sys.argv[1:] if argv is None else argv
|
||||
|
||||
parser = _build_parser()
|
||||
args, passthrough = parser.parse_known_args(effective_argv)
|
||||
|
||||
# This entrypoint is intended for one-shot CLI usage, not same-process
|
||||
# repeated invocations that can reuse imports loaded by pytest.main().
|
||||
if args.command == "run":
|
||||
return _run_profile(profile = args.profile, verbosity = args.verbose, passthrough = passthrough)
|
||||
|
||||
if args.command == "ci-prepare":
|
||||
_cleanup_coverage_artifacts()
|
||||
return 0
|
||||
|
||||
if args.command == "ci-run":
|
||||
return _run_ci(
|
||||
marker = args.marker,
|
||||
coverage_file = args.coverage_file,
|
||||
xml_file = args.xml_file,
|
||||
workers = args.workers,
|
||||
verbosity = args.verbose,
|
||||
passthrough = passthrough,
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user