Skip to content

Adopt pytest-xdist in generate-coverage (#278)#279

Merged
leynos merged 16 commits into
mainfrom
issue-278-adopt-pytest-xdist-in-generate-coverage
Jun 5, 2026
Merged

Adopt pytest-xdist in generate-coverage (#278)#279
leynos merged 16 commits into
mainfrom
issue-278-adopt-pytest-xdist-in-generate-coverage

Conversation

@lodyai

@lodyai lodyai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add configurable pytest-xdist support to the generate-coverage composite action for Python coverage runs.
  • Python coverage now runs with pytest -n auto by default via the new pytest-workers input.
  • Install pytest-xdist in the dedicated .venv-coverage tooling environment and require slipcover>=1.0.18 so xdist worker coverage can be merged correctly.
  • Allow consumers to disable parallel pytest execution by setting pytest-workers: "".

Functional changes

  • Introduces a new action input:

    pytest-workers

    Accepted values are:

    • auto
    • logical
    • a positive integer such as 1, 2, or 4
    • an empty string "" to run pytest serially
  • Threads the input through the action environment as INPUT_PYTEST_WORKERS and into the Python coverage runner.

  • Adds validation for pytest-workers before invoking slipcover/pytest:

    • valid named values are normalized to lowercase
    • surrounding whitespace is ignored
    • 0, negative numbers, floats, and arbitrary strings are rejected
    • invalid values exit with code 2 and a clear configuration error
  • Adds runtime logging to show whether the coverage run is using pytest-xdist or serial pytest execution.

  • Updates slipcover invocation so non-empty worker values append:

    -n <workers>

    after the pytest arguments.

Documentation updates

  • README documents the new pytest-workers input, default behavior, and how to disable xdist.
  • README adds guidance for the known slipcover 1.0.18 xdist regression where --omit is not propagated to worker processes.
  • CHANGELOG records the new default parallel Python coverage behavior and the related slipcover/xdist caveat.

Tests

  • Adds unit coverage for:
    • installing pytest-xdist with coverage tooling
    • enforcing the slipcover>=1.0.18 version floor
    • adding or omitting -n based on the workers value
    • worker value parsing, normalization, precedence, and error handling
    • forwarding worker settings from main() into the slipcover pytest command
    • action environment propagation of INPUT_PYTEST_WORKERS
  • Updates command-structure assertions so they remain valid when additional pytest arguments follow pytest -v.

Drive-by test robustness fix

  • Updates the setup-rust workspace validator test helper to strip ANSI escape sequences before filtering known uv VIRTUAL_ENV warning noise.
  • Adds a regression test to ensure colored uv warnings are removed while real stderr content is preserved.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Add pytest-workers input and forward it into the Python coverage runner; install and pin pytest-xdist and slipcover>=1.0.18; validate and thread worker values into slipcover/pytest -n; expand tests and document the change. Separately, strip ANSI escapes before filtering noisy stderr in setup-rust tests.

Changes

pytest-xdist integration in generate-coverage

Layer / File(s) Summary
Action input definition and environment wiring
.github/actions/generate-coverage/action.yml
Define pytest-workers input (default auto) and export it to the Python step as INPUT_PYTEST_WORKERS.
Tooling installation and default
.github/actions/generate-coverage/scripts/run_python.py
Pin/install slipcover>=1.0.18, include pytest-xdist in tooling, and add DEFAULT_PYTEST_WORKERS = "auto".
Worker parsing, normalisation and coverage-args construction
.github/actions/generate-coverage/scripts/run_python.py
Implement parsing/normalisation of pytest-workers and update _coverage_args/coverage_cmd_for_fmt to append -n <workers> when non-empty.
Run coverage propagation and resolution
.github/actions/generate-coverage/scripts/run_python.py
Propagate workers through _run_coverage and add _resolve_pytest_workers with CLI → ENV → default precedence.
CLI option and main wiring
.github/actions/generate-coverage/scripts/run_python.py
Add pytest_workers Typer option, resolve and log effective workers in main(), and pass workers into _run_coverage.
Unit and integration tests for workers wiring
.github/actions/generate-coverage/tests/test_scripts.py
Add Hypothesis and example tests for parsing, validate -n omission/appending, assert pytest-xdist installed, forward workers from main to slipcover argv, and strengthen integration env/tooling assertions.
Changelog, README and design docs
.github/actions/generate-coverage/CHANGELOG.md, .github/actions/generate-coverage/README.md, docs/*.md
Document pytest-workers input and default auto, describe disabling via pytest-workers: "", warn about slipcover/xdist --omit caveat, and update design/developer docs and changelog entries.

Test output normalisation in setup-rust

Layer / File(s) Summary
ANSI escape sequence stripping in stderr cleaning
.github/actions/setup-rust/tests/test_validate_workspaces.py
Import ANSI_ESCAPE_RE, expand _clean_stderr docstring to mention ANSI stripping before noise matching, and add a test verifying ANSI removal and known-noise filtering.

Sequence Diagram(s)

sequenceDiagram
  participant Action as GitHub Action
  participant Runner as run_python.py
  participant Slipcover as slipcover
  participant Xdist as pytest-xdist
  Action->>Runner: set INPUT_PYTEST_WORKERS and invoke Python step
  Runner->>Slipcover: build slipcover argv including pytest args and add `-n <workers>` when set
  Slipcover->>Xdist: spawn workers with `-n` and merge per-worker coverage
Loading

Possibly related PRs

Suggested labels

Roadmap

Poem

Run tests in parallel, speed the flow,
Slipcover spawns workers, watch them go.
Wire the input, normalise the flag,
Strip ANSI noise, tidy the log sag.
Coverage marches on, bright and slow.


Caution

Pre-merge checks failed

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

  • Ignore

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

Check name Status Explanation Resolution
Testing (Overall) ❌ Error test_parse_pytest_workers_normalises_named_values has 5 parameters, failing the CodeScene quality gate noted in review comments. Combine leading/trailing parameters into a padding tuple to reduce parameter count from 5 to 4.
Unit Architecture ❌ Error Two explicit review comments remain unaddressed: spelling error in docs/developers-guide.md (line 61: "parallelises"→"parallelizes") and test parameter count exceeds CodeScene threshold. Fix spelling: "parallelises"→"parallelizes" in docs/developers-guide.md. Refactor test_parse_pytest_workers_normalises_named_values to reduce parameter count by merging leading/trailing into a single tuple parameter.
Security And Privacy ❌ Error Spelling violation in docs/developers-guide.md: "parallelises" violates en-GB-oxendict (-ize) rule; must be "parallelizes". Security and privacy posture is strong. Replace "parallelises" with "parallelizes" in docs/developers-guide.md per en-GB-oxendict spelling guidelines.
Developer Documentation ⚠️ Warning docs/developers-guide.md line 61 violates en-GB-oxendict: uses "parallelises" instead of "parallelizes". Test function has 5 parameters, exceeding CodeScene threshold. Change "parallelises" to "parallelizes" in docs/developers-guide.md line 61. Refactor test_parse_pytest_workers_normalises_named_values to combine leading/trailing into tuple parameter, reducing from 5 to 4 parameters.
Domain Architecture ⚠️ Warning Domain validation entangled with CLI infrastructure: _resolve_pytest_workers couples env-var reading to CLI-aware _normalise_pytest_workers with typer.Exit side-effects. Move CLI error handling to main(). Have _resolve_pytest_workers call pure _parse_pytest_workers; let main() call _normalise_pytest_workers at the CLI boundary.
Architectural Complexity And Maintainability ⚠️ Warning Test function test_parse_pytest_workers_normalises_named_values retains 5 parameters despite explicit review feedback recommending refactor to 4 to pass CodeScene quality gate. Apply the proposed refactor: combine leading and trailing parameters into single padding tuple parameter to reduce function parameter count from 5 to 4.
Concurrency And State ❓ Inconclusive PR introduces pytest-xdist parallelism but delegates coverage merging to slipcover 1.0.18's undocumented xdist plugin. Concurrency model of worker coverage merging is not explained or tested. Document how slipcover 1.0.18's xdist plugin merges per-worker coverage data; add integration tests that actually execute pytest-xdist to verify coverage is correctly merged; clarify behaviour under partial failure or worker crashes.
✅ Passed checks (13 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Adopt pytest-xdist in generate-coverage (#278)' directly summarises the main change, includes the linked issue reference, and aligns with the comprehensive changeset across action configuration, scripts, tests, and documentation.
Description check ✅ Passed The pull request description is detailed and directly related to the changeset, covering functional changes, documentation updates, tests, and a drive-by fix; it comprehensively describes all aspects of the implementation.
Docstring Coverage ✅ Passed Docstring coverage is 97.56% 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 No docs/users-guide.md exists in repository. User-facing action documentation provided in standard GitHub Actions README.md with input, behaviour, disabling option, and known caveats documented.
Module-Level Documentation ✅ Passed All modified Python modules carry module-level docstrings explaining purpose, utility, and key relationships; no new modules were added in the PR.
Testing (Unit And Behavioural) ✅ Passed Unit tests verify parsing/validation edge cases and error paths; property tests exercise invariants; behavioural tests verify main() outputs; integration tests verify action.yml contract.
Testing (Property / Proof) ✅ Passed Hypothesis-based property tests properly cover introduced invariants: normalisation, round-trip parsing, whitespace handling, and boundary rejection. Tests are substantive, rigorous, and well-founded.
Testing (Compile-Time / Ui) ✅ Passed PR includes snapshot test for integration output with proper path redaction and direct assertions for logging. No compile-time behaviour. Testing strategy is sound.
Observability ✅ Passed The PR logs pytest-workers configuration at decision points before coverage execution, emitting clear messages for parallel/serial modes with stable contextual fields and validation errors to stderr.
Performance And Resource Use ✅ Passed No algorithmic regressions, unbounded allocations, or I/O in hot paths. Parsing is O(n) on bounded input. No new loops or blocking operations in single-threaded context.
Rust Compiler Lint Integrity ✅ Passed PR contains zero Rust source file modifications; custom check for Rust compiler lint integrity is not applicable to Python/YAML/documentation-only changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-278-adopt-pytest-xdist-in-generate-coverage

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

@sourcery-ai

sourcery-ai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Enable configurable parallel Python coverage runs via pytest-xdist in the generate-coverage action, adding a validated pytest-workers input, wiring it through the coverage runner, updating docs/CHANGELOG, and hardening stderr filtering for setup-rust workspace validation tests.

Sequence diagram for pytest-workers handling in generate-coverage

sequenceDiagram
    actor User
    participant Workflow as GitHubActionsWorkflow
    participant Action as generate_coverage_action
    participant Script as run_python_main
    participant Resolver as _resolve_pytest_workers
    participant Normaliser as _normalise_pytest_workers
    participant Runner as _run_coverage
    participant Cmd as coverage_cmd_for_fmt
    participant Pytest as pytest

    User->>Workflow: dispatch workflow
    Workflow->>Action: with pytest-workers
    Action->>Action: set INPUT_PYTEST_WORKERS env
    Action->>Script: uv run run_python.py

    Script->>Resolver: _resolve_pytest_workers(pytest_workers)
    alt CLI option omitted
        Resolver->>Resolver: os.getenv(INPUT_PYTEST_WORKERS)
        Resolver->>Resolver: fallback DEFAULT_PYTEST_WORKERS
    else CLI option provided
        Resolver->>Resolver: use pytest_workers
    end
    Resolver->>Normaliser: _normalise_pytest_workers(raw)
    Normaliser-->>Resolver: workers
    Resolver-->>Script: workers

    Script->>Runner: _run_coverage(fmt, out, workers)
    Runner->>Cmd: coverage_cmd_for_fmt(fmt, out, workers)
    Cmd->>Cmd: _coverage_args(fmt, out, workers)
    alt workers not empty
        Cmd->>Cmd: append "-n" and workers to PYTEST_ARGS
    end
    Cmd-->>Runner: BoundCommand
    Runner->>Pytest: run_cmd(Pytest, method="run_fg")
    Pytest-->>Runner: exit status
    Runner-->>Script: percent
    Script->>Action: write GITHUB_OUTPUT
    Action-->>Workflow: file, percent outputs
Loading

File-Level Changes

Change Details Files
Adopt pytest-xdist in the Python coverage runner with configurable worker count and input/env handling.
  • Include pytest-xdist in the temporary .venv-coverage tooling packages so -n is always available.
  • Extend slipcover/pytest argv construction to optionally add -n and thread this through coverage_cmd_for_fmt and _run_coverage.
  • Introduce DEFAULT_PYTEST_WORKERS and helper functions to normalise and resolve pytest worker values from CLI or INPUT_PYTEST_WORKERS (including validation and error handling).
  • Refactor main() option definitions into annotated aliases and add a pytest-workers CLI option that feeds into the worker resolution logic.
.github/actions/generate-coverage/scripts/run_python.py
Extend tests to cover pytest-xdist tooling, worker wiring, and new environment variable plumbing.
  • Relax the pytest invocation structure assertion to allow additional flags after -v.
  • Add unit tests for _coverage_args worker handling and coverage_cmd_for_fmt worker threading.
  • Add unit tests for _normalise_pytest_workers and _resolve_pytest_workers, including env-var precedence, defaults, and invalid-value exits.
  • Update integration-style tests to assert that INPUT_PYTEST_WORKERS is exported to the coverage script environment and that pytest-xdist is installed with other tooling packages.
.github/actions/generate-coverage/tests/test_scripts.py
Document the new pytest-workers input and pytest-xdist behavior including known slipcover xdist omit regression.
  • Add pytest-workers to the generate-coverage action input table with description, accepted values, and default.
  • Document how to disable xdist for serial pytest runs and describe the default parallel behavior and worker selection semantics.
  • Add a warning section about slipcover 1.0.18 xdist plugin not propagating --omit to workers and its impact on co-located tests.
  • Record these changes in the generate-coverage CHANGELOG under an Unreleased section.
.github/actions/generate-coverage/README.md
.github/actions/generate-coverage/CHANGELOG.md
.github/actions/generate-coverage/action.yml
Harden setup-rust workspace validation stderr cleaning against ANSI-colored uv output and update tests accordingly.
  • Strip ANSI escape sequences from stderr lines before filtering known uv virtual environment warnings to ensure robust matching when output is colored.
  • Keep the existing noise-prefix filtering logic but apply it after ANSI stripping.
  • Adjust or add tests for _clean_stderr to reflect the new ANSI-stripping behavior.
.github/actions/setup-rust/tests/test_validate_workspaces.py

Assessment against linked issues

Issue Objective Addressed Explanation
#278 Extend the generate-coverage action to run Python tests in parallel via pytest-xdist, by adding a configurable pytest-workers input (defaulting to auto, with "" disabling parallelism), installing pytest-xdist in the coverage venv, and wiring the worker value through to the pytest invocation.
#278 Document in the generate-coverage README the slipcover 1.0.18 pytest-xdist --omit regression and its implications for projects with co-located in-package tests, including guidance on when to disable parallelism.

Possibly linked issues


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 commented Jun 3, 2026

Copy link
Copy Markdown
Owner

@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 +523 to +531

    pytest_workers: typ.Annotated[
        str | None,
        typer.Option(
            help=(
                'Worker count for pytest-xdist (-n). Use an integer, "auto", '
                '"logical", or "" to disable parallelism. Defaults to "auto".'
            ),
        ),
    ] = None,

❌ New issue: Large Method
main has 80 lines, threshold = 70

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos leynos marked this pull request as ready for review June 3, 2026 23:53
@leynos leynos self-requested a review as a code owner June 3, 2026 23:53
sourcery-ai[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot added the Issue label Jun 4, 2026
coderabbitai[bot]

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as resolved.

leynos and others added 7 commits June 4, 2026 13:48
Run the Python coverage suite under pytest-xdist by default. A new
`pytest-workers` input (default `auto`) is appended to slipcover's pytest
invocation as `-n <workers>`; setting it to `""` restores serial behaviour.
pytest-xdist is installed alongside the existing coverage tooling so the
flag is always available. README documents the slipcover 1.0.18 omit
regression so consumers with co-located in-package tests can opt out.

Refs: #278

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recent uv versions emit coloured warnings, so the literal `warning:`
prefix match in `_clean_stderr` no longer filters the `VIRTUAL_ENV` notice
on `validate_workspaces.py` runs. Strip ANSI escape sequences from each
line before applying the prefix filters so the well-known noise is
removed regardless of terminal styling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`main` exceeded CodeScene's 70-line Large Method threshold. Lifting each
`typ.Annotated[…]` block into a private module-level alias and trimming
the docstring to a single-line summary shrinks the function while
keeping its behaviour and Typer CLI surface identical — Typer resolves
`Annotated` metadata from module-level aliases the same way as inline
declarations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeRabbit flagged three small inconsistencies in the new
`pytest-workers` documentation:

- `action.yml`'s input description omitted `logical`, even though the
  code and the README list it as a valid value.
- The README's first mention of "CPU" was not expanded; per the repo
  style guidelines uncommon acronyms must be defined on first use.
- The README's `--omit` caveat addressed the reader directly with "you",
  which the docs style guidelines prohibit.

Skipped: rejecting `-n 0` (pytest-xdist treats `0` as serial execution,
which is consistent with the issue spec's "an integer" wording), and the
suggestion to inline the Typer option aliases (those were extracted on
purpose to keep `main` under CodeScene's Large Method threshold).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a consumer project already pins an older `slipcover` in its own
`pyproject.toml`, `uv sync --inexact` installs that older version
before `_install_coverage_tooling` runs `uv pip install slipcover ...`.
Without `--upgrade` or a version constraint, uv leaves the existing
package as-is, so the new default `pytest -n auto` runs slipcover's
xdist plugin against a version that does not have it — producing
failed or merged-incorrectly coverage reports.

Constraining the install spec to `slipcover>=1.0.18` triggers uv's
version-resolution path, which upgrades an older release in place.
The integration test now asserts that the install argv carries a
version floor on slipcover so this guarantee cannot regress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README: drop the stray comma before "because" in the slipcover xdist
  omit-regression warning so the restrictive clause reads cleanly.
- generate-coverage integration test: set `INPUT_PYTEST_WORKERS=2` in the
  fake env so the subprocess actually exercises the env-reading path
  rather than silently falling back to the action.yml default.
- setup-rust: add a unit test for `_clean_stderr` that feeds a string
  containing ANSI escape sequences and the `VIRTUAL_ENV` noise prefix
  and asserts both the escapes are stripped and the noise line is
  filtered, locking in the recent ANSI-handling fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After rebasing onto main, the shared `test_support.ansi` module from
PR #281 supersedes the local ANSI helpers introduced earlier in this
branch. Pull `ANSI_ESCAPE_RE` from that module to reuse the canonical
colon-aware pattern (which already covers true-colour sequences like
`\x1b[38:2:255:0:0m`), restore the standard import ordering disturbed
by the merge, and add the missing blank line before the new
`_clean_stderr` regression test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lodyai lodyai Bot force-pushed the issue-278-adopt-pytest-xdist-in-generate-coverage branch from f5f5c0c to a49298d Compare June 4, 2026 11:51
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Jun 4, 2026

Copy link
Copy Markdown
Owner

@coderabbitai Have the following now been resolved?

Annul any requirements that violate the en-GB-oxendict spelling (-ize / -yse / -our) conventions (for example a request to replace "normalize" with "normalise" or "artefact" with "artifact"), or where the requirement unnecessarily increases cyclomatic complexity.

Please address the comments from this code review:

## Overall Comments
- The supported values for `pytest-workers` are inconsistent: the code and README allow `logical`, but `action.yml`'s description omits it—please update the action input description so it accurately reflects all accepted values.

## Individual Comments

### Comment 1
<location path=".github/actions/generate-coverage/scripts/run_python.py" line_range="300" />
<code_context>
+    lowered = value.lower()
+    if lowered in _VALID_NAMED_WORKERS:
+        return lowered
+    if value.isdigit():
+        return value
+    typer.echo(
</code_context>
<issue_to_address>
**issue (bug_risk):** Consider rejecting `0` as a worker value, since pytest-xdist expects a positive integer for `-n`.

Because this branch accepts any digit string, "0" will be treated as valid and passed through as `-n 0` to pytest-xdist, which expects a positive integer (or named value like `auto`/`logical`). To prevent unexpected runtime failures, tighten this check to require `int(value) > 0` and route `0` through the existing invalid-value error handling.
</issue_to_address>

### Comment 2
<location path=".github/actions/generate-coverage/scripts/run_python.py" line_range="467" />
<code_context>
+    return _normalise_pytest_workers(raw)
+
+
 def _resolve_inputs(
     output_path: Path | None,
     lang: str | None,
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the pytest worker handling and Typer option definitions so responsibilities stay local and the call chain has fewer threaded parameters and indirections.

You can keep all the new functionality but simplify the worker handling and option definitions to reduce indirection and keep responsibilities local.

### 1. Collapse worker resolution into a single helper and keep `_resolve_inputs` focused

Right now the flow is:

`main → _resolve_inputs → _resolve_pytest_workers → _normalise_pytest_workers → _run_coverage → coverage_cmd_for_fmt → _coverage_args`

You can keep validation + env defaulting, but localise it to `main` and stop threading `workers` through `_resolve_inputs`.

**Key ideas:**

- Let `_resolve_inputs` stay focused on core inputs (output_path/lang/fmt/github_output).
- Have a single worker helper that does both: env fallback and validation.
- Pass a simple `workers` string from `main` directly to `_run_coverage` / `_coverage_args`.

Example refactor:

```python
# keep this constant
DEFAULT_PYTEST_WORKERS = "auto"
_VALID_NAMED_WORKERS = frozenset({"auto", "logical"})


def _normalise_pytest_workers(raw: str | None) -> str:
    """Validate and normalise the pytest-workers value.

    Accepts "auto", "logical", a non-negative integer string, or an empty value
    (which disables parallelism).
    """
    if raw is None:
        return ""
    value = raw.strip()
    if not value:
        return ""
    lowered = value.lower()
    if lowered in _VALID_NAMED_WORKERS:
        return lowered
    if value.isdigit():
        return value
    typer.echo(
        f"Invalid pytest-workers value: {raw!r}. Expected an integer, "
        '"auto", "logical", or "" to disable parallelism.',
        err=True,
    )
    raise typer.Exit(2)


def _resolve_pytest_workers(pytest_workers: str | None) -> str:
    """Resolve pytest workers from CLI or INPUT_PYTEST_WORKERS env, with default."""
    raw = pytest_workers
    if raw is None:
        raw = os.getenv("INPUT_PYTEST_WORKERS", DEFAULT_PYTEST_WORKERS)
    return _normalise_pytest_workers(raw)
```

Then keep `_resolve_inputs` on its original domain:

```python
def _resolve_inputs(
    output_path: Path | None,
    lang: str | None,
    fmt: str | None,
    github_output: Path | None,
) -> tuple[Path, str, Path]:
    resolved_output_path = output_path or Path(_required_env("INPUT_OUTPUT_PATH"))
    resolved_lang = lang or _required_env("DETECTED_LANG")
    resolved_fmt = fmt or _required_env("DETECTED_FMT")
    resolved_github_output = github_output or Path(_required_env("GITHUB_OUTPUT"))
    out = _resolve_output_path(resolved_output_path, resolved_lang)
    return out, resolved_fmt, resolved_github_output
```

And keep the worker threading minimal:

```python
def _coverage_args(fmt: str, out: Path, workers: str = "") -> list[str]:
    args: list[str] = [*SLIPCOVER_ARGS]
    if fmt == "cobertura":
        args.extend(["--xml", "--out", str(out)])
    args.extend(PYTEST_ARGS)
    if workers:
        args.extend(["-n", workers])
    return args


def _run_coverage(fmt: str, out: Path, workers: str = "") -> str:
    try:
        cmd = coverage_cmd_for_fmt(fmt, out, workers)
        run_cmd(cmd, method="run_fg")
    ...
```

And in `main`:

```python
def main(
    output_path: Path | None = typer.Option(...),
    lang: str | None = typer.Option(...),
    fmt: str | None = typer.Option(...),
    github_output: Path | None = typer.Option(...),
    baseline_file: Path | None = typer.Option(...),
    pytest_workers: str | None = typer.Option(...),
) -> None:
    out, fmt, github_output = _resolve_inputs(output_path, lang, fmt, github_output)
    workers = _resolve_pytest_workers(pytest_workers)

    out.parent.mkdir(parents=True, exist_ok=True)
    percent = _run_coverage(fmt, out, workers)
    ...
```

This keeps worker logic in one place and removes the extra `workers` parameter from `_resolve_inputs`.

### 2. Inline one-off Typer option aliases

The `_OutputPathOption`, `_LangOption`, `_FmtOption`, `_GithubOutputOption`, `_BaselineFileOption`, and `_PytestWorkersOption` aliases are only used once and force readers to jump around. Inlining them back into `main` keeps the CLI definition self-contained and easier to scan.

Example simplification (preserving current help/envvar):

```python
def main(
    output_path: typ.Annotated[
        Path | None,
        typer.Option(
            help="Destination path for the coverage output file.",
        ),
    ] = None,
    lang: typ.Annotated[
        str | None,
        typer.Option(
            help='Detected project language: "rust", "python", or "mixed".',
        ),
    ] = None,
    fmt: typ.Annotated[
        str | None,
        typer.Option(
            help='Coverage format: "slipcover", "coveragepy", etc.',
        ),
    ] = None,
    github_output: typ.Annotated[
        Path | None,
        typer.Option(
            help="Path to the GitHub Actions output file.",
        ),
    ] = None,
    baseline_file: typ.Annotated[
        Path | None,
        typer.Option(
            envvar="BASELINE_PYTHON_FILE",
            help="Optional path to a previous coverage baseline file.",
        ),
    ] = None,
    pytest_workers: typ.Annotated[
        str | None,
        typer.Option(
            help=(
                'Worker count for pytest-xdist (-n). Use an integer, "auto", '
                '"logical", or "" to disable parallelism. Defaults to "auto".'
            ),
        ),
    ] = None,
) -> None:
    ...
```

If you want to reduce repetition later, you can introduce aliases only for truly shared patterns (e.g. a common `Path` with the same `envvar` and `help` across multiple commands), but for a single entry point the inline style keeps complexity lower without changing behavior.
</issue_to_address>

### Comment 3
<location path=".github/actions/generate-coverage/README.md" line_range="247" />
<code_context>
+Python coverage runs through `pytest-xdist` by default (`pytest-workers: auto`),
+and slipcover 1.0.18+ merges the per-worker coverage transparently. Set
+`pytest-workers` to an integer for a fixed worker count, to `logical` to use
+logical CPU count, or to `""` to keep the historical serial behaviour.
+
+> [!WARNING]
</code_context>
<issue_to_address>
**issue (review_instructions):** The acronym "CPU" is used without being defined on first use, which violates the requirement to define uncommon acronyms.

Consider expanding this to "central processing unit (CPU)" on first use, for example: "logical central processing unit (CPU) count".

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 4
<location path=".github/actions/generate-coverage/README.md" line_range="259" />
<code_context>
+> reported at 0% coverage. The production-code coverage values themselves are
+> unchanged; only the omit list is dropped on the worker side. Projects that
+> keep tests **outside** the source package (e.g. `tests/` next to
+> `src/mypkg/`) are unaffected. If you rely on `--omit` to exclude in-package
+> tests, either move the tests out of the package or set `pytest-workers: ""`
+> until the upstream plugin is fixed.
</code_context>
<issue_to_address>
**issue (review_instructions):** This sentence uses the 2nd person pronoun "you", which the style guidelines prohibit.

To keep the tone impersonal, consider rephrasing along the lines of: "If a project relies on `--omit` to exclude in-package tests, it should either move the tests out of the package or set `pytest-workers: ""` until the upstream plugin is fixed."

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Avoid 2nd person or 1st person pronouns ("I", "you", "we").

</details>
</issue_to_address>

@leynos

leynos commented Jun 4, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos

leynos commented Jun 4, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos

leynos commented Jun 5, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos

leynos commented Jun 5, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos

leynos commented Jun 5, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

1 similar comment
@leynos

leynos commented Jun 5, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos

leynos commented Jun 5, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@lodyai

lodyai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

CodeRabbit flagged two genuine gaps:

- `_parse_pytest_workers` validated digit strings via `str.isdigit()`,
  which returns True for characters like the superscript "²" that
  `int()` rejects. Hypothesis-driven property testing made the divergence
  obvious. Switch the guard to `str.isdecimal()`, which matches `int()`'s
  acceptance set and keeps the canonical "Invalid pytest-workers value"
  error message for unusual inputs. Four new property tests pin the
  contract: named values normalise to lowercase under arbitrary
  case-mixing and surrounding whitespace; positive integer strings
  round-trip with whitespace padding; whitespace-only strings collapse
  to ""; and zero/negative integers raise ValueError.

- The developer documentation was out of date: the install command in
  `docs/developers-guide.md` still listed `slipcover pytest coverage`
  (no version pin, no `pytest-xdist`) and the Public API row for `main`
  omitted the new `pytest_workers` parameter. The design doc had no
  entry recording the pytest-xdist adoption decision. Update both:
  refresh the install command and the API row, and add a 2026-06-04
  design-decision bullet covering the new input, the slipcover floor,
  the `"0"` rejection rationale, and the upstream `--omit` regression.

Skipped from the review:
- Module-level docstring expansion: existing docstrings are concise and
  accurate; workflow context belongs in README/AGENTS.md.
- Snapshot tests for the new "Pytest workers: ..." log lines: the two
  existing `test_main_*` tests already pin the exact wording with
  explicit string assertions, which is more diff-friendly than a
  snapshot for single one-line messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot added Roadmap and removed Issue labels Jun 5, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/actions/generate-coverage/tests/test_scripts.py:
- Around line 2425-2445: The test signature for
test_parse_pytest_workers_normalises_named_values has too many parameters;
collapse leading and trailing into a single parameter (e.g. padding or pad_pair)
produced by st.tuples(_WHITESPACE_ST, _WHITESPACE_ST) so the signature becomes
(run_python_module, name, upper_mask, pad_pair) and then unpack pad_pair into
leading and trailing inside the test before constructing raw; keep the rest of
the logic and assertion unchanged and update any references to leading/trailing
accordingly in the body of the test and preserve the decorator/given settings to
maintain coverage.

In `@docs/developers-guide.md`:
- Line 61: Replace the en-GB non-oxendict spelling "parallelises" with the
oxendict "-ize" form "parallelizes" in the sentence mentioning `workers` and
`pytest-xdist` so it reads that pytest-xdist "parallelizes" when `workers` is
non-empty; edit the text near the `workers` / `pytest-xdist` reference in
developers-guide.md accordingly.
🪄 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: 323e687e-b828-4f71-864d-e616e1c77b4d

📥 Commits

Reviewing files that changed from the base of the PR and between db23daa and 5b5f62b.

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

Comment thread .github/actions/generate-coverage/tests/test_scripts.py
Comment thread docs/developers-guide.md Outdated
- ``test_parse_pytest_workers_normalises_named_values`` previously took
  five parameters (plus the module fixture), exceeding the four-argument
  threshold. Collapse the ``leading`` and ``trailing`` whitespace
  parameters into a single ``pad_pair`` tuple generated by
  ``st.tuples(_WHITESPACE_ST, _WHITESPACE_ST)``, then unpack it inside
  the test body. The decorator settings, generation domain, and
  assertion are unchanged.
- ``docs/developers-guide.md`` used the en-GB ``-ise`` form
  ``parallelises`` for pytest-xdist's behaviour. Switch to the Oxford
  spelling ``parallelizes`` to match the repository's en-GB-oxendict
  convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@leynos

leynos commented Jun 5, 2026

Copy link
Copy Markdown
Owner

@coderabbitai pause

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews paused.

`_resolve_pytest_workers` used to delegate validation to
`_normalise_pytest_workers`, which carries Typer side-effects
(`typer.echo` + `typer.Exit(2)`). That left a CLI boundary in a helper
the domain code calls during resolution, violating the rule that the
boundary should live exclusively in `main()`.

Switch the resolver to call the already-pure `_parse_pytest_workers`
so it now raises `ValueError` for invalid input. `main()` catches
that `ValueError` at the single CLI boundary and converts it to the
existing stderr message plus `typer.Exit(2)`, preserving observable
behaviour at the action's edge.

None of the existing `_resolve_pytest_workers` tests asserted
Typer-style exits, so they continue to pass unchanged. Two new
regression tests pin the refactored contract:
`test_resolve_pytest_workers_raises_value_error_on_invalid_env`
verifies the resolver itself raises `ValueError` (no Typer touched);
`test_main_translates_invalid_workers_into_typer_exit` verifies that
`main` catches it, prints the canonical message to stderr, and exits
with code 2 — and that `run_cmd` is never invoked when validation
fails.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== docs/developers-guide.md: parallelise/paralleliz ==="
grep -n "parallelis\|paralleliz" docs/developers-guide.md

echo ""
echo "=== test_parse_pytest_workers_normalises_named_values signature ==="
grep -n -A 12 "def test_parse_pytest_workers_normalises_named_values" .github/actions/generate-coverage/tests/test_scripts.py

echo ""
echo "=== _resolve_pytest_workers current implementation ==="
grep -n -A 20 "def _resolve_pytest_workers" .github/actions/generate-coverage/scripts/run_python.py

echo ""
echo "=== _normalise_pytest_workers current implementation ==="
grep -n -A 20 "def _normalise_pytest_workers" .github/actions/generate-coverage/scripts/run_python.py

echo ""
echo "=== _parse_pytest_workers current implementation ==="
grep -n -A 20 "def _parse_pytest_workers" .github/actions/generate-coverage/scripts/run_python.py

echo ""
echo "=== main() workers resolution ==="
grep -n -A 55 "^def main" .github/actions/generate-coverage/scripts/run_python.py | head -65

echo ""
echo "=== Open GitHub issues related to slipcover xdist concurrency ==="
gh issue list --repo leynos/shared-actions --state open --search "slipcover xdist concurrency" 2>/dev/null || echo "no matching issues"

gh issue list --repo leynos/shared-actions --state open --limit 20 2>/dev/null

Length of output: 8638


🏁 Script executed:

#!/bin/bash
gh issue create \
  --repo leynos/shared-actions \
  --title "generate-coverage: document and test slipcover 1.0.18 xdist per-worker coverage merging" \
  --body "## Background

PR `#279` introduces pytest-xdist parallelism to the \`generate-coverage\` composite action. Per-worker coverage data is merged by slipcover's built-in xdist plugin, which requires \`slipcover>=1.0.18\`. The concurrency model of that merging is not currently documented or tested within this repository.

## Work required

1. **Documentation** — add a section to \`docs/generate-coverage-design.md\` (or a dedicated ADR) explaining how slipcover 1.0.18's xdist plugin merges per-worker \`.coverage\` files, including the lifecycle (worker startup, mid-run accumulation, teardown merge), the behaviour under partial worker failure, and the interaction with \`--omit\`.

2. **Integration tests** — add at least one integration test that actually executes pytest-xdist (with \`-n 2\` or \`-n auto\`) against a minimal Python project fixture and asserts that the merged coverage output is correct (i.e. coverage from all workers is present in the final report).

3. **Failure-mode coverage** — clarify and, where feasible, test the behaviour when a worker process crashes mid-run: does slipcover emit a partial result, a zero-coverage result, or an error exit?

## References

- PR `#279` (adopts pytest-xdist): https://github.com/leynos/shared-actions/pull/279
- Review comment raising this concern: https://github.com/leynos/shared-actions/pull/279

Raised by `@leynos`."

Length of output: 1650


🏁 Script executed:

#!/bin/bash
gh issue list --repo leynos/shared-actions --state open --limit 5

Length of output: 641



@leynos, all checks are now resolved or tracked.

Check Status
Testing / Unit Architecture / Security / Developer Docs — parallelizes spelling ✅ Resolved
Testing / Unit Architecture / Architectural Complexity — test_parse_pytest_workers_normalises_named_values ≤ 4 params ✅ Resolved
Domain Architecture — _resolve_pytest_workers decoupled from typer.Exit ✅ Resolved
Concurrency And State ✅ Tracked — issue #283 opened

Issue #283 ("generate-coverage: document and test slipcover 1.0.18 xdist per-worker coverage merging") covers the three required actions: document the merging lifecycle in the design doc, add an integration test that runs with -n 2 and asserts merged output is correct, and clarify failure-mode behaviour under worker crashes.

@leynos leynos merged commit 455d9ed into main Jun 5, 2026
26 checks passed
@leynos leynos deleted the issue-278-adopt-pytest-xdist-in-generate-coverage branch June 5, 2026 18:05
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