-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathci_coverage_delta.py
More file actions
executable file
·111 lines (90 loc) · 3.41 KB
/
Copy pathci_coverage_delta.py
File metadata and controls
executable file
·111 lines (90 loc) · 3.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#!/usr/bin/env python
"""Compute coverage delta against a baseline for CI reporting."""
from __future__ import annotations
import datetime as _dt
import json
import os
import sys
from pathlib import Path
from typing import Any
from xml.etree import ElementTree as ET
_DEFAULT_COVERAGE_XML = "coverage.xml"
_DEFAULT_OUTPUT = "coverage-delta.json"
_DEFAULT_BASELINE = 0.0
_DEFAULT_ALERT_DROP = 1.0
def _parse_float(value: str | None, env_name: str, default: float) -> float:
if value is None or value == "":
return default
try:
return float(value)
except ValueError as exc: # pragma: no cover - defensive
raise SystemExit(f"Invalid float for {env_name}: {value!r}") from exc
def _truthy(value: str | None) -> bool:
if value is None:
return False
return value.lower() in {"1", "true", "yes", "on"}
def _extract_line_rate(xml_path: Path) -> float:
if not xml_path.is_file():
raise FileNotFoundError(f"Coverage XML not found: {xml_path}")
try:
root = ET.parse(xml_path).getroot()
except ET.ParseError as exc: # pragma: no cover - malformed coverage report is rare
raise SystemExit(f"Failed to parse coverage XML {xml_path}: {exc}") from exc
raw = root.attrib.get("line-rate")
if raw is None:
raise SystemExit(f"Coverage XML {xml_path} missing line-rate attribute")
try:
return float(raw) * 100.0
except ValueError as exc: # pragma: no cover - defensive
raise SystemExit(f"Invalid line-rate value in coverage XML: {raw!r}") from exc
def _build_payload(
current: float,
baseline: float,
alert_drop: float,
*,
fail_on_drop: bool,
) -> tuple[dict[str, Any], bool]:
timestamp = _dt.datetime.now(_dt.UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
drop = max(0.0, baseline - current) if baseline > 0 else 0.0
delta = current - baseline
status: str
should_fail = False
if baseline <= 0:
status = "no-baseline"
elif drop == 0:
status = "ok"
elif drop >= alert_drop:
status = "fail" if fail_on_drop else "alert"
should_fail = status == "fail"
else:
status = "ok"
payload = {
"timestamp": timestamp,
"current": round(current, 4),
"baseline": round(baseline, 4),
"delta": round(delta, 4),
"drop": round(drop, 4),
"threshold": alert_drop,
"status": status,
"fail_on_drop": fail_on_drop,
}
return payload, should_fail
def main() -> int:
xml_path = Path(os.environ.get("COVERAGE_XML_PATH", _DEFAULT_COVERAGE_XML))
output_path = Path(os.environ.get("OUTPUT_PATH", _DEFAULT_OUTPUT))
baseline = _parse_float(
os.environ.get("BASELINE_COVERAGE"), "BASELINE_COVERAGE", _DEFAULT_BASELINE
)
alert_drop = _parse_float(os.environ.get("ALERT_DROP"), "ALERT_DROP", _DEFAULT_ALERT_DROP)
fail_on_drop = _truthy(os.environ.get("FAIL_ON_DROP"))
try:
current = _extract_line_rate(xml_path)
except FileNotFoundError as exc:
print(str(exc), file=sys.stderr)
return 1
payload, should_fail = _build_payload(current, baseline, alert_drop, fail_on_drop=fail_on_drop)
output_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"Coverage delta written to {output_path}")
return 1 if should_fail else 0
if __name__ == "__main__": # pragma: no cover
sys.exit(main())