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
73 changes: 73 additions & 0 deletions .github/scripts/coverage_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from __future__ import annotations

import sys
import xml.etree.ElementTree as ET
from collections import defaultdict
from pathlib import Path


def _top_level_module(package_name: str) -> str:
normalized = package_name.strip(".")
if normalized in {"", "semapact"}:
return "root"
if normalized.startswith("semapact."):
normalized = normalized.removeprefix("semapact.")
return normalized.split(".", 1)[0]


def build_summary(xml_path: Path) -> str:
root = ET.parse(xml_path).getroot()
stats: dict[str, list[int]] = defaultdict(lambda: [0, 0])

for package in root.findall(".//package"):
module = _top_level_module(package.get("name", ""))
for line in package.findall("./classes/class/lines/line"):
stats[module][0] += 1
if int(line.get("hits", "0")) > 0:
stats[module][1] += 1

rows = [
"## Test Coverage",
"",
"| Module | Statements | Missed | Coverage |",
"| --- | ---: | ---: | ---: |",
]

total_statements = 0
total_covered = 0
for module in sorted(stats):
statements, covered = stats[module]
missed = statements - covered
pct = (covered / statements * 100) if statements else 100.0
rows.append(f"| `{module}` | {statements} | {missed} | {pct:.1f}% |")
total_statements += statements
total_covered += covered

total_missed = total_statements - total_covered
total_pct = (total_covered / total_statements * 100) if total_statements else 100.0
rows.extend(
[
f"| **TOTAL** | **{total_statements}** | **{total_missed}** | **{total_pct:.1f}%** |",
"",
"Detailed per-file and line coverage is available in the `coverage-report` artifact.",
]
)
return "\n".join(rows)


def main() -> int:
if len(sys.argv) != 2:
print("usage: coverage_summary.py <coverage.xml>", file=sys.stderr)
return 2

xml_path = Path(sys.argv[1])
if not xml_path.is_file():
print(f"coverage XML not found: {xml_path}", file=sys.stderr)
return 1

print(build_summary(xml_path))
return 0


if __name__ == "__main__":
raise SystemExit(main())
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ on:
push:
branches: [main]

permissions:
contents: read

jobs:
tests:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -38,3 +42,54 @@ jobs:

- name: Verify package build
run: uv build

coverage:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
UV_NO_PROGRESS: "1"
SEMAPACT_RUNTIME_CONTEXT: "auto"
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.13"

- name: Set up uv
uses: astral-sh/setup-uv@v7

- name: Install dependencies
run: uv sync --all-extras --group dev --frozen

- name: Run tests with coverage
run: >-
uv run --with pytest-cov pytest
--cov=semapact
--cov-report=xml:coverage.xml
--cov-report=html:htmlcov

- name: Publish module coverage summary
if: always()
shell: bash
run: |
if [ -f coverage.xml ]; then
uv run python .github/scripts/coverage_summary.py coverage.xml | tee -a "$GITHUB_STEP_SUMMARY"
else
echo "## Test Coverage" | tee -a "$GITHUB_STEP_SUMMARY"
echo "" | tee -a "$GITHUB_STEP_SUMMARY"
echo "_Coverage data was not generated._" | tee -a "$GITHUB_STEP_SUMMARY"
fi

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v6
with:
name: coverage-report
path: |
coverage.xml
htmlcov/
if-no-files-found: error
retention-days: 14
Loading