Skip to content

Run Python coverage tooling in a venv#257

Open
leynos wants to merge 48 commits intomainfrom
fix/coverage-venv-fix
Open

Run Python coverage tooling in a venv#257
leynos wants to merge 48 commits intomainfrom
fix/coverage-venv-fix

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Apr 26, 2026

Summary

This branch runs the generate-coverage Python coverage phase from a short-lived virtual environment instead of relying on uv run or any system interpreter install path. The change avoids PEP 668 failures on GitHub-hosted Ubuntu runners where Python 3.12 is externally managed, while keeping coverage tooling isolated from the system interpreter.

It also hardens the Makefile check targets so automation with a narrow PATH can still find uv and action-validator from their common user-local install locations.

Review walkthrough

Validation

  • python -c "import ast, sys; ast.parse(open('.github/actions/generate-coverage/scripts/run_python.py').read()); print('parse ok')": passed
  • uv run --with typer --with packaging --with plumbum --with pyyaml pytest .github/actions/generate-coverage/tests/test_scripts.py -k 'coverage_cmd_uses_venv_python or tmp_coveragepy_xml_invokes_venv_python or run_python_cobertura_passes_out_flag' -v: passed, 3 selected tests passed
  • make check-fmt: passed
  • make typecheck: passed
  • make lint: passed
  • make test: passed, 668 passed and 90 skipped
  • make check-fmt lint typecheck: passed
  • env PATH=/usr/bin:/bin make check-fmt lint typecheck: passed

Notes

The branch deliberately does not add --break-system-packages or restore a system install path. Coverage tooling is installed into .venv-coverage and invoked through that venv's Python interpreter.

Summary by Sourcery

Run Python coverage tooling from an isolated, job-local virtual environment instead of via uv run, and make local tooling resolution in the Makefile more robust.

New Features:

  • Introduce a dedicated .venv-coverage virtual environment and helper functions to create it, install coverage tooling into it, and reuse its Python interpreter for all coverage commands.

Bug Fixes:

  • Avoid relying on system Python or uv run --with for coverage, preventing failures on externally managed Python installations such as GitHub-hosted Ubuntu runners.

Enhancements:

  • Refactor coverage command construction to use a cached venv-based Python command while keeping slipcover, pytest, and coverage invocation behaviour consistent.
  • Strengthen tests around coverage command construction and venv lifecycle, including recovery from broken or missing venv Python executables.
  • Improve Makefile commands to resolve uv, action-validator, and markdownlint from common user-local install locations and to lint only changed Markdown files where possible.

Documentation:

  • Document the new Python coverage venv architecture, lifecycle, and concurrency assumptions, and update the generate-coverage README to describe the venv-based installation behaviour.

leynos added 3 commits April 27, 2026 01:44
Create a throwaway coverage virtual environment and install
`slipcover`, `pytest`, and `coverage` into it with `uv pip --python`.
Avoid relying on the separate `uvx` shim being present on PATH when
running formatting and lint targets from automation.
Prefer the common user-local install locations for `uv` and
`action-validator` so automation with a narrow PATH can still run the
format, lint, and typecheck targets.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

Overview

This PR changes the generate-coverage GitHub Action to create and use a job‑local persistent virtual environment (.venv-coverage) for all Python coverage tooling instead of invoking tools via uv run --with or relying on a system interpreter. It centralises venv lifecycle management, adds broken‑cache detection and automatic recreation, improves observability and error handling, and is accompanied by expanded unit + integration tests and documentation — including a new design/execplan document that should be consulted as the canonical design.

Key changes

Core implementation (.github/actions/generate-coverage/scripts/run_python.py)

  • Venv lifecycle and helpers:
    • _ensure_coverage_venv(), _find_coverage_python(), _remove_coverage_venv(), _recreate_coverage_venv() implement creation, detection of broken/missing venv state (missing python executable, placeholder file/symlink), and automatic recreation. POSIX and Windows candidate paths are considered.
    • Tooling is synced/installed into the venv using uv sync --inexact --python and uv pip install --python (deliberately excluding --system).
  • Executables and invocation:
    • _coverage_python_cmd() is memoised (via @lru_cache) and returns a plumbum BoundCommand for the venv Python; slipcover, pytest and coverage XML export run through that interpreter rather than uv run.
    • Command construction and coverage execution consolidated into helpers (_resolve_output_path, _run_coverage) and slipcover/coverage post‑processing unified.
  • Observability and error handling:
    • Typer echoes at decision points (creation, reuse, tooling install); uv subprocess calls wrapped to emit contextual errors and re‑raise as typer.Exit with preserved exit codes.
    • Command construction moved into try blocks where appropriate so venv/setup failures are caught and surfaced to users.
  • Caching/behavioural notes:
    • Memoisation assumes GitHub Actions’ step‑sequential single‑threaded model; comments and tests assert setup happens only once per process.

Tests (.github/actions/generate-coverage/tests/test_scripts.py)

  • Unit tests updated to assert the venv‑Python invocation shapes and to reject --system installs.
  • New/expanded unit tests cover: venv creation, reuse, broken‑cache recovery, invalid python candidates, tooling install sequencing, and memoisation of _coverage_python_cmd().
  • Helpers and frozen dataclasses (VenvTestSetup, CoverageFmtSpec, UvFailureSpec) added to reduce duplication and parametrise cases.
  • Integration tests exercise run_python.py end‑to‑end with a fake uv shim, validating success and failure modes (uv venv / uv sync failures), the produced GITHUB_OUTPUT keys, mixed‑language renaming behaviour, and stderr fragments on failure.

Makefile and auxiliary scripts

  • Makefile hardened to prefer common user‑local tool locations before falling back to PATH:
    • UV ?= $(if $(wildcard $(HOME)/.local/bin/uv),$(HOME)/.local/bin/uv,uv) and similar resolution for ACTION_VALIDATOR and MDLINT.
    • Targets updated to invoke tools via $(UV) where appropriate.
  • New workflow_scripts/markdownlint-check.sh: POSIX shell script to lint changed Markdown files (or all if base ref missing), respects overrides (MDLINT, MARKDOWNLINT_BASE), and aggregates exit status.

Documentation

  • New design/execplan: docs/generate-coverage-design.md documents the .venv‑coverage architecture, lifecycle, caching behaviour and the public/internal API (e.g. _ensure_coverage_venv(), _coverage_python_cmd()) — treated as the canonical design/execplan for this change.
  • Developer guide: docs/developers-guide.md describes internals, repair semantics, candidate paths and Makefile tool‑resolution.
  • Updated README: .github/actions/generate-coverage/README.md, AGENTS.md (Makefile tool resolution), and CHANGELOG.md (v1.3.14 entry describing the venv switch and auto‑repair behaviour).

Review and remediation items surfaced during review

  • Reduced function arity and duplication via frozen dataclasses (CoverageFmtSpec) and helper extraction.
  • Docstrings: reviewers requested NumPy‑style docstrings for several functions; docstring expansions and prompts were provided and most were implemented; a few remaining small docstring follow‑ups noted.
  • One outstanding follow‑up: issue #259 to move a remaining command‑construction assignment into main’s try/except so venv‑setup subprocess errors are always converted to typer.Exit.
  • Suggested non‑blocking follow‑ups: centralise developer‑guide content, add richer integration assertions (GITHUB_OUTPUT, baseline handling), and optional property tests — most were implemented or intentionally scoped out to avoid over‑engineering an internal CI helper.

Tests & validation

  • Local validation reported passing for formatting, typechecking, lint and test runs; the branch adds broad unit + integration coverage focused on venv lifecycle and failure modes.

Summary

Moves Python coverage tooling to a managed, job‑local .venv-coverage, implements robust lifecycle and broken‑cache recovery, improves observability and error handling, hardens local Makefile tool resolution, adds a markdownlint diff script, expands tests significantly, and introduces a design/execplan document as the canonical reference. Remaining follow‑ups (notably #259 and minor doc/test cleanups) are tracked and do not block the core behaviour.

Walkthrough

Create and reuse a dedicated .venv-coverage; detect, repair or recreate it when its Python is missing or broken; sync project dependencies into it and install slipcover, pytest and coverage; memoise the venv Python command and invoke coverage tooling via the venv Python instead of uv run --with; print venv setup errors to stderr and exit non‑zero.

Changes

Cohort / File(s) Summary
Coverage script refactoring
\.github/actions/generate-coverage/scripts/run_python.py
Add venv discovery/repair (_find_coverage_python, _recreate_coverage_venv, _ensure_coverage_venv), memoise _coverage_python_cmd() as a plumbum BoundCommand, create/sync/recreate .venv-coverage via uv venv / uv sync --inexact --python and uv pip install --python (no --system), centralise output path handling and run slipcover/pytest/coverage through the venv Python; convert venv setup failures to stderr + exit.
Tests for venv flow
\.github/actions/generate-coverage/tests/test_scripts.py
Replace uv run assertions with checks that tooling is invoked via the venv Python; add test harnesses, helpers and fixtures to simulate uv behaviour; cover venv creation, reuse, broken-cache recovery, placeholder/symlink handling, invalid python candidates, tooling-install semantics (no --system), UV_PROJECT_ENVIRONMENT usage, memoisation and integration-style sequencing with a fake uv.
Makefile tool config
Makefile
Introduce UV, ACTION_VALIDATOR, MDLINT, MARKDOWNLINT_BASE variables preferring local installs; run tools via $(UV)/$(UV) tool run; delegate markdownlint to workflow_scripts/markdownlint-check.sh.
Docs & README
\.github/actions/generate-coverage/README.md, docs/generate-coverage-design.md, docs/developers-guide.md
Document .venv-coverage lifecycle, repair strategy, memoisation of _coverage_python_cmd() and public API signatures; update README usage and Makefile tool-resolution documentation.
Markdown lint helper
workflow_scripts/markdownlint-check.sh
Add POSIX sh script to compute changed Markdown files against MARKDOWNLINT_BASE and run markdownlint per-file; fall back to lint-all when no changes or git diff fails; aggregate exit status.
Changelog & Agents doc
\.github/actions/generate-coverage/CHANGELOG.md, AGENTS.md
Add v1.3.14 changelog describing the venv-based coverage switch; document Makefile tool-resolution strategy and defaults in AGENTS.md.

Sequence Diagram(s)

sequenceDiagram
    participant Script as run_python.py
    participant UV as uv
    participant Venv as .venv-coverage
    participant Tooling as slipcover/pytest/coverage

    Script->>Venv: Request cached Python (_coverage_python_cmd)
    alt missing or invalid
        Script->>UV: Run `uv venv .venv-coverage`
        UV-->>Script: venv created
        Script->>UV: Run `UV_PROJECT_ENVIRONMENT=... uv sync --inexact --python <venv_python>`
        UV-->>Script: deps synced
        Script->>UV: Run `uv pip install --python <venv_python> slipcover pytest coverage`
        UV-->>Script: tooling installed
    else cached valid
        Venv-->>Script: Return cached Python
    end
    Script->>Tooling: Invoke via venv Python (`<venv_python> -m slipcover | -m pytest | -m coverage`)
    Tooling-->>Script: Produce coverage artefacts
Loading

Possibly related issues

Poem

Keep the venv close and mend it when it's cold,
Sync the deps, install the tools as told,
Cache Python's path and call it with a grin,
Emit the reports and write the coverage in,
Job‑local, repaired, and tidy as gold.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Tests contain critical platform-specific defects: _coverage_python() hardcodes POSIX paths whilst production code probes both POSIX and Windows paths, creating Windows test gaps. Assertions validate only path stems rather than confirming venv Python appears in actual command strings, allowing bypass of venv logic. Update _coverage_python() to detect sys.platform == 'win32' and return Scripts/python.exe on Windows. Strengthen assertions to verify venv Python path appears in constructed commands. Add parametrised unit tests covering both POSIX and Windows paths without skip markers.
Unit Architecture ❌ Error The test helper _coverage_python() returns hardcoded POSIX paths unconditionally, diverging from production _find_coverage_python() which probes platform-specific candidates and can fail. This embeds environmental assumptions into query helpers, masking fallibility and creating dual-path test architecture hidden by skipif decorators. Update _coverage_python() to detect sys.platform and return platform-appropriate paths matching production logic. Parametrise tests exercising Windows paths rather than skipping them, eliminating skipif markers and making query helpers faithful to production discovery.
Observability ⚠️ Warning Raw subprocess stderr logged without justification in three error handlers violates high-cardinality field constraints. Add inline comments justifying stderr inclusion or refactor to log only exit codes, delegating full stderr to CI logs.
Concurrency And State ⚠️ Warning Shared mutable state (_coverage_python_cmd memoisation and .venv-coverage directory) lacks explicit concurrency safeguards and formal documentation of single-threaded GitHub Actions assumption. Document thread-safety assumptions in _coverage_python_cmd() docstring, add unit test validating memoisation occurs only once per process, clear functools.lru_cache() between test runs via _coverage_python_cmd.cache_clear().
Testing (Unit) ❓ Inconclusive Cannot execute shell scripts; manual examination of test files required to assess unit test coverage and structure. Provide the actual test file content or execute the scripts in your environment to enable assessment of test meaningfulness and edge case coverage.
✅ Passed checks (13 passed)
Check name Status Explanation
Title check ✅ Passed The title directly reflects the main change: running Python coverage tooling from an isolated virtual environment (.venv-coverage) instead of via uv run.
Description check ✅ Passed The description comprehensively covers the changeset: venv-based coverage execution, Makefile tool resolution hardening, test updates, and documentation additions.
Docstring Coverage ✅ Passed Docstring coverage is 82.26% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
User-Facing Documentation ✅ Passed Repository documents user-facing action behaviour in action-specific README.md files; the .github/actions/generate-coverage/README.md describes the isolated .venv-coverage virtual environment with automatic dependency installation via uv, and action inputs/outputs remain unchanged, confirming this is a purely implementation improvement.
Developer Documentation ✅ Passed The pull request adequately documents internal APIs and architectural changes through developers-guide.md, generate-coverage-design.md, README, CHANGELOG.md, and AGENTS.md updates.
Module-Level Documentation ✅ Passed The PR demonstrates proper module-level documentation across all modified Python files with clear docstrings and comprehensive NumPy-style documentation for all public functions.
Testing (Property / Proof) ✅ Passed The PR introduces bounded filesystem state invariants with existing comprehensive test coverage; property-based testing and formal verification are not applicable.
Testing (Compile-Time / Ui) ✅ Passed The PR adequately tests structured output through explicit integration tests validating GITHUB_OUTPUT keys and values without requiring snapshot testing infrastructure.
Domain Architecture ✅ Passed PR introduces infrastructure and automation code with appropriate separation of concerns. Framework dependencies (typer, plumbum, os.environ) are correctly contained within infrastructure adapters and orchestrators, not embedded in domain logic.
Security And Privacy ✅ Passed Pull request introduces no security or privacy vulnerabilities; command construction uses safe programmatic API rather than shell string concatenation, eliminating injection risks.
Performance And Resource Use ✅ Passed The PR demonstrates sound performance and resource management discipline through memoised _coverage_python_cmd() with @lru_cache(maxsize=1) ensuring expensive venv setup operations execute exactly once per process, all I/O operations are intentional and bounded, and no unbounded loops or repeated subprocess calls exist in hot paths.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/coverage-venv-fix

Review rate limit: 8/10 reviews remaining, refill in 11 minutes and 52 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 26, 2026

Reviewer's Guide

Run Python coverage generation inside an isolated, lazily-created .venv-coverage virtualenv instead of uv run/system Python, strengthen tests and docs around this behavior, and make the Makefile’s lint/test tooling resolution more robust and configurable in constrained environments.

Sequence diagram for Python coverage venv setup and reuse

sequenceDiagram
    actor GitHubActions
    participant generate_coverage_action
    participant run_python_script
    participant coverage_python_cmd as _coverage_python_cmd
    participant create_venv
    participant install_tools as install_coverage_tools
    participant uv_cli as uv
    participant slipcover_pytest_coverage

    GitHubActions->>generate_coverage_action: run Python coverage step
    generate_coverage_action->>run_python_script: invoke run_python.py

    Note over run_python_script,coverage_python_cmd: First coverage invocation
    run_python_script->>coverage_python_cmd: request coverage Python
    coverage_python_cmd->>create_venv: create_venv()
    alt venv_does_not_exist
        create_venv->>uv_cli: venv .venv-coverage
        uv_cli-->>create_venv: venv created
    else venv_exists
        create_venv-->>create_venv: reuse existing venv
    end
    create_venv-->>coverage_python_cmd: venv_python_path

    coverage_python_cmd->>install_tools: install_coverage_tools(venv_python_path)
    install_tools->>uv_cli: pip install --python venv_python slipcover pytest coverage
    uv_cli-->>install_tools: tools installed
    install_tools-->>coverage_python_cmd: success
    coverage_python_cmd-->>run_python_script: BoundCommand for venv_python

    run_python_script->>slipcover_pytest_coverage: run slipcover/pytest/coverage via venv_python
    slipcover_pytest_coverage-->>run_python_script: coverage results
    run_python_script-->>generate_coverage_action: Cobertura XML
    generate_coverage_action-->>GitHubActions: upload coverage artefact

    Note over run_python_script,coverage_python_cmd: Subsequent coverage invocation
    GitHubActions->>generate_coverage_action: run Python coverage step again
    generate_coverage_action->>run_python_script: invoke run_python.py
    run_python_script->>coverage_python_cmd: request coverage Python
    coverage_python_cmd-->>run_python_script: reuse cached BoundCommand
    run_python_script->>slipcover_pytest_coverage: run tools via cached venv_python
    slipcover_pytest_coverage-->>run_python_script: coverage results
Loading

Flow diagram for coverage venv lifecycle functions

flowchart TD
    start[Start coverage tooling setup]
    cmd[_coverage_python_cmd]
    venv_check[Does .venv-coverage exist]
    create[create_venv]
    install[install_coverage_tools]
    uv_venv[uv venv .venv-coverage]
    detect_python[Locate venv python executable]
    python_missing[Python executable missing]
    recreate[Remove and recreate .venv-coverage]
    uv_pip[uv pip install --python venv_python slipcover pytest coverage]
    cache_cmd[Cache BoundCommand for venv python]
    return_cmd[Return cached BoundCommand]

    start --> cmd
    cmd -->|_COVERAGE_PYTHON_CMD is None| create
    cmd -->|_COVERAGE_PYTHON_CMD is set| return_cmd

    create --> venv_check
    venv_check -->|no| uv_venv
    venv_check -->|yes| detect_python

    uv_venv --> detect_python

    detect_python -->|found| install
    detect_python -->|not found| python_missing

    python_missing --> recreate
    recreate --> uv_venv

    install --> uv_pip
    uv_pip --> cache_cmd
    cache_cmd --> return_cmd

    return_cmd --> end_node[Coverage venv python ready]
Loading

File-Level Changes

Change Details Files
Route all Python coverage execution through a cached venv-scoped Python interpreter created via uv venv and populated with slipcover/pytest/coverage using uv pip, replacing the previous uv run --with approach.
  • Introduce COVERAGE_VENV, TOOLING_PACKAGES, and a module-level _COVERAGE_PYTHON_CMD singleton to manage the dedicated coverage environment.
  • Implement _coverage_python_path(), create_venv(), and install_coverage_tools() to create, recover, or repair .venv-coverage and install slipcover, pytest, and coverage via uv pip install --python without --system.
  • Replace _uv_python_cmd() with _coverage_python_cmd() which lazily creates the venv, installs tools once, and returns a plumbum BoundCommand bound to the venv Python.
  • Update coverage_cmd_for_fmt() and tmp_coveragepy_xml() to use the venv Python command instead of uv run, ensuring slipcover, pytest, and coverage are invoked via the venv interpreter.
.github/actions/generate-coverage/scripts/run_python.py
Expand and adjust tests for the generate-coverage Python tooling to validate venv creation/reuse, tool installation, and use of the venv Python across commands and main flow.
  • Add helpers and dataclasses (VenvTestSetup, CoverageFmtSpec, _coverage_python, _set_fake_coverage_python_cmd) to simulate and assert venv behavior without actually creating environments.
  • Add tests for create_venv() and _coverage_python_path(), covering first-time creation, reuse of existing venvs, recovery from missing/invalid Python executables, and error conditions.
  • Adjust existing coverage command-shape tests to assert that commands start with the venv Python path rather than uv run, and that Cobertura and coveragepy XML flows reuse the venv Python.
  • Update main() integration-style tests to ensure the coveragepy exporter and exit paths capture the venv Python and that commands are executed via run_cmd with the expected method (e.g. run_fg).
.github/actions/generate-coverage/tests/test_scripts.py
Document the new Python coverage venv architecture and update the action README to describe the venv-based installation model.
  • Extend the generate-coverage design doc with a dated entry and a dedicated "Python Coverage Venv Architecture" section covering motivation, lifecycle, concurrency model, and public API (create_venv, install_coverage_tools).
  • Update the generate-coverage README to describe installing slipcover, pytest, and coverage into an isolated .venv-coverage instead of using uv run --with, and tweak markdownlint-related comments for MD013 handling.
docs/generate-coverage-design.md
.github/actions/generate-coverage/README.md
Harden Makefile tooling resolution for uv, action-validator, and markdownlint, and switch Ruff invocations to uv tool run with configurable UV binary.
  • Introduce ACTION_VALIDATOR, MDLINT, MARKDOWNLINT_BASE, and UV variables that prefer common user-local installation paths (e.g. ~/.cargo/bin, ~/.bun/bin, ~/.local/bin) with fallbacks to bare commands.
  • Update test and .venv targets to use $(UV) for uv run, uv venv, and uv sync so constrained PATH environments can still locate uv.
  • Replace direct uvx ruff calls for lint/fmt/check-fmt with $(UV) tool run ruff ... so Ruff is run via uv’s tool mechanism and respects the configured UV path.
  • Change markdownlint target to prefer linting only changed Markdown files based on git diff against MARKDOWNLINT_BASE, falling back to a full tree scan when no changed files are detected.
Makefile

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos leynos marked this pull request as ready for review April 27, 2026 00:50
sourcery-ai[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Cache the coverage-capable Python command behind one helper, reuse an
existing coverage venv when present, and resolve the venv interpreter
across POSIX and Windows layouts.

Extend the run_python tests to cover cached command reuse and the
coverage.py export path using the same venv interpreter.
@lodyai lodyai Bot marked this pull request as draft April 27, 2026 00:59
codescene-delta-analysis[bot]

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as resolved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 27, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response.

.github/actions/generate-coverage/tests/test_scripts.py

Comment on file

def _get_coverage_cmd_parts(
    tmp_path: Path,
    run_python_module: ModuleType,
    monkeypatch: pytest.MonkeyPatch,

❌ New issue: Excess Number of Function Arguments
_get_coverage_cmd_parts has 5 arguments, max arguments = 4

@coderabbitai

This comment was marked as resolved.

@leynos

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

Repair `.venv-coverage` when the directory exists without a usable Python
interpreter, so coverage setup can recover from incomplete or stale venvs.

Reject non-file interpreter placeholders before reusing an existing venv.
@coderabbitai

This comment was marked as resolved.

Introduce `CoverageFmtSpec` so the coverage command test helper stays
within the argument-count limit while preserving the same test behaviour.
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Rename unused `fake_tmp_coveragepy_xml` parameters in coveragepy tests so
the stubs clearly follow the unused-argument convention.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 27, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. Ensure that this is validated against the current version of the codegraph.

If further refinement to address this finding would be deleterious, please supply a clear explanatory one to two paragraph markdown message I can paste into the CodeScene web ui's diagnostic suppression function so this diagnostic can be silenced.

Code Duplication

.github/actions/generate-coverage/tests/test_scripts.py:

What lead to degradation?

introduced similar code in: test_create_venv_recreates_broken_coverage_venv,test_create_venv_recreates_invalid_python_candidate,test_create_venv_returns_coverage_python

Why does this problem occur?

Duplicated code often leads to code that's harder to change since the same logical change has to be done in multiple functions. More duplication gives lower code health.

How to fix it?

A certain degree of duplicated code might be acceptable. The problems start when it is the same behavior that is duplicated across the functions in the module, ie. a violation of the Don't Repeat Yourself (DRY) principle. DRY violations lead to code that is changed together in predictable patterns, which is both expensive and risky. DRY violations can be identified using CodeScene's X-Ray analysis to detect clusters of change coupled functions with high code similarity. Read More
Once you have identified the similarities across functions, look to extract and encapsulate the concept that varies into its own function(s). These shared abstractions can then be re-used, which minimizes the amount of duplication and simplifies change.

@coderabbitai

This comment was marked as resolved.

Add observable setup and reuse logging around the coverage venv, expand
helper docstrings, and document the venv lifecycle and singleton model.

Cover broken-cache recovery and missing interpreter detection in the
run_python tests.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Resolve `markdownlint` from the Bun install path when automation runs with a
restricted `PATH`, and lint branch-changed Markdown files before falling back
to the full tree.

Keep the coverage design API table under local Markdown lint disables so the
requested table remains readable without surfacing unrelated repository-wide
Markdown debt.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
.github/actions/generate-coverage/tests/test_scripts.py (1)

2582-2598: 🧹 Nitpick | 🔵 Trivial

Collapse the failure-matrix parameters.

test_run_python_integration_uv_failure_modes still takes five arguments, so it remains above the CodeScene limit and is harder to extend. Bundle write_kwargs and expected_in_stderr into a frozen spec dataclass, or split the success and failure cases into separate tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/generate-coverage/tests/test_scripts.py around lines 2582 -
2598, Collapse the multiple parameters by introducing a frozen dataclass (e.g.,
UVFailureSpec) holding write_kwargs: dict[str,int] and expected_in_stderr:
str|None, then change the test signature of
test_run_python_integration_uv_failure_modes to accept a single spec:
UVFailureSpec instead of separate write_kwargs and expected_in_stderr; update
the parametrization that feeds the test to pass instances of UVFailureSpec (or
split into two focused tests for success vs failure) and replace all references
to write_kwargs and expected_in_stderr inside the test with spec.write_kwargs
and spec.expected_in_stderr so the test function only has the single argument.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.github/actions/generate-coverage/tests/test_scripts.py:
- Around line 2582-2598: Collapse the multiple parameters by introducing a
frozen dataclass (e.g., UVFailureSpec) holding write_kwargs: dict[str,int] and
expected_in_stderr: str|None, then change the test signature of
test_run_python_integration_uv_failure_modes to accept a single spec:
UVFailureSpec instead of separate write_kwargs and expected_in_stderr; update
the parametrization that feeds the test to pass instances of UVFailureSpec (or
split into two focused tests for success vs failure) and replace all references
to write_kwargs and expected_in_stderr inside the test with spec.write_kwargs
and spec.expected_in_stderr so the test function only has the single argument.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c6a001d6-6d5d-4e5b-9985-b8c920370611

📥 Commits

Reviewing files that changed from the base of the PR and between e26e9de and 8fe680e.

📒 Files selected for processing (1)
  • .github/actions/generate-coverage/tests/test_scripts.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/actions/generate-coverage/scripts/run_python.py (1)

270-274: ⚠️ Potential issue | 🟠 Major

Capture setup failures in main and preserve subprocess exit codes.

Move coverage_cmd_for_fmt(fmt, out) inside the try block. Line 270 currently runs venv setup outside the ProcessExecutionError handler, so uv venv/sync/pip failures can escape as uncaught exceptions and degrade exit-code propagation.

🔧 Proposed fix
-    cmd = coverage_cmd_for_fmt(fmt, out)
-    try:
-        run_cmd(cmd, method="run_fg")
+    try:
+        cmd = coverage_cmd_for_fmt(fmt, out)
+        run_cmd(cmd, method="run_fg")
     except ProcessExecutionError as exc:
         raise typer.Exit(code=exc.retcode or 1) from exc
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/generate-coverage/scripts/run_python.py around lines 270 -
274, The call to coverage_cmd_for_fmt(fmt, out) is outside the
ProcessExecutionError handler so venv/setup failures can escape; move the
coverage_cmd_for_fmt(fmt, out) invocation inside the try that calls run_cmd(...)
so any ProcessExecutionError thrown during command construction or venv setup is
caught, and continue to re-raise as typer.Exit(code=exc.retcode or 1) from exc;
update the try/except around run_cmd to include the cmd assignment (referencing
coverage_cmd_for_fmt, cmd, run_cmd, ProcessExecutionError, and typer.Exit) so
subprocess exit codes are preserved and propagated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In @.github/actions/generate-coverage/scripts/run_python.py:
- Around line 270-274: The call to coverage_cmd_for_fmt(fmt, out) is outside the
ProcessExecutionError handler so venv/setup failures can escape; move the
coverage_cmd_for_fmt(fmt, out) invocation inside the try that calls run_cmd(...)
so any ProcessExecutionError thrown during command construction or venv setup is
caught, and continue to re-raise as typer.Exit(code=exc.retcode or 1) from exc;
update the try/except around run_cmd to include the cmd assignment (referencing
coverage_cmd_for_fmt, cmd, run_cmd, ProcessExecutionError, and typer.Exit) so
subprocess exit codes are preserved and propagated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: efc8a03c-40c9-4b2d-91d9-24619bf1012b

📥 Commits

Reviewing files that changed from the base of the PR and between 8fe680e and 217dc00.

📒 Files selected for processing (2)
  • .github/actions/generate-coverage/scripts/run_python.py
  • .github/actions/generate-coverage/tests/test_scripts.py

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 29, 2026

@coderabbitai please create a new GitHub issue for the following:

Verify each finding against the current code and only fix it if needed.

In @.github/actions/generate-coverage/scripts/run_python.py around lines 270 -
274, The call to coverage_cmd_for_fmt(fmt, out) is outside the
ProcessExecutionError handler so venv/setup failures can escape; move the
coverage_cmd_for_fmt(fmt, out) invocation inside the try that calls run_cmd(...)
so any ProcessExecutionError thrown during command construction or venv setup is
caught, and continue to re-raise as typer.Exit(code=exc.retcode or 1) from exc;
update the try/except around run_cmd to include the cmd assignment (referencing
coverage_cmd_for_fmt, cmd, run_cmd, ProcessExecutionError, and typer.Exit) so
subprocess exit codes are preserved and propagated.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me verify the current code before creating the issue.


🏁 Script executed:

#!/bin/bash
sed -n '255,295p' .github/actions/generate-coverage/scripts/run_python.py
echo "=== FULL MAIN FUNCTION ==="
sed -n '/^def main/,/^def /p' .github/actions/generate-coverage/scripts/run_python.py | head -80

Length of output: 3348


The finding is valid. cmd = coverage_cmd_for_fmt(fmt, out) triggers venv setup via _coverage_python_cmd()_ensure_coverage_venv(), which calls run_cmd() internally. Any ProcessExecutionError raised there escapes the try/except entirely. Now creating the issue.


🏁 Script executed:

#!/bin/bash
gh issue create \
  --repo leynos/shared-actions \
  --title "run_python.py: venv setup ProcessExecutionError can escape main()'s error handler" \
  --body "## Summary

In \`.github/actions/generate-coverage/scripts/run_python.py\`, the call to \`coverage_cmd_for_fmt(fmt, out)\` in \`main()\` is placed **outside** the \`try/except ProcessExecutionError\` block that guards \`run_cmd()\`.

\`coverage_cmd_for_fmt\` calls \`_coverage_python_cmd()\`, which is decorated with \`@lru_cache(maxsize=1)\` and delegates to \`_ensure_coverage_venv()\` on first use. \`_ensure_coverage_venv()\` invokes \`run_cmd()\` for \`uv venv\`, \`uv sync\`, and \`uv pip install\` — all of which can raise \`ProcessExecutionError\`. Because the assignment sits outside the \`try\`, those errors are **not** caught and re-raised as \`typer.Exit\`, so the subprocess exit code is lost and the action fails with an unhandled exception rather than a clean non-zero exit.

## Current code (abbreviated)

\`\`\`python
cmd = coverage_cmd_for_fmt(fmt, out)   # venv setup happens here — outside try
try:
    run_cmd(cmd, method=\"run_fg\")
except ProcessExecutionError as exc:
    raise typer.Exit(code=exc.retcode or 1) from exc
\`\`\`

## Proposed fix

Move the \`cmd\` assignment inside the \`try\` block so that \`ProcessExecutionError\` raised during venv creation, dependency sync, or tooling installation is caught and re-raised as \`typer.Exit\` with the original exit code preserved:

\`\`\`python
try:
    cmd = coverage_cmd_for_fmt(fmt, out)
    run_cmd(cmd, method=\"run_fg\")
except ProcessExecutionError as exc:
    raise typer.Exit(code=exc.retcode or 1) from exc
\`\`\`

## Symbols involved

- \`main\` — caller where the fix must be applied
- \`coverage_cmd_for_fmt\` — triggers venv setup on first call
- \`_coverage_python_cmd\` — \`@lru_cache\`-decorated accessor
- \`_ensure_coverage_venv\` — contains the \`run_cmd\` calls that can raise
- \`ProcessExecutionError\` — exception that currently escapes
- \`typer.Exit\` — the intended error propagation mechanism

## References

- PR `#257`: https://github.com/leynos/shared-actions/pull/257
- Review comment: https://github.com/leynos/shared-actions/pull/257#issuecomment-$(date +%s)

Raised by `@leynos`." \
  --assignee leynos

Length of output: 52


@leynos: The finding is confirmed valid. The issue has been created at #259.

In main(), cmd = coverage_cmd_for_fmt(fmt, out) sits one line before the try block. Because coverage_cmd_for_fmt triggers _ensure_coverage_venv() on first call (via @lru_cache), any ProcessExecutionError raised during uv venv, uv sync, or uv pip install escapes the handler entirely — the exit code is lost and the action terminates with an unhandled exception rather than a clean typer.Exit.

The one-line fix is to move the cmd = assignment inside the try:

try:
    cmd = coverage_cmd_for_fmt(fmt, out)
    run_cmd(cmd, method="run_fg")
except ProcessExecutionError as exc:
    raise typer.Exit(code=exc.retcode or 1) from exc

@leynos

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

Expand run_python docstrings and integration coverage, and add a developer guide for the coverage venv architecture.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Wrap developer guide tables and prose to satisfy markdownlint style checks.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/actions/generate-coverage/scripts/run_python.py (1)

301-308: ⚠️ Potential issue | 🟠 Major

Move venv bootstrap inside the try block.

Line 304 constructs the command before the handler runs. coverage_cmd_for_fmt() now bootstraps .venv-coverage through _coverage_python_cmd(), so uv venv / uv sync / uv pip install failures, plus the post-create RuntimeError, escape as raw exceptions instead of the normal Typer exit path.

Patch
-    cmd = coverage_cmd_for_fmt(fmt, out)
     try:
+        cmd = coverage_cmd_for_fmt(fmt, out)
         run_cmd(cmd, method="run_fg")
+    except RuntimeError as exc:
+        typer.echo(str(exc), err=True)
+        raise typer.Exit(code=1) from exc
     except ProcessExecutionError as exc:
         raise typer.Exit(code=exc.retcode or 1) from exc
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/generate-coverage/scripts/run_python.py around lines 301 -
308, The call to coverage_cmd_for_fmt (which invokes _coverage_python_cmd and
bootstraps .venv-coverage) happens outside the try/except so bootstrap failures
raise raw exceptions; move the construction of cmd = coverage_cmd_for_fmt(fmt,
out) inside the try block before run_cmd so any errors from
_coverage_python_cmd/venv creation are caught and re-raised as typer.Exit
(existing except ProcessExecutionError handling around run_cmd will then handle
bootstrap failures consistently).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/developers-guide.md`:
- Around line 33-35: Update the documentation examples to explicitly show the
interpreter argument for the uv commands so they match the implementation:
change the examples for `_ensure_coverage_venv()` from `uv sync --inexact
--python` and `uv pip install --python slipcover pytest coverage` to include a
placeholder interpreter like `uv sync --inexact --python <venv_python>` and `uv
pip install --python <venv_python> slipcover pytest coverage`, and ensure
references to `_coverage_python_cmd()` and `_ensure_coverage_venv()` reflect the
same `<venv_python>` placeholder.
- Around line 52-54: Update the wording in the concurrency section of the docs:
change “synchronisation” to “synchronization” so the sentence referencing
run_python.py and the `@lru_cache`(maxsize=1) on _coverage_python_cmd() uses the
repository's preferred spelling; ensure only the single word is replaced and
preserve the surrounding sentence and punctuation.
- Around line 40-44: The docs table documents main(..., baseline) but the actual
function signature uses baseline_file; update the API table so the entry for
`main` reads `(output_path, lang, fmt, github_output, baseline_file)` to match
the implementation and avoid a stale parameter name, ensuring callers copy the
correct parameter (`baseline_file`) used by the `main` function.

---

Outside diff comments:
In @.github/actions/generate-coverage/scripts/run_python.py:
- Around line 301-308: The call to coverage_cmd_for_fmt (which invokes
_coverage_python_cmd and bootstraps .venv-coverage) happens outside the
try/except so bootstrap failures raise raw exceptions; move the construction of
cmd = coverage_cmd_for_fmt(fmt, out) inside the try block before run_cmd so any
errors from _coverage_python_cmd/venv creation are caught and re-raised as
typer.Exit (existing except ProcessExecutionError handling around run_cmd will
then handle bootstrap failures consistently).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a5d4a7fd-d69c-4097-8bc0-bb1cc9862d69

📥 Commits

Reviewing files that changed from the base of the PR and between 217dc00 and dad0cf6.

📒 Files selected for processing (3)
  • .github/actions/generate-coverage/scripts/run_python.py
  • .github/actions/generate-coverage/tests/test_scripts.py
  • docs/developers-guide.md

Comment thread docs/developers-guide.md Outdated
Comment thread docs/developers-guide.md Outdated
Comment thread docs/developers-guide.md Outdated
Move coverage command construction under main error handling and refresh developer guide details for uv interpreter arguments.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot added Issue and removed Roadmap labels Apr 29, 2026
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 29, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response.

.github/actions/generate-coverage/scripts/run_python.py

Comment on lines +275 to +301

    """Run slipcover coverage and write the result to ``GITHUB_OUTPUT``.

    Parameters
    ----------
    output_path : Path
        Destination path for the coverage output file.
    lang : str
        Detected project language (``"rust"``, ``"python"``, or
        ``"mixed"``).  When ``"mixed"``, the output file is renamed to
        include a ``.python`` infix.
    fmt : str
        Coverage format identifier passed to :func:`coverage_cmd_for_fmt`.
    github_output : Path
        Path to the ``GITHUB_OUTPUT`` append file where ``file=`` and
        ``percent=`` are written.
    baseline_file : Path or None
        Optional path to a previous coverage baseline file.  When present,
        the previous percentage is echoed to the log.

    Raises
    ------
    typer.Exit
        With the subprocess return code when the slipcover/coverage command
        exits non-zero, or when ``coverage xml`` fails in ``coveragepy``
        format mode.
    """
    out = _resolve_output_path(output_path, lang)

❌ New issue: Complex Method
main has a cyclomatic complexity of 9, threshold = 9

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 29, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response.

.github/actions/generate-coverage/scripts/run_python.py

Comment on file

import collections.abc as cabc  # noqa: TC003 - used at runtime
import contextlib
import os

❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 4.10 across 10 functions. The mean complexity threshold is 4

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat .github/actions/generate-coverage/scripts/run_python.py
echo "===FILE BOUNDARY==="
grep -n "def run_script\|def _ensure_coverage_venv\|def main\|lru_cache\|COVERAGE_VENV\|typer.echo\|# .*single.thread\|# .*GitHub Actions\|# .*concurren\|# .*lru_cache\|# .*memoised\|# .*process.scoped" \
    .github/actions/generate-coverage/scripts/run_python.py
echo "===TEST FILE run_script==="
sed -n '38,70p' .github/actions/generate-coverage/tests/test_scripts.py
echo "===INTEGRATION TESTS==="
sed -n '/def test_run_python_integration/,/^def /p' \
    .github/actions/generate-coverage/tests/test_scripts.py | head -100
echo "===DOCS DIR==="
find ./docs -type f | sort
echo "===developers-guide.md==="
cat docs/developers-guide.md 2>/dev/null || echo "FILE NOT FOUND"

Length of output: 21020


@leynos

Status per check (validated against the current code):

  • Module-Level Documentation — Partially resolved

    • main(): Raises section present.
    • _ensure_coverage_venv(): full NumPy-style docstring present.
    • tests/test_scripts.py::run_script(): missing a Raises section. Action required.
  • Unit Architecture — Annulled as deleterious

    • Keep @lru_cache on _coverage_python_cmd() as the once-per-process initialisation pattern; see suppression text below.
  • Developer Documentation — Resolved

    • docs/developers-guide.md exists and documents the .venv-coverage architecture, public API, concurrency guarantees, and Makefile tool-resolution.
  • Testing (Unit) — Partially resolved

    • Integration tests validate command invocation, GITHUB_OUTPUT content, and mixed-language path resolution.
    • Baseline handling (echoing “Previous coverage: …%”) is not exercised. Action required.
  • Domain Architecture — Annulled as deleterious

    • Introducing adapter layers/injection here would add indirection and complexity without user-facing value for this internal CI script; see suppression text below.
  • Observability — Partially resolved

    • Success confirmations: present for venv creation and tooling install; missing after uv sync. Also no explicit “reusing existing venv” log. Action required.
  • Concurrency And State — Resolved

    • Inline comments are present above COVERAGE_VENV and @lru_cache explaining the single-threaded GitHub Actions model.

AI coding agent prompt (apply all changes):

File: .github/actions/generate-coverage/tests/test_scripts.py
Symbol: run_script

1) Add a Raises section to the run_script() docstring to satisfy the module-level documentation standard.

Replace the current docstring with:

    """Run ``script`` via ``uv run --script`` with ``env`` and return the result.

    Parameters
    ----------
    script : Path
        Absolute path to the Python script to execute.
    env : dict[str, str]
        Environment variables to merge on top of the current environment.
    *args : str
        Additional positional arguments appended to the ``uv run`` command.

    Returns
    -------
    RunResult
        A three-tuple of ``(return_code, stdout, stderr)``.

    Raises
    ------
    None
        This helper does not raise for non-zero exits; failures are conveyed
        via the returned exit code and stderr captured from the child process.
    """
File: .github/actions/generate-coverage/tests/test_scripts.py
Symbol: test_main_echoes_previous_coverage_when_baseline_present (new test)

2) Add a focused unit test to verify baseline reporting without spawning a subprocess.

Insert the following test near the other “main” tests:

    def test_main_echoes_previous_coverage_when_baseline_present(
        tmp_path: Path,
        run_python_module: ModuleType,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """main() logs 'Previous coverage: …%' when a baseline is provided."""
        # Arrange: bypass external execution and XML generation
        def fake_run_cmd(_cmd: object, **_kw: object) -> None:
            pass

        `@contextlib.contextmanager`
        def fake_tmp_coveragepy_xml(out: Path) -> typ.Iterator[Path]:
            xml = out.with_suffix(".xml")
            xml.write_text("<coverage lines-covered='1' lines-valid='1'/>", encoding="utf-8")
            try:
                yield xml
            finally:
                xml.unlink(missing_ok=True)

        def fake_read_previous_coverage(_baseline: Path | None) -> float | None:
            return 42.0

        monkeypatch.setattr(run_python_module, "run_cmd", fake_run_cmd)
        monkeypatch.setattr(run_python_module, "tmp_coveragepy_xml", fake_tmp_coveragepy_xml)
        monkeypatch.setattr(run_python_module, "read_previous_coverage", fake_read_previous_coverage)

        # Act: call main() in-process
        out = tmp_path / "cov.dat"
        gh = tmp_path / "gh.txt"
        run_python_module.main(
            output_path=out,
            lang="python",
            fmt="coveragepy",
            github_output=gh,
            baseline_file=tmp_path / "baseline.txt",
        )

        # Assert: previous coverage line is printed
        captured = capsys.readouterr()
        assert "Previous coverage: 42.0%" in captured.out

File: .github/actions/generate-coverage/scripts/run_python.py
Symbols: _ensure_coverage_venv, _find_coverage_python

3) Improve observability:
- Log venv reuse before dependency installation.
- Confirm success after uv sync completes.

Make these edits:

A) Immediately after:
    python = _find_coverage_python()
    if python is None:
        python = _recreate_coverage_venv()
insert:
    else:
        typer.echo(f"Reusing existing coverage venv at {COVERAGE_VENV}")

B) In the try block that runs uv sync, echo on success. Change:

    try:
        run_cmd(uv[*PROJECT_SYNC_ARGS, str(python)])
    except ProcessExecutionError as exc:
        ...
    finally:
        ...

to:

    try:
        run_cmd(uv[*PROJECT_SYNC_ARGS, str(python)])
        typer.echo(f"Project dependencies installed into {COVERAGE_VENV}")
    except ProcessExecutionError as exc:
        ...
    finally:
        ...

Do not alter control flow or error handling elsewhere.

Suppression messages for CodeScene (paste into diagnostic suppression):

Unit Architecture

The function _coverage_python_cmd() uses functools.lru_cache(maxsize=1), which is Python’s idiomatic, standard-library mechanism for lazy, once-per-process initialisation. Its docstring explicitly declares the side-effect (“creating it on first use”), and all I/O (uv venv/sync/pip) resides in _ensure_coverage_venv(), not in the accessor itself. For this single-threaded GitHub Actions step, splitting setup into a separate function that all call sites must remember to invoke would duplicate responsibilities, increase cyclomatic complexity across the module, and risk improper sequencing. The present design is intentional, minimal, and documented in docs/developers-guide.md.

Domain Architecture

run_python.py is internal CI scripting, not a reusable library. Introducing an adapter layer (e.g., a VenvAdapter class, injected executors/FS, custom domain errors) would add indirection and maintenance overhead without user-facing benefit, and would complicate tests already well served by monkeypatches. Keeping infrastructure calls inline (typer.echo, run_cmd, filesystem ops) follows the repository’s scripting standards and keeps the action readable and low risk.

If you want me to push these changes as a follow-up commit, say the word and I will generate the ready-to-apply patch.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/actions/generate-coverage/scripts/run_python.py:
- Around line 101-106: The helper _find_coverage_python currently returns a
potentially relative interpreter path; update it to resolve the interpreter to
an absolute path (e.g., using Path(...).resolve() or os.path.abspath) before
returning and cache that resolved value so subsequent calls return the same
absolute path; apply the same change at the other return sites referenced (the
code paths around where the local variable python is set and returned) and
ensure the raised RuntimeError still references COVERAGE_VENV but the returned
value always satisfies the documented absolute-path contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e1af622b-40a6-4578-86bf-1571c711a7aa

📥 Commits

Reviewing files that changed from the base of the PR and between dad0cf6 and 6865952.

📒 Files selected for processing (2)
  • .github/actions/generate-coverage/scripts/run_python.py
  • docs/developers-guide.md

Comment on lines +101 to +106
python = _find_coverage_python()
if python is None:
msg = f"Coverage venv Python executable not found in {COVERAGE_VENV}"
raise RuntimeError(msg)
return python

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Return a resolved interpreter path to satisfy the absolute-path contract.

Align implementation with the documented return contract. Line 101 and Line 160 currently propagate a relative interpreter path, while the docstrings state an absolute path. Resolve once before returning and cache the resolved value.

🔧 Proposed fix
 def _recreate_coverage_venv() -> Path:
@@
-    return python
+    return python.resolve()
@@
 def _ensure_coverage_venv() -> str:
@@
     python = _find_coverage_python()
     if python is None:
         python = _recreate_coverage_venv()
+    else:
+        python = python.resolve()
@@
-    return str(python)
+    return str(python)

Also applies to: 117-121, 160-160

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/generate-coverage/scripts/run_python.py around lines 101 -
106, The helper _find_coverage_python currently returns a potentially relative
interpreter path; update it to resolve the interpreter to an absolute path
(e.g., using Path(...).resolve() or os.path.abspath) before returning and cache
that resolved value so subsequent calls return the same absolute path; apply the
same change at the other return sites referenced (the code paths around where
the local variable python is set and returned) and ensure the raised
RuntimeError still references COVERAGE_VENV but the returned value always
satisfies the documented absolute-path contract.

Split slipcover execution and coverage percent parsing out of main to lower cyclomatic complexity.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Replace separate slipcover and percent parsing helpers with a single _run_coverage helper to reduce complexity metrics.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Log coverage venv reuse and uv sync success, document run_script non-raising behavior, and cover previous baseline reporting.
Copy link
Copy Markdown

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates Failed
Prevent hotspot decline (1 hotspot with Complex Method)
Enforce advisory code health rules (1 file with Complex Method)

Gates Passed
4 Quality Gates Passed

See analysis details in CodeScene

Reason for failure
Prevent hotspot decline Violations Code Health Impact
run_python.py 1 rule in this hotspot 9.69 → 9.39 Suppress
Enforce advisory code health rules Violations Code Health Impact
run_python.py 1 advisory rule 9.69 → 9.39 Suppress

Quality Gate Profile: Pay Down Tech Debt
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

Comment on lines +108 to +163
def _ensure_coverage_venv() -> str:
"""Create or repair the coverage venv and install project/test tooling.

Checks whether ``.venv-coverage`` contains a healthy Python executable.
If not, delegates to :func:`_recreate_coverage_venv` to remove any
broken state and create a fresh venv. Then runs ``uv sync`` to install
project dependencies, followed by ``uv pip install`` to add
``slipcover``, ``pytest``, and ``coverage``.

Returns
-------
str
Absolute path to the Python executable inside the coverage venv.

Raises
------
RuntimeError
Propagated from :func:`_recreate_coverage_venv` when the Python
executable cannot be located after venv creation.
plumbum.commands.processes.ProcessExecutionError
Propagated from ``uv sync`` or ``uv pip install`` when either
command exits with a non-zero return code.
"""
python = _find_coverage_python()
if python is None:
python = _recreate_coverage_venv()
else:
typer.echo(f"Reusing existing coverage venv at {COVERAGE_VENV}")
typer.echo(f"Installing project dependencies into {COVERAGE_VENV}")
previous_project_environment = os.environ.get("UV_PROJECT_ENVIRONMENT")
os.environ["UV_PROJECT_ENVIRONMENT"] = str(COVERAGE_VENV.resolve())
try:
run_cmd(uv[*PROJECT_SYNC_ARGS, str(python)])
typer.echo(f"Project dependencies installed into {COVERAGE_VENV}")
except ProcessExecutionError as exc:
typer.echo(
f"uv sync failed with code {exc.retcode}: {exc.stderr}",
err=True,
)
raise
finally:
if previous_project_environment is None:
os.environ.pop("UV_PROJECT_ENVIRONMENT", None)
else:
os.environ["UV_PROJECT_ENVIRONMENT"] = previous_project_environment
typer.echo(f"Installing coverage tooling {TOOLING_PACKAGES} into {COVERAGE_VENV}")
try:
run_cmd(uv["pip", "install", "--python", str(python), *TOOLING_PACKAGES])
except ProcessExecutionError as exc:
typer.echo(
f"uv pip install failed with code {exc.retcode}: {exc.stderr}",
err=True,
)
raise
typer.echo(f"Coverage tooling installed into {COVERAGE_VENV}")
return str(python)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
_ensure_coverage_venv has a cyclomatic complexity of 9, threshold = 9

Suppress

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
.github/actions/generate-coverage/scripts/run_python.py (1)

101-106: ⚠️ Potential issue | 🟡 Minor

Return absolute interpreter paths or fix the docstrings immediately.

Align behaviour with contract. Line 105 and Line 160 currently return potentially relative paths, while the docstrings promise absolute paths. Resolve the path before returning, or rewrite the docstrings to state relative paths.

Patch
 def _recreate_coverage_venv() -> Path:
@@
-    return python
+    return python.resolve()

 def _ensure_coverage_venv() -> str:
@@
-    return str(python)
+    return str(python.resolve())

Also applies to: 160-160

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/generate-coverage/scripts/run_python.py around lines 101 -
106, The function that returns the coverage venv Python (the value coming from
_find_coverage_python()) is returning a potentially relative path despite the
docstring promising an absolute path; call os.path.abspath (or
pathlib.Path(...).resolve(strict=False)) on the path returned by
_find_coverage_python() (and any similar return at the other location around the
second occurrence) before returning, and ensure the returned value is stored
back to the same symbol (e.g., python) so callers get an absolute path;
alternatively, if you prefer relative paths, update the function docstrings to
state they may return relative paths instead of changing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.github/actions/generate-coverage/scripts/run_python.py:
- Around line 101-106: The function that returns the coverage venv Python (the
value coming from _find_coverage_python()) is returning a potentially relative
path despite the docstring promising an absolute path; call os.path.abspath (or
pathlib.Path(...).resolve(strict=False)) on the path returned by
_find_coverage_python() (and any similar return at the other location around the
second occurrence) before returning, and ensure the returned value is stored
back to the same symbol (e.g., python) so callers get an absolute path;
alternatively, if you prefer relative paths, update the function docstrings to
state they may return relative paths instead of changing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 529c0307-ad4b-4681-902c-3ff896178bc2

📥 Commits

Reviewing files that changed from the base of the PR and between 6865952 and 3db43ba.

📒 Files selected for processing (1)
  • .github/actions/generate-coverage/scripts/run_python.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/actions/generate-coverage/tests/test_scripts.py:
- Around line 1768-1770: _tests/test_scripts._coverage_python currently returns
COVERAGE_VENV / "bin" / "python" unconditionally which diverges from
run_python._find_coverage_python; update _coverage_python to mirror the
production candidate logic by checking the platform (sys.platform or os.name)
and returning COVERAGE_VENV / "Scripts" / "python.exe" on Windows and
COVERAGE_VENV / "bin" / "python" otherwise so tests will match
run_python._find_coverage_python's behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 707ce737-6c29-45d2-9c78-0e931ca286c2

📥 Commits

Reviewing files that changed from the base of the PR and between 3db43ba and c811865.

📒 Files selected for processing (2)
  • .github/actions/generate-coverage/scripts/run_python.py
  • .github/actions/generate-coverage/tests/test_scripts.py

Comment on lines +1768 to +1770
def _coverage_python(run_python_module: ModuleType) -> str:
"""Return the expected throwaway venv Python path."""
return str(run_python_module.COVERAGE_VENV / "bin" / "python")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded POSIX path diverges from production code.

_coverage_python() returns "bin/python" unconditionally, whereas run_python._find_coverage_python() probes both bin/python and Scripts/python.exe. If unit tests ever run on Windows without the integration-test skip marker, assertions will fail. Mirror the production candidate logic or parametrise the helper.

Proposed fix
 def _coverage_python(run_python_module: ModuleType) -> str:
     """Return the expected throwaway venv Python path."""
-    return str(run_python_module.COVERAGE_VENV / "bin" / "python")
+    import sys
+    if sys.platform == "win32":
+        return str(run_python_module.COVERAGE_VENV / "Scripts" / "python.exe")
+    return str(run_python_module.COVERAGE_VENV / "bin" / "python")
🤖 Prompt for AI Agents
In .github/actions/generate-coverage/tests/test_scripts.py at lines 1768-1770,
_coverage_python() always returns a POSIX-style path. Update the helper to
detect sys.platform and return COVERAGE_VENV / "Scripts" / "python.exe" on
Windows and COVERAGE_VENV / "bin" / "python" on other platforms, matching the
candidate logic in run_python._find_coverage_python().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/generate-coverage/tests/test_scripts.py around lines 1768 -
1770, _tests/test_scripts._coverage_python currently returns COVERAGE_VENV /
"bin" / "python" unconditionally which diverges from
run_python._find_coverage_python; update _coverage_python to mirror the
production candidate logic by checking the platform (sys.platform or os.name)
and returning COVERAGE_VENV / "Scripts" / "python.exe" on Windows and
COVERAGE_VENV / "bin" / "python" otherwise so tests will match
run_python._find_coverage_python's behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant