Skip to content

Propagate LLVM env into Rust coverage runs#253

Merged
leynos merged 28 commits intomainfrom
use-env-vars-for-codegen
Apr 17, 2026
Merged

Propagate LLVM env into Rust coverage runs#253
leynos merged 28 commits intomainfrom
use-env-vars-for-codegen

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Apr 16, 2026

Replace the cargo llvm-cov --config override path with CARGO_PROFILE_*_CODEGEN_BACKEND=llvm overrides when the target repo configures Cranelift. This lets nested cargo processes spawned by cargo-llvm-cov inherit the LLVM setting that coverage builds require.

Update the generate-coverage regression tests to assert env propagation for Cranelift-configured repos, keep non-Cranelift runs unchanged, and verify _run_cargo forwards explicit environment overrides.

Summary by Sourcery

Propagate LLVM codegen configuration for Rust coverage runs by switching from cargo config flags to environment-based overrides and ensuring nested cargo processes inherit the required settings.

Enhancements:

  • Expose a get_cargo_coverage_env helper that returns LLVM codegen env overrides for Cranelift-configured projects and use it when invoking cargo-llvm-cov and cucumber-rs coverage runs.
  • Extend the internal _run_cargo helper to accept optional environment overrides and pass them through to the spawned cargo process while preserving the existing environment.

Tests:

  • Update Rust coverage script tests to validate that LLVM env overrides are applied only for Cranelift-configured repositories and are absent for LLVM-based ones.
  • Add tests to verify that _run_cargo correctly merges explicit environment overrides with inherited environment variables and that cargo calls no longer rely on --config codegen-backend flags.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 16, 2026

Reviewer's Guide

Propagates LLVM codegen configuration for coverage runs via environment overrides instead of cargo --config flags, and extends tests to validate env propagation, coverage env computation, and _run_cargo behavior.

Sequence diagram for Rust coverage env propagation via _run_cargo

sequenceDiagram
    actor GitHubActions
    participant run_rust_py as run_rust.py
    participant get_cargo_coverage_env
    participant _run_cargo
    participant cargo

    GitHubActions->>run_rust_py: main(manifest_path, fmt, use_nextest, ...)
    run_rust_py->>get_cargo_coverage_env: get_cargo_coverage_env(manifest_path)
    get_cargo_coverage_env-->>run_rust_py: cargo_env
    run_rust_py->>_run_cargo: _run_cargo(args, env_overrides=cargo_env)
    alt env_overrides is not None
        _run_cargo->>_run_cargo: merge os_environ with env_overrides
    else
        _run_cargo->>_run_cargo: use os_environ unchanged
    end
    _run_cargo->>cargo: invoke cargo llvm-cov with env
    cargo-->>_run_cargo: stdout
    _run_cargo-->>run_rust_py: stdout

    opt with_cucumber_rs
        run_rust_py->>get_cargo_coverage_env: get_cargo_coverage_env(manifest_path)
        get_cargo_coverage_env-->>run_rust_py: cargo_env
        run_rust_py->>_run_cargo: _run_cargo(cucumber_args, env_overrides=cargo_env)
        _run_cargo->>cargo: invoke cargo test with env
        cargo-->>_run_cargo: stdout
        _run_cargo-->>run_rust_py: stdout
    end
Loading

Flow diagram for cargo coverage env computation and use

flowchart TD
    A[Start get_cargo_coverage_env] --> B[Call _uses_cranelift_backend with manifest_path]
    B -->|Project uses Cranelift| C[Return env map:
CARGO_PROFILE_DEV_CODEGEN_BACKEND=llvm
CARGO_PROFILE_TEST_CODEGEN_BACKEND=llvm]
    B -->|Project does not use Cranelift| D[Return empty env map]

    C --> E[main receives nonempty cargo_env]
    D --> E

    E --> F[Call _run_cargo with args and env_overrides=cargo_env]

    F --> G{env_overrides is None?}
    G -->|Yes| H[Set env to None
use process default environment]
    G -->|No| I[Create env by copying os.environ
and updating with env_overrides]

    H --> J[Spawn cargo subprocess with env]
    I --> J

    J --> K[Stream cargo output
collect stdout]
    K --> L[Return stdout to caller]
Loading

File-Level Changes

Change Details Files
Switch from cargo --config overrides to environment-based LLVM codegen overrides for Cranelift projects in coverage scripts.
  • Remove hard-coded LLVM codegen --config argument list used for overriding Cranelift backends.
  • Introduce get_cargo_coverage_env to derive CARGO_PROFILE_*_CODEGEN_BACKEND overrides when the project configures Cranelift.
  • Update get_cargo_coverage_cmd to always start with llvm-cov/nextest without injecting --config flags and rely on environment instead.
  • Pass coverage env overrides into run_cucumber_rs_coverage and main via get_cargo_coverage_env.
.github/actions/generate-coverage/scripts/run_rust.py
Enhance cargo invocation helper to accept and forward environment overrides to the spawned process.
  • Extend _run_cargo signature to accept an optional env_overrides mapping.
  • Build a merged environment combining os.environ with env_overrides when provided.
  • Pass the merged env into cargo[...] .popen so nested cargo processes inherit coverage-related settings.
  • Add a regression test to assert that _run_cargo merges inherited and override environment variables and that coverage output is preserved.
.github/actions/generate-coverage/scripts/run_rust.py
.github/actions/generate-coverage/tests/test_scripts.py
Expand tests to validate LLVM env propagation semantics for Cranelift vs non-Cranelift projects and to simplify cargo call inspection.
  • Augment FakeCargo to record popen keyword arguments for later inspection in tests.
  • Refactor helper logic by introducing _run_rust_coverage_call to return the recorded cargo Call object alongside paths.
  • Update existing happy-path test_run_rust_success to assert absence of coverage env overrides when Cranelift is not used.
  • Rewrite Cranelift-specific coverage test to assert env-based overrides instead of --config argument prefixes.
  • Add tests for get_cargo_coverage_env covering Cranelift vs LLVM profile configurations.
  • Update _run_rust_main_variant helper and its fake_run_cargo to record and expose env_overrides in addition to args, maintaining type safety casts.
.github/actions/generate-coverage/tests/test_scripts.py

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cf937a5e-9313-442c-b2c7-a151d552c047

📥 Commits

Reviewing files that changed from the base of the PR and between f811709 and 3f4d644.

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

Walkthrough

Detect Cranelift from .cargo/config* or Cargo.toml profile sections; derive environment overrides setting CARGO_PROFILE_DEV_CODEGEN_BACKEND=llvm and CARGO_PROFILE_TEST_CODEGEN_BACKEND=llvm; remove inherited codegen-related env keys, merge overrides into a copied env, and pass the merged env to spawned cargo llvm-cov and optional cucumber-rs runs. Add a reusable _cargo_runner.py to centralise spawning, streaming and timeout behaviour; add _cranelift.py to centralise detection and override generation.

Changes

Cohort / File(s) Summary
Runner
/.github/actions/generate-coverage/scripts/_cargo_runner.py
Add reusable cargo runner: build environment from os.environ, unset keys in env_unsets, merge env_overrides, spawn cargo with captured stdout/stderr, stream output (Windows: threads; POSIX: selectors), enforce absolute wait timeout, kill on timeout, and raise typer.Exit on failure.
Cranelift detector
/.github/actions/generate-coverage/scripts/_cranelift.py
Add Cranelift detection and get_cargo_coverage_env(manifest_path) that scans upward for .cargo/config* and inspects Cargo.toml profile sections; return {} or LLVM overrides and export _CARGO_COVERAGE_ENV_UNSETS.
Coverage script
/.github/actions/generate-coverage/scripts/run_rust.py
Remove in-file subprocess/env plumbing; import _cargo_runner and _cranelift; compute cargo_env = get_cargo_coverage_env(manifest_path) and call _run_cargo(..., env_overrides=cargo_env, env_unsets=_CARGO_COVERAGE_ENV_UNSETS); update run_cucumber_rs_coverage signature to accept cargo_env and drop extra_env.
Unit tests
/.github/actions/generate-coverage/tests/test_scripts.py
Refactor tests: add FakeCargo.with_env(...), record last_popen_kwargs, implement FakeProc.poll(), adapt helpers to new _run_cargo contract, assert env override/unset merging, add pump-loop timeout/kill tests, and create synthetic .cargo/config.toml scenarios for Cranelift detection.
Windows pump tests
/.github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py
Load _cargo_runner module for testing; update _pump_cargo_output_windows protocol to accept a _CargoProcCtx-style ctx; add poll() to dummy process stub and adjust test calls.
Docs & README
/.github/actions/generate-coverage/README.md, docs/generate-coverage-design.md
Remove prior claim of exporting profile envs; add Mermaid sequence diagram and docs for get_cargo_coverage_env and _run_cargo semantics, detail Cranelift detection heuristics and limitations, update usage examples, and link to design notes.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(200,200,255,0.5)
  participant Script as "run_rust.py"
  end
  rect rgba(200,255,200,0.5)
  participant Detector as "get_cargo_coverage_env(manifest)"
  end
  rect rgba(255,230,200,0.5)
  participant Env as "os.environ"
  end
  rect rgba(255,200,200,0.5)
  participant Cargo as "cargo (popen)"
  end

  Script->>Detector: request env_overrides(manifest_path)
  Detector-->>Script: return {} or {CARGO_PROFILE_DEV_CODEGEN_BACKEND: "llvm", CARGO_PROFILE_TEST_CODEGEN_BACKEND: "llvm"}
  Script->>Env: copy current environment
  Script->>Script: remove keys in env_unsets, then apply env_overrides
  Script->>Cargo: popen(args=["llvm-cov", ...], env=merged_env)
  Cargo-->>Script: stream output / exit code
  alt cucumber follow-up
    Script->>Detector: re-derive env_overrides for cucumber manifest
    Script->>Cargo: popen(args=cucumber_args, env=merged_env)
    Cargo-->>Script: stream output / exit code
  end
Loading

Poem

🔎 Detect Cranelift, flip the backend to LLVM,
Unset the stray vars, stitch an honest env,
Spawn llvm-cov with the crafted shell,
Rerun cucumber with the same stitched spell,
CI collects coverage, badges swell ✨

🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Developer Documentation ⚠️ Warning The pull request lacks adequate developer documentation for new internal APIs and architectural boundaries in the generate-coverage system. Add comprehensive module docstrings, API documentation, and design documentation covering _cargo_runner.py and _cranelift.py internal APIs and their architectural roles.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarises the main change: propagating LLVM environment variables into Rust coverage runs, which aligns precisely with the changeset's focus on switching from cargo config flags to environment-based overrides.
Description check ✅ Passed The description provides substantive detail about the changes: it explains the rationale (letting nested cargo processes inherit LLVM settings), the mechanism (environment variables instead of config flags), and the testing scope, all directly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 90.20% which is sufficient. The required threshold is 80.00%.
Testing ✅ Passed PR introduces two substantial new modules (_cargo_runner.py with 434 lines and _cranelift.py with 155 lines) with comprehensive test coverage including 11 dedicated tests for _run_cargo and three Cranelift detection tests.
User-Facing Documentation ✅ Passed User-facing documentation thoroughly updated with dedicated Cranelift section, examples, and known limitations documented.
Module-Level Documentation ✅ Passed All five modules carry comprehensive module-level docstrings clearly explaining their purpose, utilities, and function.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 use-env-vars-for-codegen

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

codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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.

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 Roadmap label Apr 16, 2026
coderabbitai[bot]

This comment was marked as resolved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 16, 2026

@coderabbitai Have the following failed checks now been resolved?

If further work is required, please provide an AI agent prompt for the remaining work to be done to address these failures.

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Testing ⚠️ Warning PR introduces new functionality with basic unit tests covering straightforward cases, but critical edge cases lack test coverage: workspace member scenario detection and inherited environment variable cleanup. Add unit tests for workspace member scenarios, inherited CARGO_PROFILE_DEV_CODEGEN_BACKEND variable stripping, and remove invalid CARGO_PROFILE_TEST_CODEGEN_BACKEND variable from implementation.
User-Facing Documentation ⚠️ Warning README lacks user-facing documentation of automatic Cranelift codegen backend detection and handling; technical diagram present but user guidance absent. Add dedicated user-facing section documenting Cranelift support, automatic detection behaviour, LLVM overrides, example workflow, and known limitations regarding workspace members and environment variables.
Developer Documentation ⚠️ Warning Critical internal API change to _run_cargo() lacks proper inline documentation of the new env_overrides keyword-only parameter and its semantics. Update the _run_cargo docstring to fully document env_overrides parameter semantics and merging behaviour. Add design document section explaining workspace member inheritance constraints.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 16, 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_rust.py

Comment on lines +131 to +152

def _manifest_uses_cranelift_backend(manifest_path: Path) -> bool:
    """Return ``True`` when ``manifest_path`` configures Cranelift in profiles."""
    try:
        content = manifest_path.read_text(encoding="utf-8")
    except (OSError, UnicodeDecodeError):
        return False

    in_profile_section = False
    for line in content.splitlines():
        stripped = line.strip()
        if not stripped or stripped.startswith("#"):
            continue
        section_match = re.match(r"^\[(?P<section>[^\]]+)\]$", stripped)
        if section_match:
            section = section_match.group("section").strip()
            in_profile_section = section == "profile" or section.startswith("profile.")
            continue
        if not in_profile_section:
            continue
        if re.match(r'^[ \t]*codegen-backend\s*=\s*["\']cranelift["\']', line):
            return True
    return False

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

@coderabbitai

This comment was marked as resolved.

@coderabbitai

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[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[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 17, 2026

@coderabbitai Have the following now been resolved?

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

228-261: ⚠️ Potential issue | 🟡 Minor
Exercise the unset-only path in this test.
Seed CARGO_PROFILE_DEV_CODEGEN_BACKEND=cranelift here instead of deleting it. The current setup bypasses the behaviour this PR changed, so a regression where env_unsets stops scrubbing inherited values for non-Cranelift projects would still pass.

Patch sketch
-    monkeypatch.delenv("CARGO_PROFILE_DEV_CODEGEN_BACKEND", raising=False)
-    monkeypatch.delenv("CARGO_PROFILE_TEST_CODEGEN_BACKEND", raising=False)
+    monkeypatch.setenv("CARGO_PROFILE_DEV_CODEGEN_BACKEND", "cranelift")
+    monkeypatch.delenv("CARGO_PROFILE_TEST_CODEGEN_BACKEND", raising=False)
@@
     assert cargo_args == expected_args
     assert "CARGO_PROFILE_DEV_CODEGEN_BACKEND" not in cargo_call.env
🤖 Prompt for AI Agents

.github/actions/generate-coverage/README.md (1)

107-178: ⚠️ Potential issue | 🟠 Major
Repair all uses: references in the code examples—local actions cannot use @v1 syntax.
Local action paths like ./.github/actions/generate-coverage do not support @ref suffixes. Version tags (e.g., @v1) apply only to remote repository references ({owner}/{repo}@v1). Every example in this section will fail when executed because the @v1 suffix is invalid on local paths.
Correct the examples by removing the @v1 suffix:

Corrected examples
- uses: ./.github/actions/generate-coverage
  with:
    output-path: coverage.xml
    format: cobertura

Alternatively, if the action is published to a separate repository, reference it as {owner}/{repo}@v1 (but this requires a dedicated repository, not a local path).

🤖 Prompt for AI Agents

@coderabbitai

This comment was marked as resolved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 17, 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.

Complex Method

.github/actions/generate-coverage/scripts/run_rust.py: _run_cargo

What lead to degradation?

_run_cargo increases in cyclomatic complexity from 11 to 13, threshold = 9

Why does this problem occur?

A Complex Method has a high cyclomatic complexity. The recommended threshold for the Python language is a cyclomatic complexity lower than 9.

How to fix it?

There are many reasons for Complex Method. Sometimes, another design approach is beneficial such as a) modeling state using an explicit state machine rather than conditionals, or b) using table lookup rather than long chains of logic. In other scenarios, the function can be split using EXTRACT FUNCTION. Just make sure you extract natural and cohesive functions. Complex Methods can also be addressed by identifying complex conditional expressions and then using the DECOMPOSE CONDITIONAL refactoring.

Helpful refactoring examples

To get a general understanding of what this code health issue looks like - and how it might be addressed - we have prepared some diffs for illustrative purposes.

SAMPLE

# complex_method.js
 function postItem(item) {
   if (!item.id) {
-    if (item.x != null && item.y != null) {
-      post(item);
-    } else {
-      throw Error("Item must have x and y");
-    }
+    // extract a separate function for creating new item
+    postNew(item);
   } else {
-    if (item.x < 10 && item.y > 25) {
-      put(item);
-    } else {
-      throw Error("Item must have an x and y value between 10 and 25");
-    }
+    // and one for updating existing items
+    updateItem(item);
   }
 }
+
+function postNew(item) {
+  validateNew(item);
+  post(item);
+}
+
+function updateItem(item) {
+  validateUpdate(item);
+  put(item);
+}
+

@coderabbitai

This comment was marked as resolved.

@leynos

This comment was marked as resolved.

@coderabbitai

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

This comment was marked as resolved.

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/tests/test_run_rust_windows.py (1)

103-109: ⚠️ Potential issue | 🟠 Major

Pass a real future deadline into the pump helper.

deadline=1.0 is already in the past on any real clock, so this test only passes when both reader threads drain before _poll_pump_loop_iteration() performs its first timeout check. Derive the deadline from the helper’s clock, or stub that clock explicitly, so the test stops depending on thread scheduling.

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

In @.github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py
around lines 103 - 109, Test passes a past deadline (deadline=1.0) so make the
deadline real by deriving it from a monotonic clock and pass that into
run_rust_typed._pump_cargo_output_windows (or stub the helper clock); e.g.,
compute deadline = time.monotonic() + 1.0 (import time in the test) and call
_pump_cargo_output_windows(dummy_proc, stdout_stream, stderr_stream,
deadline=deadline, wait_timeout=1.0) so _poll_pump_loop_iteration() sees a
future deadline rather than an expired one.
♻️ Duplicate comments (1)
.github/actions/generate-coverage/scripts/_cargo_runner.py (1)

71-76: ⚠️ Potential issue | 🟠 Major

Base the timeout budget on time.monotonic().

Carry the deadline on time.monotonic() end-to-end. time.time() follows the wall clock, so an NTP or manual clock adjustment can make _pump_cargo_output() / _wait_for_cargo() kill cargo early or let it run past RUN_RUST_CARGO_WAIT_TIMEOUT.

Patch sketch
-    remaining = max(0.0, deadline - time.time())
+    remaining = max(0.0, deadline - time.monotonic())
...
-            if time.time() >= deadline:
+            if time.monotonic() >= deadline:
...
-            timeout = max(0.0, deadline - time.time())
+            timeout = max(0.0, deadline - time.monotonic())
...
-        return proc.wait(timeout=max(0.0, deadline - time.time()))
+        return proc.wait(timeout=max(0.0, deadline - time.monotonic()))
...
-    deadline = time.time() + wait_timeout
+    deadline = time.monotonic() + wait_timeout
#!/bin/bash
sed -n '57,77p;177,224p;283,379p' .github/actions/generate-coverage/scripts/_cargo_runner.py
rg -n "time\.(time|monotonic)\(" .github/actions/generate-coverage/scripts/_cargo_runner.py

Also applies to: 212-216, 291-294, 367-379

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

In @.github/actions/generate-coverage/scripts/_cargo_runner.py around lines 71 -
76, The timeout logic must use a monotonic clock end-to-end: change construction
and checks of the deadline and remaining time in _pump_cargo_output() and
_wait_for_cargo() to use time.monotonic() (compute deadline = time.monotonic() +
wait_timeout and later compute remaining = max(0.0, deadline - time.monotonic())
instead of time.time()), and replace any time.time() usage that influences
join_timeout/ _raise_cargo_timeout() decisions (the variables remaining,
join_timeout, deadline) so the proc/threads join and the call to
_raise_cargo_timeout(proc, wait_timeout=wait_timeout) are based on monotonic
time rather than the wall clock.
🤖 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/tests/test_run_rust_windows.py:
- Around line 103-109: Test passes a past deadline (deadline=1.0) so make the
deadline real by deriving it from a monotonic clock and pass that into
run_rust_typed._pump_cargo_output_windows (or stub the helper clock); e.g.,
compute deadline = time.monotonic() + 1.0 (import time in the test) and call
_pump_cargo_output_windows(dummy_proc, stdout_stream, stderr_stream,
deadline=deadline, wait_timeout=1.0) so _poll_pump_loop_iteration() sees a
future deadline rather than an expired one.

---

Duplicate comments:
In @.github/actions/generate-coverage/scripts/_cargo_runner.py:
- Around line 71-76: The timeout logic must use a monotonic clock end-to-end:
change construction and checks of the deadline and remaining time in
_pump_cargo_output() and _wait_for_cargo() to use time.monotonic() (compute
deadline = time.monotonic() + wait_timeout and later compute remaining =
max(0.0, deadline - time.monotonic()) instead of time.time()), and replace any
time.time() usage that influences join_timeout/ _raise_cargo_timeout() decisions
(the variables remaining, join_timeout, deadline) so the proc/threads join and
the call to _raise_cargo_timeout(proc, wait_timeout=wait_timeout) are based on
monotonic time rather than the wall clock.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2f48db7c-3202-4136-9a50-54cf5a4561af

📥 Commits

Reviewing files that changed from the base of the PR and between 553c267 and 4aed49e.

📒 Files selected for processing (3)
  • .github/actions/generate-coverage/scripts/_cargo_runner.py
  • .github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py
  • .github/actions/generate-coverage/tests/test_scripts.py

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

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

🤖 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/_cargo_runner.py:
- Around line 266-292: The timeout uses time.time(), which can be skewed; update
the deadline arithmetic to use time.monotonic() instead: in _wait_for_cargo (and
any other functions in this module that compute deadlines or time remaining)
replace time.time() with time.monotonic() so proc.wait(timeout=max(0.0, deadline
- time.monotonic())) uses a monotonic clock; ensure any callers that construct
the deadline also use time.monotonic() (so deadline and comparisons are computed
from the same monotonic reference) and keep existing behavior of calling
_raise_cargo_timeout(proc, wait_timeout=wait_timeout).

In @.github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py:
- Around line 48-54: The ctx parameter on _pump_cargo_output_windows is typed
too loosely as object; tighten it by declaring/using a structural protocol or
the existing _CargoProcCtx type so callers and IDEs get proper attributes.
Update the test module to either define a small Protocol with the exact
attributes/methods used by _pump_cargo_output_windows (e.g., attributes
referenced inside the function) and annotate ctx: ThatProtocol, or import and
annotate ctx: _CargoProcCtx if that internal type is available to tests; keep
the function name _pump_cargo_output_windows and only change the ctx annotation
and any imports accordingly.
- Around line 65-66: Remove the unused poll() method from the _DummyProc test
helper: update the class _DummyProc in test_run_rust_windows.py to only
implement the protocol methods kill() and wait(), since
_pump_cargo_output_windows and the test suite never call poll() and the
_SupportsKillWait protocol defines only kill() and wait(); this prevents a
misleading API surface in the test helper.
🪄 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

Run ID: dfa83806-3074-42f9-8713-074329c1766e

📥 Commits

Reviewing files that changed from the base of the PR and between 4aed49e and 112fbc7.

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

Comment thread .github/actions/generate-coverage/scripts/_cargo_runner.py
Comment on lines 48 to 54
def _pump_cargo_output_windows(
self,
proc: _SupportsKillWait,
stdout_stream: typ.IO[str],
stderr_stream: typ.IO[str],
ctx: object,
) -> list[str]:
"""Mirror of the helper under test."""
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Tighten the ctx type annotation in the protocol.

ctx: object is permissive. For better documentation and IDE support, declare a structural protocol or import _CargoProcCtx and use it directly (even in a test-only protocol).

♻️ Proposed change
+class _CargoProcCtxProtocol(typ.Protocol):
+    proc: _SupportsKillWait
+    deadline: float
+    wait_timeout: float
+

 class _RunRustModule(typ.Protocol):
     """Subset of the run_rust module used by the tests."""

     sys: typ.Any

     def _pump_cargo_output_windows(
         self,
         stdout_stream: typ.IO[str],
         stderr_stream: typ.IO[str],
-        ctx: object,
+        ctx: _CargoProcCtxProtocol,
     ) -> list[str]:
         """Mirror of the helper under test."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py
around lines 48 - 54, The ctx parameter on _pump_cargo_output_windows is typed
too loosely as object; tighten it by declaring/using a structural protocol or
the existing _CargoProcCtx type so callers and IDEs get proper attributes.
Update the test module to either define a small Protocol with the exact
attributes/methods used by _pump_cargo_output_windows (e.g., attributes
referenced inside the function) and annotate ctx: ThatProtocol, or import and
annotate ctx: _CargoProcCtx if that internal type is available to tests; keep
the function name _pump_cargo_output_windows and only change the ctx annotation
and any imports accordingly.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai Has this now been resolved in the latest commit?

Use codegraph analysis to determine your answer.

If this comment is now resolved, please mark it as such using the API. Otherwise, please provide an AI agent prompt for the remaining work to be done to address this 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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat .github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py

Length of output: 3388


@leynos, the comment is not yet resolved. The current HEAD of test_run_rust_windows.py still has ctx: object on line 52 of _RunRustModule._pump_cargo_output_windows.

Here is the agent prompt to address it:

In `.github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py`,
tighten the `ctx` parameter annotation on `_RunRustModule._pump_cargo_output_windows`
from `object` to a structural Protocol that mirrors `_CargoProcCtx`.

Steps:
1. Above the `_RunRustModule` class, define a new Protocol class:

```python
class _CargoProcCtxProtocol(typ.Protocol):
    """Structural protocol mirroring _CargoProcCtx for static typing."""

    proc: _SupportsKillWait
    deadline: float
    wait_timeout: float
```

2. In `_RunRustModule._pump_cargo_output_windows`, change the `ctx` parameter
   annotation from `object` to `_CargoProcCtxProtocol`:

```python
    def _pump_cargo_output_windows(
        self,
        stdout_stream: typ.IO[str],
        stderr_stream: typ.IO[str],
        ctx: _CargoProcCtxProtocol,
    ) -> list[str]:
        """Mirror of the helper under test."""
```

3. Do not change any other logic, imports, or tests — `_DummyProc` already
   satisfies `_SupportsKillWait`, so `run_rust._CargoProcCtx(proc=dummy_proc, ...)`
   will continue to satisfy the new protocol at runtime without further changes.

Verify: confirm `_SupportsKillWait` is already declared before `_CargoProcCtxProtocol`
in the file so the forward reference is valid.

Comment on lines +65 to +66
def poll(self) -> None:
return None
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for poll() calls in _cargo_runner.py to confirm usage
rg -n '\.poll\(' .github/actions/generate-coverage/scripts/_cargo_runner.py

Repository: leynos/shared-actions

Length of output: 47


🏁 Script executed:

rg -n 'poll' .github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py

Repository: leynos/shared-actions

Length of output: 95


🏁 Script executed:

rg -n '\.poll\(' .github/actions/generate-coverage/scripts/tests/

Repository: leynos/shared-actions

Length of output: 47


🏁 Script executed:

cat -n .github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py | head -100

Repository: leynos/shared-actions

Length of output: 3683


🏁 Script executed:

sed -n '48,120p' .github/actions/generate-coverage/scripts/_cargo_runner.py

Repository: leynos/shared-actions

Length of output: 2457


🏁 Script executed:

sed -n '120,180p' .github/actions/generate-coverage/scripts/_cargo_runner.py

Repository: leynos/shared-actions

Length of output: 1895


Remove the poll() method from _DummyProc.

The poll() method is never invoked by _pump_cargo_output_windows or anywhere else in the test suite. The _SupportsKillWait protocol explicitly defines only kill() and wait(), not poll(). Remove the unused method to avoid misleading maintainers about the expected contract.

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

In @.github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py
around lines 65 - 66, Remove the unused poll() method from the _DummyProc test
helper: update the class _DummyProc in test_run_rust_windows.py to only
implement the protocol methods kill() and wait(), since
_pump_cargo_output_windows and the test suite never call poll() and the
_SupportsKillWait protocol defines only kill() and wait(); this prevents a
misleading API surface in the test helper.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai Has this now been resolved in the latest commit?

Use codegraph analysis to determine your answer.

If this comment is now resolved, please mark it as such using the API. Otherwise, please provide an AI agent prompt for the remaining work to be done to address this 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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether poll() is still present in _DummyProc in the latest commit
rg -n 'poll' .github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py

Length of output: 30


@leynos, the poll() method is still present on _DummyProc at Line 65 — this comment is not yet resolved.

Here is an AI agent prompt to address it:

In `.github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py`, remove the unused `poll()` method from the `_DummyProc` class (currently at line 65–66):

```python
    def poll(self) -> None:
        return None
```

`_pump_cargo_output_windows` never calls `poll()` on the process object, and the `_SupportsKillWait` protocol only declares `kill()` and `wait()`. Removing this method keeps `_DummyProc`'s API surface honest and avoids misleading future maintainers about what the Windows pump requires of its process argument.

No other changes are needed.

codescene-delta-analysis[bot]

This comment was marked as outdated.

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: 2

🤖 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/_cargo_runner.py:
- Around line 142-146: The polling loop that calls
_poll_pump_loop_iteration(threads, ctx, thread_exceptions) must be wrapped in a
try/finally so _finalise_pump_threads(threads, ctx.proc, thread_exceptions)
always runs even if _poll_pump_loop_iteration raises (e.g. via
_raise_cargo_timeout()); replace the bare while-not loop with a try: while not
_poll_pump_loop_iteration(...): pass finally: _finalise_pump_threads(...) to
ensure non-daemon pump threads are joined on timeout or any exception.

In @.github/actions/generate-coverage/tests/test_scripts.py:
- Around line 881-888: The test's fake_run_cargo currently ignores env_overrides
so the test can't detect whether cargo_env was forwarded; modify fake_run_cargo
in the test to capture the env_overrides (e.g., store it into a
nonlocal/captured variable or list) and return the same result, then after
calling main() add an assertion that the captured env_overrides equals the
expected cargo_env; apply the same capture-and-assert change to the other
fake_run_cargo instance referenced around lines 914-917 so both sites verify env
reuse.
🪄 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

Run ID: ed900854-8f50-4b51-9d70-f045bf217d76

📥 Commits

Reviewing files that changed from the base of the PR and between 112fbc7 and 2e6b0e9.

📒 Files selected for processing (4)
  • .github/actions/generate-coverage/scripts/_cargo_runner.py
  • .github/actions/generate-coverage/scripts/_cranelift.py
  • .github/actions/generate-coverage/scripts/tests/test_run_rust_windows.py
  • .github/actions/generate-coverage/tests/test_scripts.py

Comment thread .github/actions/generate-coverage/scripts/_cargo_runner.py Outdated
Comment thread .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.

Actionable comments posted: 1

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

973-1067: 🧹 Nitpick | 🔵 Trivial

Consider extracting helper functions to reduce method length.

Static analysis flags this test at 73 lines (threshold 70). Extract the FakeThread, FakeProc, FakeRunner, and FakeCargoCommand classes into module-level fixtures or a shared factory to trim the test body.

🤖 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 973 -
1067, The test function test_run_cargo_windows_pump_timeout is too long and
should extract the helper classes to reduce method length; move FakeThread,
FakeProc, FakeRunner, and FakeCargoCommand out of the test body into
module-level fixtures or shared factory functions (e.g., create_fake_thread,
create_fake_proc, fake_runner_factory or pytest fixtures named fake_thread,
fake_proc, fake_runner, fake_cargo_command) and update the test to instantiate
or use those fixtures, keeping the test logic in
test_run_cargo_windows_pump_timeout focused on setup, invocation of _run_cargo,
and assertions.

915-922: ⚠️ Potential issue | 🟡 Minor

Capture env_overrides to verify _run_cargo receives the correct env.

The test verifies cargo_env is threaded to cucumber but not that the primary _run_cargo call receives it. Capture and assert env_overrides to ensure end-to-end correctness.

🔧 Proposed fix
     cargo_env = {"CARGO_PROFILE_DEV_CODEGEN_BACKEND": "llvm"}
     env_calls: list[Path] = []
     cucumber_calls: list[dict[str, object]] = []
+    run_cargo_calls: list[dict[str, object]] = []

     def fake_get_cargo_coverage_env(manifest_path: Path) -> dict[str, str]:
         env_calls.append(manifest_path)
         return cargo_env

     def fake_run_cargo(
         args: list[str],
         *,
         env_overrides: typ.Mapping[str, str] | None = None,
         env_unsets: typ.Iterable[str] = (),
     ) -> str:
-        _ = (args, env_overrides, env_unsets)
+        run_cargo_calls.append({
+            "args": args,
+            "env_overrides": dict(env_overrides or {}),
+            "env_unsets": tuple(env_unsets),
+        })
         return "Coverage: 100%"
@@
     assert env_calls == [manifest_path]
+    assert len(run_cargo_calls) >= 1
+    assert run_cargo_calls[0]["env_overrides"] == cargo_env
     assert len(cucumber_calls) == 1
     assert cucumber_calls[0]["cargo_env"] == cargo_env
🤖 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 915 -
922, The test's fake_run_cargo currently ignores env_overrides so we don't
verify that _run_cargo receives cargo_env; modify fake_run_cargo to capture and
store its env_overrides argument (e.g., append to a list or set an outer-scope
variable) when called, keep returning the same "Coverage: 100%", and then add an
assertion after the call that the captured env_overrides equals the expected
cargo_env to ensure the primary _run_cargo invocation received the correct
environment.
.github/actions/generate-coverage/scripts/_cargo_runner.py (1)

162-167: ⚠️ Potential issue | 🟠 Major

Wrap the pump loop in try/finally to guarantee thread finalisation on timeout.

When _poll_pump_loop_iteration raises via _raise_cargo_timeout, the _finalise_pump_threads call is skipped, leaving non-daemon pump threads unjoined. Wrap the loop so cleanup always runs.

🔧 Proposed fix
     for thread in threads:
         thread.start()
-    while not _poll_pump_loop_iteration(threads, ctx, thread_exceptions):
-        pass
-    _finalise_pump_threads(threads, ctx.proc, thread_exceptions)
+    try:
+        while not _poll_pump_loop_iteration(threads, ctx, thread_exceptions):
+            pass
+    except BaseException:
+        with contextlib.suppress(Exception):
+            _finalise_pump_threads(threads, ctx.proc, thread_exceptions)
+        raise
+    _finalise_pump_threads(threads, ctx.proc, thread_exceptions)
     return stdout_lines
🤖 Prompt for AI Agents
In .github/actions/generate-coverage/scripts/_cargo_runner.py around lines 162-167, the while loop calling _poll_pump_loop_iteration can raise (via _raise_cargo_timeout) before _finalise_pump_threads executes; wrap the while-not loop in a try block with a bare except BaseException that suppresses exceptions while calling _finalise_pump_threads, then re-raises, followed by the normal _finalise_pump_threads call outside the except so cleanup runs on both success and failure paths.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/generate-coverage/scripts/_cargo_runner.py around lines 162
- 167, Wrap the pump loop in a try/finally so _finalise_pump_threads always runs
even if _poll_pump_loop_iteration raises (e.g. via _raise_cargo_timeout): move
the while not _poll_pump_loop_iteration(threads, ctx, thread_exceptions): pass
loop into a try block and call _finalise_pump_threads(threads, ctx.proc,
thread_exceptions) in the finally, then return stdout_lines; ensure you
reference the existing symbols (_poll_pump_loop_iteration,
_finalise_pump_threads, threads, ctx, thread_exceptions, ctx.proc, stdout_lines)
and do not change thread creation/start logic.
🤖 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 128-132: The test double's wait method is flagged for an unused
timeout parameter; rename the parameter timeout to _timeout in the wait(self,
timeout: float | None = None) -> int signature (and update any internal
references) so linters understand it is intentionally unused, and keep the
behavior in wait (setting self.waited when track_lifecycle and returning
returncode) and the docstring unchanged.

---

Duplicate comments:
In @.github/actions/generate-coverage/scripts/_cargo_runner.py:
- Around line 162-167: Wrap the pump loop in a try/finally so
_finalise_pump_threads always runs even if _poll_pump_loop_iteration raises
(e.g. via _raise_cargo_timeout): move the while not
_poll_pump_loop_iteration(threads, ctx, thread_exceptions): pass loop into a try
block and call _finalise_pump_threads(threads, ctx.proc, thread_exceptions) in
the finally, then return stdout_lines; ensure you reference the existing symbols
(_poll_pump_loop_iteration, _finalise_pump_threads, threads, ctx,
thread_exceptions, ctx.proc, stdout_lines) and do not change thread
creation/start logic.

In @.github/actions/generate-coverage/tests/test_scripts.py:
- Around line 973-1067: The test function test_run_cargo_windows_pump_timeout is
too long and should extract the helper classes to reduce method length; move
FakeThread, FakeProc, FakeRunner, and FakeCargoCommand out of the test body into
module-level fixtures or shared factory functions (e.g., create_fake_thread,
create_fake_proc, fake_runner_factory or pytest fixtures named fake_thread,
fake_proc, fake_runner, fake_cargo_command) and update the test to instantiate
or use those fixtures, keeping the test logic in
test_run_cargo_windows_pump_timeout focused on setup, invocation of _run_cargo,
and assertions.
- Around line 915-922: The test's fake_run_cargo currently ignores env_overrides
so we don't verify that _run_cargo receives cargo_env; modify fake_run_cargo to
capture and store its env_overrides argument (e.g., append to a list or set an
outer-scope variable) when called, keep returning the same "Coverage: 100%", and
then add an assertion after the call that the captured env_overrides equals the
expected cargo_env to ensure the primary _run_cargo invocation received the
correct environment.
🪄 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

Run ID: de683c77-8c1f-49d1-aa0a-ab1fb9e988fe

📥 Commits

Reviewing files that changed from the base of the PR and between 2e6b0e9 and f811709.

📒 Files selected for processing (4)
  • .github/actions/generate-coverage/scripts/_cargo_runner.py
  • .github/actions/generate-coverage/scripts/_cranelift.py
  • .github/actions/generate-coverage/scripts/run_rust.py
  • .github/actions/generate-coverage/tests/test_scripts.py

Comment thread .github/actions/generate-coverage/tests/test_scripts.py Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 17, 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 lines +973 to +1067

def test_run_cargo_windows_pump_timeout(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """``_run_cargo`` enforces the wait timeout on Windows pump threads."""
    mod = _load_module(monkeypatch, "run_rust")
    monkeypatch.setattr(mod.os, "name", "nt")
    messages: list[tuple[str, bool]] = []

    def fake_echo(message: str, *, err: bool = False, nl: bool = True) -> None:
        _ = nl
        messages.append((message, err))

    monkeypatch.setattr(mod.typer, "echo", fake_echo)
    monkeypatch.setenv("RUN_RUST_CARGO_WAIT_TIMEOUT", "1")

    class FakeThread:
        """Thread stub that stays alive for a fixed number of joins."""

        join_calls = 0

        def __init__(
            self,
            *,
            name: str,
            target: typ.Callable[..., object],
            args: tuple[object, ...],
            kwargs: dict[str, object],
        ) -> None:
            """Initialise the thread stub with ignored thread metadata."""
            _ = (name, target, args, kwargs)

        def start(self) -> None:
            """Pretend to start the thread."""

        def is_alive(self) -> bool:
            """Report liveness until enough joins have occurred."""
            return self.join_calls < 4

        def join(self, timeout: float | None = None) -> None:
            """Record a join call without blocking."""
            _ = timeout
            type(self).join_calls += 1

    monkeypatch.setattr(mod.threading, "Thread", FakeThread)
    time_values = iter([0.0, 0.0, 1.0])
    monkeypatch.setattr(mod.time, "monotonic", lambda: next(time_values))

    class FakeProc:
        """Minimal subprocess test double for Windows timeout handling."""

        def __init__(self) -> None:
            """Initialise the stub with captured output streams."""
            self.stdout = io.StringIO("out-line\n")
            self.stderr = io.StringIO("err-line\n")
            self.killed = False
            self.waited = False

        def poll(self) -> None:
            """Report that the process is still running."""

        def kill(self) -> None:
            """Record that the process was killed."""
            self.killed = True

        def wait(self, timeout: float | None = None) -> int:
            """Record the wait and return success."""
            _ = timeout
            self.waited = True
            return 0

    fake_proc = FakeProc()

    class FakeRunner:
        """Runner stub that returns the fake Windows process."""

        def popen(self, **_kw: object) -> FakeProc:
            """Return the pre-created fake process."""
            return fake_proc

    class FakeCargoCommand:
        """Cargo command stub that yields the fake Windows runner."""

        def __getitem__(self, _args: list[str]) -> FakeRunner:
            """Return a runner for the requested cargo arguments."""
            return FakeRunner()

    monkeypatch.setattr(mod, "cargo", FakeCargoCommand())

    with pytest.raises(mod.typer.Exit) as excinfo:
        mod._run_cargo(["llvm-cov"])

    assert _exit_code(excinfo.value) == 1
    assert fake_proc.killed
    assert fake_proc.waited
    assert ("::error::cargo did not exit within 1.0s; killing", True) in messages

❌ New issue: Large Method
test_run_cargo_windows_pump_timeout has 73 lines, threshold = 70

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 17, 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.

Excess Number of Function Arguments

.github/actions/generate-coverage/scripts/_cargo_runner.py: _pump_cargo_output_posix

What lead to degradation?

_pump_cargo_output_posix has 5 arguments, max arguments = 4

Why does this problem occur?

Functions with many arguments indicate either a) low cohesion where the function has too many responsibilities, or b) a missing abstraction that encapsulates those arguments.
The threshold for the Python language is 4 function arguments.

How to fix it?

Start by investigating the responsibilities of the function. Make sure it doesn't do too many things, in which case it should be split into smaller and more cohesive functions. Consider the refactoring INTRODUCE PARAMETER OBJECT to encapsulate arguments that refer to the same logical concept.

@coderabbitai

This comment was marked as resolved.

@coderabbitai

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.

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.

Code Health Improved (1 files improve in Code Health)

Gates Failed
Prevent hotspot decline (1 hotspot with Number of Functions in a Single Module)
Enforce advisory code health rules (1 file with Number of Functions in a Single Module)

Gates Passed
4 Quality Gates Passed

See analysis details in CodeScene

Reason for failure
Prevent hotspot decline Violations Code Health Impact
test_scripts.py 1 rule in this hotspot 7.79 → 7.55 Suppress
Enforce advisory code health rules Violations Code Health Impact
test_scripts.py 1 advisory rule 7.79 → 7.55 Suppress
View Improvements
File Code Health Impact Categories Improved
run_rust.py 7.24 → 7.96 Complex Method, Bumpy Road Ahead, Overall Code Complexity, Deep, Nested Complexity

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.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 17, 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.

Number of Functions in a Single Module

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

What lead to degradation?

This module has 90 functions, threshold = 75

@leynos leynos merged commit 6b13b51 into main Apr 17, 2026
22 of 23 checks passed
@leynos leynos deleted the use-env-vars-for-codegen branch April 17, 2026 18:39
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== All def/class lines with line numbers ==="
find . -name "test_scripts.py" -path "*/generate-coverage/*" -exec grep -n "^def \|^    def \|^class \|^    class " {} \;

echo ""
echo "=== Total def count ==="
find . -name "test_scripts.py" -path "*/generate-coverage/*" -exec python3 -c "
import ast, sys
src = open('{}').read()
tree = ast.parse(src)
funcs = [n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
print(f'Total def count: {len(funcs)}')
# Show top-level only
top_level = [n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and any(isinstance(p, ast.Module) for p in ast.walk(tree) if hasattr(p, 'body') and n in getattr(p, 'body', []))]
print(f'Module-level def count: {len(top_level)}')
for f in top_level:
    print(f'  line {f.lineno}: {f.name}')
" \;

echo ""
echo "=== Inner functions (nested defs) ==="
find . -name "test_scripts.py" -path "*/generate-coverage/*" -exec python3 -c "
import ast, sys
src = open('{}').read()
tree = ast.parse(src)

def get_inner_funcs(node, depth=0):
    results = []
    for child in ast.iter_child_nodes(node):
        if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
            if depth > 0:
                results.append((child.lineno, child.name, depth))
            results.extend(get_inner_funcs(child, depth+1))
        else:
            results.extend(get_inner_funcs(child, depth))
    return results

inner = get_inner_funcs(tree)
print(f'Inner (nested) defs: {len(inner)}')
for lineno, name, depth in inner:
    print(f'  line {lineno} depth={depth}: {name}')
" \;

Length of output: 8247


🏁 Script executed:

#!/bin/bash
FILE=$(find . -name "test_scripts.py" -path "*/generate-coverage/*")

echo "=== nextest config tests (lines 643-720) ==="
sed -n '643,720p' "$FILE"

echo ""
echo "=== find_nextest_binary tests (lines 1479-1530) ==="
sed -n '1479,1530p' "$FILE"

echo ""
echo "=== windows none_stdout / none_stderr tests (lines 1111-1180) ==="
sed -n '1111,1180p' "$FILE"

echo ""
echo "=== fake_echo at lines 726-740 ==="
sed -n '720,745p' "$FILE"

echo ""
echo "=== fake_echo at lines 802-820 ==="
sed -n '802,820p' "$FILE"

echo ""
echo "=== fake_echo at lines 893-910 ==="
sed -n '893,910p' "$FILE"

echo ""
echo "=== fake_echo at lines 1056-1075 ==="
sed -n '1056,1075p' "$FILE"

echo ""
echo "=== fake_run_cmd bodies in python coverage tests ==="
sed -n '1836,1900p' "$FILE"
echo "---"
sed -n '1940,2015p' "$FILE"

echo ""
echo "=== cobertura simple tests (1891-1935) ==="
sed -n '1891,1935p' "$FILE"

echo ""
echo "=== test_run_cargo_unix_pump_timeout (lines 802-892) ==="
sed -n '802,892p' "$FILE"

echo ""
echo "=== test_run_cargo_windows_nonzero_exit (lines 1012-1031) ==="
sed -n '1012,1031p' "$FILE"

Length of output: 21596


🏁 Script executed:

#!/bin/bash
FILE=$(find . -name "test_scripts.py" -path "*/generate-coverage/*")

echo "=== install_nextest tests (lines 1532-1660) ==="
sed -n '1532,1660p' "$FILE"

echo ""
echo "=== find_nextest tests for comparison (lines 1479-1540) ==="
sed -n '1479,1542p' "$FILE"

echo ""
echo "=== _WindowsPumpTimeoutFakeThread/Runner/CargoCommand module classes ==="
sed -n '174,226p' "$FILE"

echo ""
echo "=== _make_fake_cargo factory (lines 89-173) ==="
sed -n '89,173p' "$FILE"

Length of output: 11530


@leynos

File: .github/actions/generate-coverage/tests/test_scripts.py
Symbols to touch: multiple top-level test functions listed below

Fix proposal
Reduce the number of top-level tests by merging closely related tests into parametrised ones. This avoids moving fixtures or helpers and keeps hermiticity. The following merges bring the module from 90 to ≤74 functions:

  • Merge 4 nextest-config tests into 1:
    • test_nextest_config_is_temporary
    • test_nextest_config_preserves_existing
    • test_nextest_config_respects_env_path
    • test_nextest_config_prefers_xdg_when_present
  • Merge 4 nextest-binary tests into 1:
    • test_find_nextest_binary_prefers_path
    • test_find_nextest_binary_falls_back_to_home
    • test_find_nextest_binary_missing_exits
    • test_resolve_nextest_binary_returns_none_when_missing
  • Merge 2 Windows missing-stream tests into 1:
    • test_run_cargo_windows_none_stdout
    • test_run_cargo_windows_none_stderr
  • Merge 3 Cobertura percentage tests into 1:
    • test_cobertura_detail
    • test_cobertura_root_totals
    • test_cobertura_zero_lines
  • Merge 2 install_nextest checksum tests into 1:
    • test_install_nextest_checksum_match
    • test_install_nextest_checksum_mismatch
  • Merge 2 platform SHA tests into 1:
    • test_expected_sha_for_supported_platforms
    • test_expected_sha_for_unsupported_platform
  • Merge 2 cucumber runs into 1:
    • test_run_rust_with_cucumber
    • test_run_rust_with_cucumber_nextest
  • Merge 2 cucumber Cobertura flows into 1:
    • test_run_rust_with_cucumber_cobertura
    • test_run_rust_with_cucumber_cobertura_merge_failure
  • Merge 2 LCOV error-path tests into 1:
    • test_lcov_file_missing
    • test_lcov_permission_error
  • Merge 2 coveragepy XML behaviour tests into 1:
    • test_run_python_coveragepy_empty_xml
    • test_run_python_coveragepy_malformed_xml_exits

Representative snippets (pattern to apply)

  1. Windows missing-streams → single parametrised test
import pytest

`@pytest.mark.parametrize`(
    "missing, out_text, err_text",
    [
        ("stdout", None, "err-line\n"),
        ("stderr", "out-line\n", None),
    ],
)
def test_run_cargo_windows_missing_streams(monkeypatch: pytest.MonkeyPatch, missing: str, out_text: str | None, err_text: str | None) -> None:
    """``_run_cargo`` fails when either stdout or stderr is missing on Windows."""
    mod = _load_module(monkeypatch, "run_rust")
    monkeypatch.setattr(mod.os, "name", "nt")
    monkeypatch.setattr(mod.typer, "echo", lambda *_a, **_k: None)
    fake_cargo = _make_fake_cargo(out_text, err_text)
    monkeypatch.setattr(mod, "cargo", fake_cargo)

    with pytest.raises(mod.typer.Exit):
        mod._run_cargo([])

    proc = fake_cargo.last_proc
    assert proc is not None
    if missing == "stdout":
        assert proc.stderr is not None and proc.stderr.closed
    else:
        assert proc.stdout is not None and proc.stdout.closed
  1. Cobertura percent parsing → single parametrised test
import pytest

`@pytest.mark.parametrize`(
    ("xml_text", "expected"),
    [
        ("""
<coverage>
  <packages><package><classes><class><lines>
    <line hits='1'/><line hits='0'/>
  </lines></class></classes></package></packages>
</coverage>
""", "50.00"),
        ("<coverage lines-covered='81' lines-valid='100' />", "81.00"),
        ("<coverage lines-covered='0' lines-valid='0' />", "0.00"),
    ],
)
def test_cobertura_percent_variants(tmp_path: Path, run_python_module: ModuleType, xml_text: str, expected: str) -> None:
    """``get_line_coverage_percent_from_cobertura`` handles detail, totals, and zero-lines."""
    xml = tmp_path / "cov.xml"
    xml.write_text(xml_text, encoding="utf-8")
    assert run_python_module.get_line_coverage_percent_from_cobertura(xml) == expected
  1. LCOV error-paths → single parametrised test
import pytest

`@pytest.mark.parametrize`(
    ("setup", "expect_exit"),
    [
        ("missing", True),
        ("permission", True),
    ],
)
def test_lcov_error_paths(tmp_path: Path, run_rust_module: ModuleType, setup: str, expect_exit: bool) -> None:
    """Error paths for LCOV parsing: missing file and permission error."""
    lcov = tmp_path / "file.lcov"
    if setup == "permission":
        lcov.write_text("LF:1\nLH:1\n", encoding="utf-8")
        lcov.chmod(0)  # provoke a PermissionError on read
    if expect_exit:
        with pytest.raises(run_rust_module.typer.Exit):
            run_rust_module.get_line_coverage_percent_from_lcov(lcov)
  1. Install-nextest checksums → single parametrised test
import pytest

`@pytest.mark.parametrize`(
    ("payload", "expected_ok"),
    [
        (b"payload", True),
        (b"other", False),
    ],
)
def test_install_nextest_checksum_variants(tmp_path: Path, install_nextest_module: ModuleType, payload: bytes, expected_ok: bool) -> None:
    """``verify_nextest_binary`` returns True on match, False on mismatch."""
    binary = tmp_path / "cargo-nextest"
    binary.write_bytes(payload)
    import hashlib as _h
    good = _h.sha256(b"payload").hexdigest()
    assert install_nextest_module.verify_nextest_binary(binary, good) is (payload == b"payload") is expected_ok

AI coding-agent prompt

Goal: Reduce “Number of Functions in a Single Module” for .github/actions/generate-coverage/tests/test_scripts.py from 90 to ≤75 by merging related tests into parametrised ones. Do not move fixtures or helpers to other files. Keep test semantics and hermiticity intact.

Constraints:
- File to edit: .github/actions/generate-coverage/tests/test_scripts.py
- Preserve existing fixtures and helper factories (e.g., run_rust_module, install_nextest_module, _make_fake_cargo, _WindowsPumpTimeoutFakeThread, _WindowsPumpFakeProc, _make_cucumber_spy, assertion helpers).
- Keep assertions and behaviours equivalent. Where a test currently expects Exit, keep the expectation.
- Use pytest.mark.parametrize to merge tests; reuse existing code inside each new test.
- Remove the superseded individual tests after introducing parametrised replacements.
- After changes, ensure total function count in this module ≤75.

Plan:
1) Merge nextest-config tests (4→1)
   - Replace:
     - test_nextest_config_is_temporary
     - test_nextest_config_preserves_existing
     - test_nextest_config_respects_env_path
     - test_nextest_config_prefers_xdg_when_present
   - With a single:
     - test_nextest_config_variants[temporary|preserves|env|xdg]
   - Param columns: mode, pre_setup (callable), expected_path_factory (callable), expect_cleanup (bool).
   - Inline per-variant setup matching current bodies; assert identical outcomes.

2) Merge nextest-binary tests (4→1)
   - Replace:
     - test_find_nextest_binary_prefers_path
     - test_find_nextest_binary_falls_back_to_home
     - test_find_nextest_binary_missing_exits
     - test_resolve_nextest_binary_returns_none_when_missing
   - With a single test_find_nextest_binary_variants parametrised over:
     - case="prefer_path" → expect concrete Path
     - case="fallback_home" → expect concrete Path
     - case="missing_exits" → expect pytest.raises(typer.Exit)
     - case="resolve_missing" → call _resolve_nextest_binary() and expect None
   - Reuse the same monkeypatch stubs from current tests inside the param cases.

3) Merge Windows missing-stream tests (2→1)
   - Replace:
     - test_run_cargo_windows_none_stdout
     - test_run_cargo_windows_none_stderr
   - With one test_run_cargo_windows_missing_streams parametrised by missing={"stdout","stderr"}.
   - Use the representative snippet provided.

4) Merge Cobertura percentage tests (3→1)
   - Replace:
     - test_cobertura_detail
     - test_cobertura_root_totals
     - test_cobertura_zero_lines
   - With test_cobertura_percent_variants as in the snippet.

5) Merge install-nextest checksum tests (2→1)
   - Replace:
     - test_install_nextest_checksum_match
     - test_install_nextest_checksum_mismatch
   - With test_install_nextest_checksum_variants as in the snippet.

6) Merge expected SHA tests (2→1)
   - Replace:
     - test_expected_sha_for_supported_platforms
     - test_expected_sha_for_unsupported_platform
   - With test_expected_sha_for_platforms(case, expected_ok) parametrising OS/arch tuples; assert pass/fail accordingly using the current helper under test.

7) Merge cucumber base runs (2→1)
   - Replace:
     - test_run_rust_with_cucumber
     - test_run_rust_with_cucumber_nextest
   - With test_run_rust_with_cucumber_variants parametrised by use_nextest={False,True}. Keep existing setup and assertions per branch.

8) Merge cucumber Cobertura flows (2→1)
   - Replace:
     - test_run_rust_with_cucumber_cobertura
     - test_run_rust_with_cucumber_cobertura_merge_failure
   - With test_run_rust_with_cucumber_cobertura_variants parametrised by merge_ok={True,False}. Reuse present XML creation and failure path; assert both success and failure cases.

9) Merge LCOV error-path tests (2→1)
   - Replace:
     - test_lcov_file_missing
     - test_lcov_permission_error
   - With test_lcov_error_paths parametrised over {"missing","permission"} as in the snippet.

10) Merge coverage.py empty/malformed (2→1)
   - Replace:
     - test_run_python_coveragepy_empty_xml
     - test_run_python_coveragepy_malformed_xml_exits
   - With test_run_python_coveragepy_xml_variants parametrised by xml_text and expect_exit(bool). Reuse current fake_run_cmd and fake_tmp_coveragepy_xml, switching the XML content per case and asserting either successful percent/side-effects or pytest.raises(typer.Exit).

11) Verify function count ≤75
   - At the end of the edit, run an AST counter script to print the number of FunctionDef/AsyncFunctionDef nodes in this module and ensure ≤75. If still above 75, optionally:
     - Combine test_run_cargo_windows and test_run_cargo_windows_closes_streams into a single parametrised test over {check_streaming, check_cleanup} retaining all existing assertions.

Acceptance criteria:
- pytest passes locally (where available).
- The merged tests retain identical behaviour and assertions.
- The module’s function count is ≤75.

Deliverables:
- Updated test_scripts.py with parametrised tests replacing the listed individual tests.
- Short commit message: “tests: reduce function count in test_scripts.py via parametrised merges (90→≤75)”.

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