Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
2b71fb7
Run Python coverage tooling in a venv
leynos Apr 26, 2026
009141b
Use uv tool run for Ruff make targets
leynos Apr 26, 2026
c6669c4
Resolve tool paths in make checks
leynos Apr 26, 2026
fb502bc
Refine coverage venv command handling
leynos Apr 27, 2026
a93f3b7
Recreate broken coverage venvs
leynos Apr 27, 2026
cb49e90
Bundle coverage format test parameters
leynos Apr 27, 2026
6a8c09b
Mark coverage XML stub parameters unused
leynos Apr 27, 2026
2ab45d6
Document coverage venv lifecycle
leynos Apr 27, 2026
600e710
Stabilise markdownlint for branch docs
leynos Apr 27, 2026
23b373e
Share coverage venv test scaffolding
leynos Apr 27, 2026
ba7e729
Remove duplicate coverage venv cache test
leynos Apr 27, 2026
006b165
Document coverage venv setup in action README
leynos Apr 27, 2026
1a302b8
Fix generate coverage README markdown lint
leynos Apr 27, 2026
89cfd3a
Decompose coverage venv command predicate
leynos Apr 27, 2026
3827e03
Simplify coverage venv setup
leynos Apr 27, 2026
7c9e72f
Sync project dependencies into coverage venv
leynos Apr 27, 2026
c759a4e
Harden coverage venv cache repair
leynos Apr 27, 2026
f4fbdce
Clarify markdownlint fallback warning
leynos Apr 27, 2026
2b60470
Extract coverage venv recreation assertions
leynos Apr 27, 2026
3d8f177
Rename coverage venv rebuild assertion helper
leynos Apr 27, 2026
a9f3b44
Cover coverage venv repair edge cases
leynos Apr 27, 2026
145b90b
Guard coverage command test venv detection
leynos Apr 27, 2026
533fa20
Run action-validator per action metadata file
leynos Apr 27, 2026
ee24905
Extract coverage venv recreation helpers
leynos Apr 27, 2026
3c20bbd
Move markdownlint logic into helper script
leynos Apr 27, 2026
3588178
Prefer Bun action-validator and quote markdownlint command
leynos Apr 27, 2026
ab88554
Extract default coverage venv rebuild assertion
leynos Apr 27, 2026
e7b1ce7
Use executable stems in coverage tests
leynos Apr 27, 2026
aaf1276
Merge non-Cobertura coverage command tests
leynos Apr 28, 2026
13da80f
Document coverage venv migration and add integration tests
leynos Apr 28, 2026
d712151
Fix AGENTS markdown table alignment
leynos Apr 28, 2026
d002a85
Handle invalid coverage venv paths
leynos Apr 28, 2026
394def3
Fix AGENTS documentation links and tool lookup
leynos Apr 28, 2026
d617af4
Fix coverage design wording
leynos Apr 28, 2026
ee1226c
Refactor run_python integration test runner
leynos Apr 28, 2026
a8aa693
Remove unused run_python test helper
leynos Apr 28, 2026
92d0b34
Handle dangling coverage venv symlinks
leynos Apr 29, 2026
dc432bb
Reduce run_python complexity
leynos Apr 29, 2026
bcbc0f1
Parametrize coverage venv failure tests
leynos Apr 29, 2026
e26e9de
Target uv sync at coverage venv
leynos Apr 29, 2026
8fe680e
Validate run_python integration env contract
leynos Apr 29, 2026
217dc00
Bundle uv failure test parameters
leynos Apr 29, 2026
6086cec
Document coverage venv internals
leynos Apr 29, 2026
dad0cf6
Fix developer guide markdownlint
leynos Apr 29, 2026
6865952
Handle coverage venv bootstrap failures
leynos Apr 29, 2026
3db43ba
Extract run_python main helpers
leynos Apr 29, 2026
77ca00c
Combine run_python coverage execution helper
leynos Apr 29, 2026
c811865
Improve run_python observability and baseline test
leynos Apr 29, 2026
0fa150f
Return absolute coverage venv python path
leynos Apr 30, 2026
fcb0b4f
Align test coverage python helper with platform layout
leynos Apr 30, 2026
33951ae
Extract coverage venv setup helpers
leynos Apr 30, 2026
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
10 changes: 7 additions & 3 deletions .github/actions/generate-coverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ projects. The action uses `cargo llvm-cov` (with `cargo nextest` by default)
when a `Cargo.toml` is present and `slipcover` with `pytest` when a
`pyproject.toml` is present. If the repository root does not contain a Cargo
manifest, set `cargo-manifest` to point to a nested `Cargo.toml`. It installs
`slipcover` and `pytest` automatically via `uv` before running the tests,
leveraging ``uv run --with`` so no system-level Python installs are required.
`slipcover`, `pytest`, and `coverage` automatically via `uv` into an isolated
throwaway virtual environment (`.venv-coverage`) before running the tests, so
no system-level Python installs are required.
When Rust coverage is required, `cargo-llvm-cov` and `cargo-nextest` are
installed automatically. If both configuration files are present, coverage is
run for each language and the Cobertura reports are merged using
Expand Down Expand Up @@ -45,6 +46,7 @@ codegen-backend-related variables first, and then merges
`get_cargo_coverage_env(manifest_path)` on top so workflow-level Cranelift
exports are not treated as the default coverage behaviour.

<!-- markdownlint-disable MD013 -->
```mermaid
sequenceDiagram
actor GitHubActions
Expand Down Expand Up @@ -78,6 +80,7 @@ sequenceDiagram
_run_cargo-->>run_rust_py: stdout
end
```
<!-- markdownlint-enable MD013 -->

## Cranelift codegen backend support

Expand Down Expand Up @@ -123,6 +126,7 @@ Known limitations:

## Inputs

<!-- markdownlint-disable MD013 -->
| Name | Description | Required | Default |
| --- | --- | --- | --- |
| features | Enable Cargo (Rust) features; space- or comma-separated. | no | |
Expand All @@ -134,11 +138,11 @@ Known limitations:
| with-ratchet | Fail if coverage drops below baseline | no | `false` |
| artefact-name-suffix | Additional suffix appended to the uploaded coverage artefact | no | |
| baseline-rust-file | Rust baseline path | no | `.coverage-baseline.rust` |
<!-- markdownlint-disable-next-line MD013 -->
| baseline-python-file | Python baseline path | no | `.coverage-baseline.python` |
| with-cucumber-rs | Run cucumber-rs scenarios under coverage | no | `false` |
| cucumber-rs-features | Path to cucumber feature files | no | |
| cucumber-rs-args | Extra arguments for cucumber | no | |
<!-- markdownlint-enable MD013 -->

\* `lcov` is only supported for Rust projects, while `coveragepy` is only
supported for Python projects. Mixed projects must use `cobertura`.
Expand Down
114 changes: 100 additions & 14 deletions .github/actions/generate-coverage/scripts/run_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@

import collections.abc as cabc # noqa: TC003 - used at runtime
import contextlib
import shutil
import typing as typ
from pathlib import Path

import typer
from cmd_utils_loader import run_cmd
from coverage_parsers import get_line_coverage_percent_from_cobertura
from plumbum import local
from plumbum.cmd import uv
from plumbum.commands.processes import ProcessExecutionError
from shared_utils import read_previous_coverage
Expand All @@ -27,6 +29,14 @@
FMT_OPT = typer.Option(..., envvar="DETECTED_FMT")
GITHUB_OUTPUT_OPT = typer.Option(..., envvar="GITHUB_OUTPUT")
BASELINE_OPT = typer.Option(None, envvar="BASELINE_PYTHON_FILE")
COVERAGE_VENV = Path(".venv-coverage")
TOOLING_PACKAGES: tuple[str, ...] = ("slipcover", "pytest", "coverage")
# _COVERAGE_PYTHON_CMD is a module-level lazy singleton. GitHub Actions
# runners execute action steps sequentially in a single thread, so no
# synchronisation is required. The variable is None until the first call
# to _coverage_python_cmd(), after which it is reused for the lifetime of
# the process.
_COVERAGE_PYTHON_CMD: BoundCommand | None = None

SLIPCOVER_ARGS: tuple[str, ...] = (
"-m",
Expand All @@ -40,18 +50,93 @@
)


def _uv_python_cmd() -> BoundCommand:
"""Return ``uv run`` configured with the required coverage tools."""
return uv[
"run",
"--with",
"slipcover",
"--with",
"pytest",
"--with",
"coverage",
"python",
]
def _coverage_python_path() -> Path:
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
"""Return the Python executable path inside the coverage venv."""
candidates = (
COVERAGE_VENV / "bin" / "python",
COVERAGE_VENV / "Scripts" / "python.exe",
COVERAGE_VENV / "Scripts" / "python",
)
for candidate in candidates:
if candidate.is_file():
return candidate
paths = ", ".join(str(candidate) for candidate in candidates)
msg = f"Coverage venv Python executable not found; checked: {paths}"
raise RuntimeError(msg)


def create_venv() -> str:
"""Create a throwaway venv for coverage tooling.

If the venv directory already exists but its Python executable cannot be
located (broken-cache state), the directory is removed and the venv is
recreated before returning the interpreter path.

Returns
-------
str
Absolute path to the Python executable inside the created venv.
"""
if not COVERAGE_VENV.exists():
typer.echo(f"Creating coverage venv at {COVERAGE_VENV}")
run_cmd(uv["venv", str(COVERAGE_VENV)])
else:
typer.echo(f"Reusing existing coverage venv at {COVERAGE_VENV}")
try:
python = str(_coverage_python_path())
except RuntimeError:
typer.echo(
f"Coverage venv at {COVERAGE_VENV} is missing its Python "
"executable; recreating.",
err=True,
)
shutil.rmtree(COVERAGE_VENV)
run_cmd(uv["venv", str(COVERAGE_VENV)])
python = str(_coverage_python_path())
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return python

Comment thread
coderabbitai[bot] marked this conversation as resolved.

def install_coverage_tools(python: str) -> None:
"""Install coverage tooling into the throwaway venv.

Parameters
----------
python : str
Path to the Python executable inside the target venv, as returned by
create_venv().

Raises
------
plumbum.commands.processes.ProcessExecutionError
Propagated from run_cmd() when uv pip install fails.
"""
typer.echo(f"Installing coverage tooling {TOOLING_PACKAGES} into {COVERAGE_VENV}")
run_cmd(uv["pip", "install", "--python", python, *TOOLING_PACKAGES])
Comment thread
leynos marked this conversation as resolved.
Outdated


def _coverage_python_cmd() -> BoundCommand:
"""Set up the coverage venv on first call and return the cached command.

Side effects on first call
--------------------------
* Creates .venv-coverage via create_venv() (recreates on broken cache).
* Installs slipcover, pytest, and coverage into the venv.
* Caches the resulting BoundCommand in _COVERAGE_PYTHON_CMD.

Returns
-------
BoundCommand
A plumbum command bound to the venv's Python executable.
"""
global _COVERAGE_PYTHON_CMD
if _COVERAGE_PYTHON_CMD is not None:
typer.echo("Reusing cached coverage Python command.")
return _COVERAGE_PYTHON_CMD
typer.echo("Setting up coverage Python environment (first use).")
python = create_venv()
install_coverage_tools(python)
_COVERAGE_PYTHON_CMD = local[python]
return _COVERAGE_PYTHON_CMD


def _coverage_args(fmt: str, out: Path) -> list[str]:
Expand All @@ -66,16 +151,17 @@ def _coverage_args(fmt: str, out: Path) -> list[str]:

def coverage_cmd_for_fmt(fmt: str, out: Path) -> BoundCommand:
"""Return the slipcover command for the requested format."""
python_cmd = _uv_python_cmd()
python_cmd = _coverage_python_cmd()
return python_cmd[_coverage_args(fmt, out)]


@contextlib.contextmanager
def tmp_coveragepy_xml(out: Path) -> cabc.Generator[Path]:
"""Generate a cobertura XML from coverage.py and clean up afterwards."""
xml_tmp = out.with_suffix(".xml")
python_cmd = _coverage_python_cmd()
try:
cmd = _uv_python_cmd()["-m", "coverage", "xml", "-o", str(xml_tmp)]
cmd = python_cmd["-m", "coverage", "xml", "-o", str(xml_tmp)]
run_cmd(cmd)
except ProcessExecutionError as exc:
typer.echo(
Expand Down
Loading
Loading