Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[run]
omit =
*/config.py
*/config-*.py
capybara/cpuinfo.py
capybara/utils/system_info.py
capybara/vision/ipcam/app.py

[report]
omit =
*/config.py
*/config-*.py
capybara/cpuinfo.py
capybara/utils/system_info.py
capybara/vision/ipcam/app.py
173 changes: 173 additions & 0 deletions .github/scripts/coverage_gate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from __future__ import annotations

import argparse
import sys
from pathlib import Path
from xml.etree import ElementTree


def _as_float(value: str | None, default: float | None = None) -> float | None:
if value is None:
return default
text = value.strip()
if not text:
return default
try:
return float(text)
except ValueError:
return default


def _as_bool(value: str | None) -> bool:
if value is None:
return False
return str(value).strip().lower() in {"1", "true", "yes", "on", "y"}


def _percent(value: float | None) -> str:
if value is None or value < 0:
return "N/A"
pct = value * 100.0
if pct.is_integer():
return f"{int(pct)}%"
return f"{pct:.2f}%"


def _load_coverage_root(path: Path) -> ElementTree.Element | None:
if not path.exists():
return None
try:
return ElementTree.parse(path).getroot()
except ElementTree.ParseError:
return None


def evaluate_coverage(
coverage_path: Path,
min_line: float | None,
min_branch: float | None,
) -> tuple[bool, list[str], float | None, float | None]:
"""Return a tuple describing coverage gate result."""
root = _load_coverage_root(coverage_path)
if root is None:
return False, [f"coverage XML '{coverage_path}' 無法讀取."], None, None

line_rate = _as_float(root.attrib.get("line-rate"))
branch_rate = _as_float(root.attrib.get("branch-rate"))

messages: list[str] = []
passed = True

if min_line is not None:
if line_rate is None:
passed = False
messages.append("行覆蓋率資料不存在.")
elif line_rate + 1e-9 < min_line:
passed = False
messages.append(
f"行覆蓋率 {_percent(line_rate)} 低於門檻 {_percent(min_line)}."
)
else:
messages.append(
f"行覆蓋率 {_percent(line_rate)} >= 門檻 {_percent(min_line)}."
)

if min_branch is not None:
if branch_rate is None:
passed = False
messages.append("分支覆蓋率資料不存在.")
elif branch_rate + 1e-9 < min_branch:
passed = False
messages.append(
f"分支覆蓋率 {_percent(branch_rate)} 低於門檻 {_percent(min_branch)}."
)
else:
messages.append(
f"分支覆蓋率 {_percent(branch_rate)} >= 門檻 {_percent(min_branch)}."
)

if min_line is None and min_branch is None:
messages.append("未設定覆蓋率門檻, 跳過檢查.")

return passed, messages, line_rate, branch_rate


def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Coverage gate checker")
parser.add_argument(
"--file",
required=True,
help="Path to coverage XML report generated by coverage.py",
)
parser.add_argument(
"--min-line",
dest="min_line",
default=None,
help="Minimum line coverage required (0.0 - 1.0)",
)
parser.add_argument(
"--min-branch",
dest="min_branch",
default=None,
help="Minimum branch coverage required (0.0 - 1.0)",
)
parser.add_argument(
"--enforce",
dest="enforce",
default="0",
help="Set to 1/true to enforce the gate (exit with non-zero on failure)",
)
return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
args = parse_args(argv or sys.argv[1:])

coverage_path = Path(args.file)
min_line = _as_float(args.min_line)
min_branch = _as_float(args.min_branch)
enforce_gate = _as_bool(args.enforce)

if not coverage_path.exists():
message = f"找不到覆蓋率報告: {coverage_path}"
if enforce_gate:
print(f"::error::{message}")
print("::error::Coverage Gate Result: FAIL (N/A)")
return 2
print(f"::warning::{message}")
print("::warning::Coverage Gate Result: SKIP (N/A)")
return 0

passed, messages, line_rate, branch_rate = evaluate_coverage(
coverage_path, min_line, min_branch
)
prefix = "::notice::" if passed else "::error::"
for line in messages:
print(f"{prefix}{line}")

summary_parts: list[str] = []
if line_rate is not None:
summary_parts.append(f"行 {_percent(line_rate)}")
if branch_rate is not None:
summary_parts.append(f"分支 {_percent(branch_rate)}")
summary_text = ", ".join(summary_parts) if summary_parts else "N/A"
summary_prefix = (
"::notice::"
if passed
else ("::warning::" if not enforce_gate else "::error::")
)
result_text = "PASS" if passed else "FAIL"
print(
f"{summary_prefix}Coverage Gate Result: {result_text} ({summary_text})"
)

if passed or not enforce_gate:
if not passed:
print("::warning::覆蓋率未達門檻, 但目前未強制執行 (enforce=0).")
return 0

return 1


if __name__ == "__main__":
sys.exit(main())
Loading
Loading