mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
chore: improve coverage reporting (#683)
## ℹ️ Description * Restrict coverage reporting to library files and collect per-suite coverage data for Codecov’s flags. - Link to the related issue(s): Issue #N/A - Describe the motivation and context for this change. ## 📋 Changes Summary - add `coverage:prepare` and per-suite `COVERAGE_FILE`s so each test group writes its own sqlite and XML artifacts without appending - replace the shell scripts with `scripts/coverage_helper.py`, scope the report to `src/kleinanzeigen_bot/*`, and add logging/validation around cleanup, pytest runs, and data combining - ensure the helper works in CI (accepts extra pytest args, validates file presence) ### ⚙️ Type of Change - [x] 🐞 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (adds new functionality without breaking existing usage) - [ ] 💥 Breaking change (changes that might break existing user setups, scripts, or configurations) ## ✅ Checklist - [x] I have reviewed my changes to ensure they meet the project's standards. - [x] I have tested my changes and ensured that all tests pass (`pdm run test`). - [x] I have formatted the code (`pdm run format`). - [x] I have verified that linting passes (`pdm run lint`). - [x] I have updated documentation where necessary.
This commit is contained in:
@@ -123,11 +123,12 @@ test = { composite = ["utest", "itest", "smoke"] }
|
||||
# Coverage scripts:
|
||||
# - Each group writes its own data file to .temp/.coverage.<group>.xml
|
||||
#
|
||||
"test:cov" = { composite = ["utest:cov", "itest:cov", "smoke:cov", "coverage:combine"] }
|
||||
"utest:cov" = { shell = "python -m pytest --capture=tee-sys -m \"not itest and not smoke\" --cov=src/kleinanzeigen_bot --cov-report=xml:.temp/coverage-unit.xml --cov-append" }
|
||||
"itest:cov" = { shell = "python -m pytest --capture=tee-sys -m \"itest and not smoke\" --cov=src/kleinanzeigen_bot --cov-report=xml:.temp/coverage-integration.xml --cov-append" }
|
||||
"smoke:cov" = { shell = "python -m pytest --capture=tee-sys -m smoke --cov=src/kleinanzeigen_bot --cov-report=xml:.temp/coverage-smoke.xml --cov-append" }
|
||||
"coverage:combine" = { shell = "coverage report -m" }
|
||||
"coverage:prepare" = { shell = "python scripts/coverage_helper.py prepare" }
|
||||
"test:cov" = { composite = ["coverage:prepare", "utest:cov", "itest:cov", "smoke:cov", "coverage:combine"] }
|
||||
"utest:cov" = { shell = "python scripts/coverage_helper.py run .temp/.coverage-unit.sqlite .temp/coverage-unit.xml \"not itest and not smoke\"" }
|
||||
"itest:cov" = { shell = "python scripts/coverage_helper.py run .temp/.coverage-itest.sqlite .temp/coverage-integration.xml \"itest and not smoke\"" }
|
||||
"smoke:cov" = { shell = "python scripts/coverage_helper.py run .temp/.coverage-smoke.sqlite .temp/coverage-smoke.xml smoke" }
|
||||
"coverage:combine" = { shell = "python scripts/coverage_helper.py combine .temp/.coverage-unit.sqlite .temp/.coverage-itest.sqlite .temp/.coverage-smoke.sqlite" }
|
||||
# Run all tests with coverage in a single invocation
|
||||
"test:cov:unified" = "python -m pytest --capture=tee-sys --cov=src/kleinanzeigen_bot --cov-report=term-missing"
|
||||
|
||||
@@ -375,6 +376,7 @@ relative_files = true
|
||||
precision = 2
|
||||
show_missing = true
|
||||
skip_covered = false
|
||||
include = ["src/kleinanzeigen_bot/*"]
|
||||
|
||||
#####################
|
||||
# yamlfix
|
||||
|
||||
116
scripts/coverage_helper.py
Normal file
116
scripts/coverage_helper.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Utility helpers for the coverage pipeline used by the pdm test scripts."""
|
||||
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import subprocess # noqa: S404 subprocess usage is limited to known internal binaries
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
TEMP = ROOT / ".temp"
|
||||
|
||||
logging.basicConfig(level = logging.INFO, format = "%(asctime)s %(levelname)s %(name)s %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def prepare() -> None:
|
||||
logger.info("Preparing coverage artifacts in %s", TEMP)
|
||||
try:
|
||||
TEMP.mkdir(parents = True, exist_ok = True)
|
||||
removed_patterns = 0
|
||||
for pattern in ("coverage-*.xml", ".coverage-*.sqlite"):
|
||||
for coverage_file in TEMP.glob(pattern):
|
||||
coverage_file.unlink()
|
||||
removed_patterns += 1
|
||||
removed_paths = 0
|
||||
for path in (TEMP / "coverage.sqlite", ROOT / ".coverage"):
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
removed_paths += 1
|
||||
except Exception as exc: # noqa: S110 suppress to log
|
||||
logger.exception("Failed to clean coverage artifacts: %s", exc)
|
||||
raise
|
||||
logger.info(
|
||||
"Removed %d pattern-matching files and %d fixed paths during prepare",
|
||||
removed_patterns,
|
||||
removed_paths,
|
||||
)
|
||||
|
||||
|
||||
def run_suite(data_file:Path, xml_file:Path, marker:str, extra_args:list[str]) -> None:
|
||||
os.environ["COVERAGE_FILE"] = str(ROOT / data_file)
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pytest",
|
||||
"--capture=tee-sys",
|
||||
"-m",
|
||||
marker,
|
||||
"--cov=src/kleinanzeigen_bot",
|
||||
f"--cov-report=xml:{ROOT / xml_file}",
|
||||
]
|
||||
if extra_args:
|
||||
cmd.extend(extra_args)
|
||||
logger.info("Running pytest marker=%s coverage_data=%s xml=%s", marker, data_file, xml_file)
|
||||
subprocess.run(cmd, cwd = ROOT, check = True) # noqa: S603 arguments are constant and controlled
|
||||
logger.info("Pytest marker=%s finished", marker)
|
||||
|
||||
|
||||
def combine(data_files:list[Path]) -> None:
|
||||
combined = TEMP / "coverage.sqlite"
|
||||
os.environ["COVERAGE_FILE"] = str(combined)
|
||||
resolved = []
|
||||
missing = []
|
||||
for data in data_files:
|
||||
candidate = ROOT / data
|
||||
if not candidate.exists():
|
||||
missing.append(str(candidate))
|
||||
else:
|
||||
resolved.append(candidate)
|
||||
if missing:
|
||||
message = f"Coverage data files missing: {', '.join(missing)}"
|
||||
logger.error(message)
|
||||
raise FileNotFoundError(message)
|
||||
cmd = [sys.executable, "-m", "coverage", "combine"] + [str(path) for path in resolved]
|
||||
logger.info("Combining coverage data files: %s", ", ".join(str(path) for path in resolved))
|
||||
subprocess.run(cmd, cwd = ROOT, check = True) # noqa: S603 arguments controlled by this script
|
||||
logger.info("Coverage combine completed, generating report")
|
||||
subprocess.run([sys.executable, "-m", "coverage", "report", "-m"], cwd = ROOT, check = True) # noqa: S603
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description = "Coverage helper commands")
|
||||
subparsers = parser.add_subparsers(dest = "command", required = True)
|
||||
|
||||
subparsers.add_parser("prepare", help = "Clean coverage artifacts")
|
||||
|
||||
run_parser = subparsers.add_parser("run", help = "Run pytest with a custom coverage file")
|
||||
run_parser.add_argument("data_file", type = Path, help = "Coverage data file to write")
|
||||
run_parser.add_argument("xml_file", type = Path, help = "XML report path")
|
||||
run_parser.add_argument("marker", help = "pytest marker expression")
|
||||
|
||||
combine_parser = subparsers.add_parser("combine", help = "Combine coverage data files")
|
||||
combine_parser.add_argument(
|
||||
"data_files",
|
||||
nargs = "+",
|
||||
type = Path,
|
||||
help = "List of coverage data files to combine",
|
||||
)
|
||||
|
||||
args, extra_args = parser.parse_known_args()
|
||||
|
||||
if args.command == "prepare":
|
||||
prepare()
|
||||
elif args.command == "run":
|
||||
run_suite(args.data_file, args.xml_file, args.marker, extra_args)
|
||||
else:
|
||||
combine(args.data_files)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user