Skip to content

Fan scoped mutation runs out across shards for large windows (#370, #371) - #378

Open
leynos wants to merge 1 commit into
mainfrom
fix/scoped-mutation-run-fanout
Open

Fan scoped mutation runs out across shards for large windows (#370, #371)#378
leynos wants to merge 1 commit into
mainfrom
fix/scoped-mutation-run-fanout

Conversation

@leynos

@leynos leynos commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

Scheduled (scoped) mutation runs pinned the root target to a single shard
regardless of shard-count. When the window-hours change-detection window
caught a large batch of changed files, that one job outgrew timeout-minutes
and was killed, silently truncating coverage — wildside ran 308 mutants for
384 minutes before the 300-minute step timeout (#370); rstest-bdd was
cancelled at 42% of 661 mutants (#371).

This sizes the scoped root fan-out from the changed-file count:

  • Small windows stay single-shard — each shard would re-pay the baseline
    build-and-test cost, not worth it for a handful of mutants.
  • Large windows fan the root target out across up to shard-count shards.
    Every shard carries the same scoped --file list and cargo mutants --shard k/N partitions the mutant set among them, reusing the existing
    sharding path.

The fan-out reuses the existing shard-count input (default 6) as an upper
bound, so callers benefit with no new input and no configuration change. Extra
crates stay single-shard, matching full_run_matrix. The threshold
(SCOPED_FILES_PER_SHARD = 15) is sized conservatively so a shard's changed
files stay well under the ~200-250-mutant budget observed at the 300-minute
ceiling (~60-65 s/mutant).

Closes #370. Closes #371.

Review walkthrough

  • workflow_scripts/mutation_detect_changes.py — new scoped_shard_count
    helper and SCOPED_FILES_PER_SHARD; scoped_run_matrix fans the root
    target out across shards for large batches; docstrings updated.
  • .github/workflows/mutation-cargo.ymlshard-count input description
    updated to describe scoped fan-out.
  • docs/mutation-cargo-workflow.md — behaviour and input table updated.
  • workflow_scripts/tests/test_mutation_detect_changes.py — unit tests for
    scoped_shard_count thresholds, single-shard small batches, fan-out,
    the shard-count cap, and extra crates staying single-shard.

Validation

  • pytest workflow_scripts/tests/test_mutation_detect_changes.py — 25 passed.
  • pytest workflow_scripts/tests/test_mutation_properties.py — 6 passed.
  • ruff format --check / ruff check — clean; markdownlint / typos on the
    changed doc — clean.

…370)

Scheduled (scoped) runs pinned the root target to a single shard regardless
of shard-count. When window-hours caught a large batch of changed files that
one job outgrew timeout-minutes and was killed, truncating coverage
(wildside 308 mutants over 384 min; rstest-bdd cancelled at 42%).

Size the scoped root fan-out from the changed-file count: small windows stay
single-shard (each shard would re-pay the baseline build for a handful of
mutants), while large windows fan out across up to shard-count shards. Every
shard carries the same scoped file list and cargo mutants --shard k/N
partitions the mutants among them, reusing the existing sharding path with
no new caller input. Extra crates stay single-shard, matching full runs.

Cover scoped_shard_count and the fan-out matrix with unit tests; update the
script, workflow input, and caller-guide docs.

@sourcery-ai sourcery-ai 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.

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

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

Summary

  • Added dynamic sharding for large scheduled mutation-testing windows, capped by the configured shard-count (default 6).
  • Preserved single-shard behaviour for small scoped runs and extra crates.
  • Ensured each shard uses the same scoped file list while cargo mutants --shard k/N partitions mutants.
  • Added threshold and matrix-generation tests, plus updated workflow and user documentation.
  • Validation passes: tests, formatting, linting, Markdown, and spelling checks.

Walkthrough

Update scoped mutation detection to fan out large root-file windows across dynamically calculated shards, retain single-shard extra-crate targets, test the boundaries, and document the full versus scoped shard-count behaviour.

Changes

Scoped mutation sharding

Layer / File(s) Summary
Compute and validate scoped fan-out
workflow_scripts/mutation_detect_changes.py, workflow_scripts/tests/test_mutation_detect_changes.py
Calculate root shard counts from changed-file totals, cap them at the configured maximum, keep extra-crate targets single-sharded, and validate threshold and mixed-target behaviour.
Document shard-count behaviour
.github/workflows/mutation-cargo.yml, docs/mutation-cargo-workflow.md
Describe full-run fan-out, size-aware scoped fan-out, and the timeout-related coverage limitation.

Sequence Diagram(s)

sequenceDiagram
  participant MutationDetectChanges
  participant GitHubActions
  participant MutationJobs
  MutationDetectChanges->>MutationDetectChanges: Calculate scoped shard count from changed root files
  MutationDetectChanges->>GitHubActions: Emit root and extra-crate matrix entries
  GitHubActions->>MutationJobs: Start matrix jobs with shard metadata
Loading

Possibly related issues

  • Issue 340: It also changes scoped_run_matrix to address mutation-testing coverage gaps.
  • Axinite issue 290: Its objective is directly addressed by dynamically fanning out large scoped mutation batches.

Suggested labels: Issue

Poem

Count the files and split the load,
Let busy shards share the road.
Keep small windows neat and light,
Cap the swarm at its set height.
Test each path, then run mutation bright.

🚥 Pre-merge checks | ✅ 16 | ❌ 4

❌ Failed checks (3 warnings, 1 inconclusive)

Check name Status Explanation Resolution
User-Facing Documentation ⚠️ Warning Changed mutation-run fan-out is documented in docs/mutation-cargo-workflow.md, but docs/users-guide.md is absent and was not updated. Create or update docs/users-guide.md to explain scoped-run shard fan-out, the threshold, and any user-facing usage or migration notes.
Developer Documentation ⚠️ Warning The patch updates workflow docs, but docs/developers-guide.md was untouched and the mutation execplan still says scoped runs stay single-shard, so the new sharding design is not fully documented. Update docs/developers-guide.md’s mutation section and the execplan decision log to describe threshold-based scoped fan-out, then align any roadmap wording.
Testing (Property / Proof) ⚠️ Warning The new fan-out helper encodes range invariants, but coverage is only example-based; no Hypothesis property test or proof exercises the general cases. Add Hypothesis tests for monotonicity and capping in scoped_shard_count, plus matrix invariants for root versus extra crates, or justify an exhaustive proof.
Observability ❓ Inconclusive placeholder Need code evidence before deciding observability coverage.
✅ Passed checks (16 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the scoped fan-out change and links both related issues.
Description check ✅ Passed The description clearly explains the scoped shard fan-out fix and its validation.
Linked Issues check ✅ Passed The changes implement size-aware scoped sharding, preserve small-window single-shard runs, and keep extra crates single-shard, satisfying #370 and #371.
Out of Scope Changes check ✅ Passed All edits are confined to the workflow, detector script, tests, and docs needed for the scoped fan-out change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Testing (Overall) ✅ Passed PASS — The new tests exercise the real matrix logic and CLI path, cover threshold, fan-out, capping, and extra-crate behaviour, and use no mocks.
Module-Level Documentation ✅ Passed Both touched Python modules carry clear module docstrings: the script explains change-detection and sharding, and the test module states it tests that helper.
Testing (Unit And Behavioural) ✅ Passed PASS: New unit tests cover shard-threshold edge cases and invariants, and CLI-level tests exercise the script entrypoint, skip path, dispatch path, and error path.
Testing (Compile-Time / Ui) ✅ Passed No compile-time/UI surface is present: the PR only changes Python/workflow/docs and adds focused unit tests for matrix sizing and JSON shape, so trybuild/snapshot tests are unnecessary.
Unit Architecture ✅ Passed PASS: The new shard-sizing logic is pure and confined to matrix construction; I/O, git access, and output writes stay in the main boundary.
Domain Architecture ✅ Passed PASS: The PR only alters workflow, docs, and tests in mutation-testing automation; no domain model or business logic is introduced or entangled with infrastructure.
Security And Privacy ✅ Passed PASS: only sharding thresholds and docs changed; no secrets, auth/permission changes, or unsafe input sinks were added.
Performance And Resource Use ✅ Passed PASS: The new sharding logic is linear in file/bucket count and capped by shard_count; no new unbounded loops, I/O, or allocations appear.
Concurrency And State ✅ Passed PASS: the change is pure matrix construction; fan-out jobs are isolated, with no shared mutable state, locks, async tasks, or ordering-sensitive coordination introduced.
Architectural Complexity And Maintainability ✅ Passed PASS: keep the new shard-sizing helper local and specific; it replaces duplicated inline logic and adds no new layer, indirection, or dependency.
Rust Compiler Lint Integrity ✅ Passed No Rust sources changed in the PR; the diff only touches workflow, docs, and Python tests/scripts, so no lint suppressions or clone issues were introduced.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/scoped-mutation-run-fanout

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

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos marked this pull request as ready for review July 26, 2026 22:46

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No quality gates enabled for this code.

@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 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.

@coderabbitai coderabbitai Bot added the Issue label Jul 27, 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: 1

🤖 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 `@workflow_scripts/tests/test_mutation_detect_changes.py`:
- Around line 150-209: Add descriptive failure messages to every assertion
introduced in the listed tests: test_scoped_shard_count_thresholds,
test_small_scoped_root_stays_single_shard, test_large_scoped_root_fans_out,
test_large_scoped_root_caps_at_shard_count, and
test_scoped_extra_crate_stays_single_shard_when_root_fans_out. Preserve each
assertion’s existing condition while supplying a useful message that identifies
the expected scoped shard or matrix behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cd1724dc-d882-483e-9bd7-3d5ba2725eec

📥 Commits

Reviewing files that changed from the base of the PR and between 074f7d8 and d783059.

📒 Files selected for processing (4)
  • .github/workflows/mutation-cargo.yml
  • docs/mutation-cargo-workflow.md
  • workflow_scripts/mutation_detect_changes.py
  • workflow_scripts/tests/test_mutation_detect_changes.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/polythene (auto-detected)

Comment on lines +150 to +209
@pytest.mark.parametrize(
("file_count", "shard_count", "expected"),
[
(0, 6, 1),
(1, 6, 1),
(detect.SCOPED_FILES_PER_SHARD, 6, 1),
(detect.SCOPED_FILES_PER_SHARD + 1, 6, 2),
(detect.SCOPED_FILES_PER_SHARD * 3, 6, 3),
(detect.SCOPED_FILES_PER_SHARD * 100, 6, 6),
(detect.SCOPED_FILES_PER_SHARD * 100, 1, 1),
],
)
def test_scoped_shard_count_thresholds(
self, file_count: int, shard_count: int, expected: int
) -> None:
"""Scoped fan-out grows with the batch, capped at ``shard_count``."""
assert detect.scoped_shard_count(file_count, shard_count) == expected

def test_small_scoped_root_stays_single_shard(self) -> None:
"""A handful of changed files runs the root target as one shard."""
config = _config(shard_count=6)
files = [f"src/f{i}.rs" for i in range(detect.SCOPED_FILES_PER_SHARD)]
entries = detect.scoped_run_matrix({".": files}, config)
assert len(entries) == 1
assert entries[0].shard == 0
assert entries[0].shard_count == 1

def test_large_scoped_root_fans_out(self) -> None:
"""A large root batch fans out across shards sharing the file list."""
config = _config(shard_count=6)
files = [f"src/f{i}.rs" for i in range(detect.SCOPED_FILES_PER_SHARD * 3)]
entries = detect.scoped_run_matrix({".": files}, config)
assert len(entries) == 3
assert [e.shard for e in entries] == [0, 1, 2]
assert all(e.shard_count == 3 for e in entries)
# Every shard carries the same scoped file list; cargo-mutants'
# --shard k/N partitions the mutant set among them.
assert all(e.files == " ".join(files) for e in entries)
assert all(e.slug == "root" for e in entries)

def test_large_scoped_root_caps_at_shard_count(self) -> None:
"""Fan-out never exceeds the configured shard-count ceiling."""
config = _config(shard_count=4)
files = [f"src/f{i}.rs" for i in range(detect.SCOPED_FILES_PER_SHARD * 20)]
entries = detect.scoped_run_matrix({".": files}, config)
assert len(entries) == 4
assert all(e.shard_count == 4 for e in entries)

def test_scoped_extra_crate_stays_single_shard_when_root_fans_out(self) -> None:
"""Only the root target fans out; extra crates stay single-shard."""
config = _config(shard_count=6, extra_crate_dirs=("testkit",))
root_files = [f"src/f{i}.rs" for i in range(detect.SCOPED_FILES_PER_SHARD * 2)]
buckets = {".": root_files, "testkit": ["testkit/src/lib.rs"]}
entries = detect.scoped_run_matrix(buckets, config)
root = [e for e in entries if e.dir == "."]
extra = [e for e in entries if e.dir == "testkit"]
assert len(root) == 2
assert len(extra) == 1
assert extra[0].shard == 0
assert extra[0].shard_count == 1

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add failure messages to the new bare asserts.

Every new assertion here (test_scoped_shard_count_thresholds, test_small_scoped_root_stays_single_shard, test_large_scoped_root_fans_out, test_large_scoped_root_caps_at_shard_count, test_scoped_extra_crate_stays_single_shard_when_root_fans_out) omits a failure message. As per path instructions, **/*.py files must "Use assert …, "message" over bare asserts".

🧪 Example fix for one case (apply the same pattern to the rest)
-        assert detect.scoped_shard_count(file_count, shard_count) == expected
+        assert (
+            detect.scoped_shard_count(file_count, shard_count) == expected
+        ), f"expected {expected} shards for {file_count} files/{shard_count} cap"
🤖 Prompt for 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.

In `@workflow_scripts/tests/test_mutation_detect_changes.py` around lines 150 -
209, Add descriptive failure messages to every assertion introduced in the
listed tests: test_scoped_shard_count_thresholds,
test_small_scoped_root_stays_single_shard, test_large_scoped_root_fans_out,
test_large_scoped_root_caps_at_shard_count, and
test_scoped_extra_crate_stays_single_shard_when_root_fans_out. Preserve each
assertion’s existing condition while supplying a useful message that identifies
the expected scoped shard or matrix behavior.

Source: Path instructions

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

Labels

Projects

None yet

2 participants