Files
iot-dc3/dc3-coverage/scripts/check_coverage.py

103 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""Validate the aggregate JaCoCo XML report against repository thresholds."""
from __future__ import annotations
import argparse
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class Counter:
name: str
missed: int
covered: int
@property
def total(self) -> int:
return self.missed + self.covered
@property
def ratio(self) -> float:
if self.total == 0:
return 1.0
return self.covered / self.total
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Fail the build if aggregate JaCoCo coverage is below threshold.",
)
parser.add_argument("xml_report", type=Path, help="Path to jacoco.xml")
parser.add_argument("--minimum-line", type=float, required=True)
parser.add_argument("--minimum-branch", type=float, required=True)
return parser.parse_args()
def read_counters(xml_report: Path) -> dict[str, Counter]:
if not xml_report.is_file():
raise FileNotFoundError(f"JaCoCo XML report does not exist: {xml_report}")
root = ET.parse(xml_report).getroot()
counters: dict[str, Counter] = {}
for element in root.findall("counter"):
name = element.attrib["type"]
counters[name] = Counter(
name=name,
missed=int(element.attrib["missed"]),
covered=int(element.attrib["covered"]),
)
return counters
def format_percent(value: float) -> str:
return f"{value * 100:.2f}%"
def check(counter: Counter, minimum: float) -> bool:
print(
f"Aggregate {counter.name.lower()} coverage: "
f"{format_percent(counter.ratio)} "
f"({counter.covered}/{counter.total}), "
f"minimum {format_percent(minimum)}"
)
return counter.ratio >= minimum
def main() -> int:
args = parse_args()
try:
counters = read_counters(args.xml_report)
except Exception as exc: # noqa: BLE001 - build tool should show the exact cause.
print(f"Coverage check failed: {exc}", file=sys.stderr)
return 1
required = {
"LINE": args.minimum_line,
"BRANCH": args.minimum_branch,
}
missing = [name for name in required if name not in counters]
if missing:
print(
f"Coverage check failed: missing counters {', '.join(missing)}",
file=sys.stderr,
)
return 1
passed = True
for name, minimum in required.items():
passed = check(counters[name], minimum) and passed
if not passed:
print("Coverage check failed: aggregate coverage is below threshold.", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())