Skip to content
Open
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
97 changes: 97 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,103 @@ jobs:
- name: Run all tests with coverage
run: make test-coverage

# Renders the numbers into the run's summary page so they are readable
# without downloading the artifact. Stdlib only — no extra install step.
- name: Coverage summary
if: always()
run: |
python - coverage.xml pytest-results.xml <<'PY' >> "$GITHUB_STEP_SUMMARY"
import glob, sys, xml.etree.ElementTree as ET
from pathlib import Path

MAX_ROWS = 300
cov, junit = sys.argv[1], sys.argv[2:]
o = ["## Coverage Report", ""]


def num(v, default=0):
"""Attribute values are text, and a malformed one must degrade rather than
raise: this step runs under `if: always()`, so an exception here would
stack a spurious failure on top of whatever actually went wrong."""
try:
return int(v)
except (TypeError, ValueError):
return default


def parse(path):
try:
return ET.parse(path).getroot(), None
except (ET.ParseError, OSError) as e:
return None, e


t = f = s = 0
seen = False
for pat in junit:
for path in sorted(glob.glob(pat)):
r, _ = parse(path)
if r is None:
continue
seen = True
for su in (r.iter("testsuite") if r.tag == "testsuites" else [r]):
t += num(su.get("tests"))
f += num(su.get("failures")) + num(su.get("errors"))
s += num(su.get("skipped"))
if seen:
# A suite dying at import reports errors>0 with tests possibly 0, so
# "no failures AND something actually ran" is what keeps a crashed run
# from rendering as passed.
ok = f == 0 and t > 0
o += ["### Test Results", "",
"| Status | Passed | Failed | Skipped | Total |",
"|---|---:|---:|---:|---:|",
f"| {'✅ Passed' if ok else '❌ Failed'} | {t - f - s} | {f} | {s} | {t} |", ""]

p = Path(cov)
root, err = (None, None) if not p.exists() else parse(p)
if root is None:
o += [f"> No coverage report at `{cov}`." if err is None
else f"> Coverage report at `{cov}` could not be parsed: {err}"]
else:
c, v = num(root.get("lines-covered")), num(root.get("lines-valid"))
o += ["### Coverage Summary", "", "| Metric | Value |", "|---|---:|",
f"| Line coverage | **{(100.0 * c / v if v else 0):.1f}%** |",
f"| Lines covered | {c:,} / {v:,} |",
f"| Lines missing | {v - c:,} |", ""]
rows = []
for cl in root.iter("class"):
ls = list(cl.iter("line"))
if ls:
h = sum(1 for x in ls if num(x.get("hits")) > 0)
rows.append((100.0 * h / len(ls),
cl.get("filename") or cl.get("name") or "?", h, len(ls)))
rows.sort(key=lambda r: (r[0], r[1]))
if rows:
shown = rows[:MAX_ROWS]
o += ["<details>",
f"<summary>Per-file coverage ({len(rows)} files, least covered first)</summary>",
"", "| File | Coverage | Covered / Total |", "|---|---:|---:|"]
o += [f"| `{n}` | {q:.1f}% | {h} / {tt} |" for q, n, h, tt in shown]
# Capped so a growing repo cannot push the summary past GitHub's 1 MiB
# limit, which would truncate it silently.
if len(rows) > MAX_ROWS:
o += [f"| _…and {len(rows) - MAX_ROWS} more files_ | | |"]
o += ["", "</details>", ""]
print("\n".join(o))
PY

# Consumed by the weekly coverage report, which reads the artifact from
# the newest successful run on main. Name is per-matrix-version because
# upload-artifact@v4 requires unique names within a run.
- name: Upload coverage report
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-${{ matrix.python-version }}
path: coverage.xml
if-no-files-found: warn

lint:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.head_ref != 'release-please--branches--main'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ htmlcov/
.cache
nosetests.xml
coverage.xml
pytest-results.xml
*.cover
*.py,cover
.hypothesis/
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ test-integration: # Run integration tests only
uv run pytest tests/integration/ -v -m integration

test-coverage: # Run tests with coverage report (parallel)
uv run pytest tests/ -v -n auto --dist loadscope --cov=handler --cov=remote_execution --cov-report=term-missing
uv run pytest tests/ -v -n auto --dist loadscope --cov=handler --cov=remote_execution --cov-branch --cov-report=term-missing --cov-report=xml --junitxml=pytest-results.xml

test-fast: # Run tests with fast-fail mode
uv run pytest tests/ -v -x --tb=short
Expand Down
Loading