Skip to content

Kill mutation survivors in the post-turn quality stop hook (#36, #37, #38, #40) - #50

Open
leynos wants to merge 3 commits into
mainfrom
kill-mutation-survivors
Open

Kill mutation survivors in the post-turn quality stop hook (#36, #37, #38, #40)#50
leynos wants to merge 3 commits into
mainfrom
kill-mutation-survivors

Conversation

@leynos

@leynos leynos commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Closes #37 and #38. Addresses #36 and #40. Issues #39 and #41 are deferred in full.

Survivor-triage PR for the mutation-testing run recorded against main (b850e2f). It adds targeted unit tests for the highest-value surviving mutants in hooks/post-turn-quality-stop-hook.py, and eliminates or suppresses the equivalent mutants identified at triage.

Before/after (full mutmut run, mutmut 3.6.0, CPython 3.14)

Status Before After
Killed 151 461
Survived 235 129
No tests 639 429
Total generated 1025 1019

Six equivalent mutants are no longer generated (pragma suppression and the removal of a redundant argument — see below). The 129 remaining survivors break down as 117 deferred real gaps (71 prepare_run_stop_checks + 10 run_stop_checks#39; 18 repo_root + 18 get_make_targets#36) and 12 newly identified equivalents (7 format_reason, 3 parse_make_targets, 2 truncate — justified below).

Review walkthrough

  • hooks/test_post_turn_quality_stop_hook.py — new test classes for truncate (boundary and exact head/tail split), parse_make_targets and is_missing_makefile (representative make -qp fixture), detect_categories and default_categories (full-dict equality across every extension group), format_reason (exact multi-line rendering, the 60-file elision boundary, missing-key command entries), the full parse_env tuple and parse_max_output fallback, and run() (real subprocess capturing text output, the NotADirectoryError branch, the filename-less FileNotFoundError guard). Existing plumbing tests gain exact argv (call_args_list) assertions, exact error-message equality in place of substring checks, wiring assertions in compush_check, and the ahead-by-exactly-one boundary case.
  • hooks/post-turn-quality-stop-hook.py — equivalence handling only; no behavioural change (the full suite and a fresh mutation run confirm).

Equivalent mutants (category 2)

mutmut 3.6 honours # pragma: no mutate only at statement level (its pragma visitor ignores trailing comments inside call argument lists), so the worklist's four equivalents were handled structurally:

  • parse_env flag defaults ("""XXXX" on POST_TURN_ALWAYS_FETCH/POST_TURN_COMPUSH): hoisted into a single flag_default = "" # pragma: no mutate statement with a justification comment — any mutated default is still non-truthy to parse_bool_env.
  • run() check=Falsecheck=None/dropped: the argument was the subprocess default, so both mutants were equivalent by construction; the redundant argument is removed and a comment records that non-zero return codes are deliberately surfaced via CompletedProcess.returncode.
  • Newly identified during the re-run: the POST_TURN_MAX_OUTPUT_CHARS default ("12000" → non-numeric) falls back to parse_max_output's own 12000 default — hoisted and suppressed the same way.

Twelve further equivalents remain in the survivor list unsuppressed, because each sits on a line that also carries killable mutants (line-level suppression would shield them):

  • format_reason (7): c.get("exit_code", …) defaults are unreachable — the failures filter guarantees the key is present; c.get("stdout"/"stderr", None/dropped) defaults are filtered identically to "" by the if x comprehension guard.
  • parse_make_targets (3): corrupting one element of the startswith(("#", "\t", " ")) guard is unobservable — the rule regex rejects those lines anyway.
  • truncate (2): <=< at both boundaries converges on the same output through the fall-through slice paths.

Red-green evidence

Each new test was verified against a hand-applied mutant diff (fail) and the reverted source (pass); cycles were run for format_reason, run, default_categories, has_unpushed_commits, and parse_env. Representative transcript:

=== RED: format_reason literal (x_format_reason__mutmut_2)
    ("Post-turn checks failed." -> "XXPost-turn checks failed.XX")
E   AssertionError: unexpected minimal reason: 'XXPost-turn checks failed.XX\n\nError: boom…'
2 failed, 2 passed, 68 deselected in 0.15s
=== GREEN: format_reason literal (reverted)
4 passed, 68 deselected in 0.12s

(Two same-length mutations initially produced misleading transcripts because Python's mtime-plus-size .pyc invalidation served stale bytecode; the cycles were re-run with PYTHONDONTWRITEBYTECODE=1 and clean caches, and both then behaved correctly.)

Deferred work

The 400-line test-budget tolerance for this PR is spent (392 net new test lines). Deferred explicitly:

Validation

  • make ci (check-fmt, lint, typecheck, full pytest suite — 149 tests) green.
  • Full mutmut run re-runs after each commit; scoped mutmut run 'hooks.post-turn-quality-stop-hook.x_format_reason*' confirmed the final residual kill.

Summary by Sourcery

Strengthen post-turn quality hook behavior around git status, upstream detection, commit/push checks, environment parsing, and subprocess execution, while annotating equivalent mutation points.

New Features:

  • Add comprehensive unit tests for truncate, make target parsing, missing-makefile detection, category detection/defaults, environment parsing, and max-output parsing in the post-turn quality stop hook.
  • Add tests covering real subprocess execution, error fallbacks, and boundary conditions in run(), including NotADirectoryError and FileNotFoundError cases.

Enhancements:

  • Tighten existing tests to assert exact git command invocations, precise error messages, and compush_check wiring and messages.
  • Refine run() and parse_env() implementation to remove redundant arguments and centralize environment defaults with pragma annotations for known equivalent mutants.

leynos added 2 commits July 13, 2026 20:51
Add targeted unit tests for the highest-value mutation-testing
survivors reported against the stop hook: exact rendering tests for
format_reason, boundary and split tests for truncate, representative
parsing tests for parse_make_targets and is_missing_makefile,
extension-mapping tests for detect_categories, full-tuple parse_env
and parse_max_output tests, equality tests for default_categories,
real-subprocess and error-branch tests for run(), and argv, wiring,
and exact-message assertions for the git plumbing helpers.

Annotate the four equivalent mutants (subprocess check=False and the
falsy env-var defaults) with pragma no mutate comments, splitting the
affected arguments onto their own lines so the suppression does not
shield killable mutants on the same line.
mutmut 3.6 only honours pragma comments at statement level, so the
argument-level annotations added earlier were ignored. Hoist the
falsy env-var defaults into their own pragma-suppressed statements,
drop the redundant check=False argument from run() (False is the
subprocess default, so both of its mutants were equivalent by
construction), and suppress the max-output default whose mutation
falls back to the same 12000 value.

Also convert the remaining substring assertions on git error
messages to exact equality, cover truncate at max_chars=1, extend
the make -qp fixture with a mid-stream blank line and a mixed
pattern/plain rule, and exercise format_reason with a command entry
lacking cmd/stdout/stderr keys and with a falsy base ref.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 20 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 77975421-9b7c-40fe-9e95-2e398031342f

📥 Commits

Reviewing files that changed from the base of the PR and between 8cd59d2 and 97441e9.

📒 Files selected for processing (2)
  • hooks/post-turn-quality-stop-hook.py
  • hooks/test_post_turn_quality_stop_hook.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kill-mutation-survivors

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

@sourcery-ai

sourcery-ai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR adds high-precision tests around the post-turn quality stop hook to kill/suppress surviving mutation-testing mutants, and performs small refactorings in the hook implementation to structurally suppress known equivalent mutants without changing behaviour.

Flow diagram for updated parse_env default handling

flowchart TD
    parse_env[parse_env]
    flag_default["flag_default = ''  (no mutate)"]
    max_output_default["max_output_default = '12000'  (no mutate)"]

    parse_env --> flag_default
    parse_env --> max_output_default

    base_ref_env["os.environ.get POST_TURN_BASE_REF"]
    always_fetch_env["os.environ.get POST_TURN_ALWAYS_FETCH"]
    max_output_env["os.environ.get POST_TURN_MAX_OUTPUT_CHARS"]
    compush_env["os.environ.get POST_TURN_COMPUSH"]

    parse_env --> base_ref_env
    parse_env --> always_fetch_env
    parse_env --> max_output_env
    parse_env --> compush_env

    always_fetch_env --> parse_bool_env
    compush_env --> parse_bool_env
    max_output_env --> parse_max_output

    flag_default --> always_fetch_env
    flag_default --> compush_env
    max_output_default --> max_output_env

    parse_bool_env[parse_bool_env]
    parse_max_output[parse_max_output]

    parse_bool_env --> always_fetch
    parse_bool_env --> compush
    parse_max_output --> max_out

    base_ref_env --> base_ref

    base_ref[base_ref]
    always_fetch[always_fetch]
    max_out[max_out]
    compush[compush]

    base_ref --> result_tuple
    always_fetch --> result_tuple
    max_out --> result_tuple
    compush --> result_tuple

    result_tuple[("(base_ref, always_fetch, max_out, compush)")]
Loading

File-Level Changes

Change Details Files
Strengthen existing git-plumbing and compush-check tests with exact command, error-message, and wiring assertions to better constrain behaviour under mutation.
  • Add unittest.mock.call imports and assert exact call_args_list for git diff/ls-files/rev-parse/rev-list invocations.
  • Replace substring-based error-message checks with exact string equality for git error paths.
  • Add tests for edge conditions such as rev-list count == 1 and empty outputs, and assert compush_check calls its helpers with precise arguments.
hooks/test_post_turn_quality_stop_hook.py
Add focused unit tests for run(), truncate(), parse_make_targets(), is_missing_makefile(), detect_categories(), default_categories(), format_reason(), parse_env(), and parse_max_output to pin down their behaviour and kill surviving mutants.
  • Add round-trip tests for run() covering normal subprocess output capture, NotADirectoryError cwd handling, and filename-less FileNotFoundError fallbacks.
  • Define TRUNCATE_MARKER and parametrized tests that cover truncate() boundaries and exact head/tail splits for truncated text.
  • Add representative make -qp fixture output and verify parse_make_targets() target extraction and is_missing_makefile() case-insensitive detection.
  • Verify default_categories() and detect_categories() mappings across Python/TS, Rust, and Markdown extension groups.
  • Add multi-scenario format_reason() tests for minimal error-only state, full state with commands and targets, file-list truncation at 60/61 files, and treatment of commands missing exit_code.
  • Add parse_env() tests for default tuple and environment-variable overrides, and parse_max_output() tests for valid integers and non-numeric/empty fallbacks to 12000.
hooks/test_post_turn_quality_stop_hook.py
Refactor parse_env() and run() implementation to structurally suppress known equivalent mutants using pragma comments, without altering runtime behaviour.
  • Remove the redundant check=False argument from subprocess.run in run(), documenting that non-zero exit codes are intentionally surfaced via CompletedProcess.returncode.
  • Hoist POST_TURN_ALWAYS_FETCH and POST_TURN_COMPUSH flag defaults into a single flag_default variable annotated with # pragma: no mutate to prevent equivalent mutants of empty-string defaults.
  • Hoist POST_TURN_MAX_OUTPUT_CHARS default into max_output_default annotated with # pragma: no mutate to prevent equivalent non-numeric default mutants, while still letting parse_max_output enforce the 12000 fallback.
hooks/post-turn-quality-stop-hook.py

Assessment against linked issues

Issue Objective Addressed Explanation
#37 Add tests for run() that execute a real subprocess command and assert decoded stdout/stderr and the actual exit code, ensuring text=True, capture_output=True usage and that check=True would raise instead of returning.
#37 Add tests that exercise both error-path fallbacks of run(): the missing-cwd (FileNotFoundError with a non-matching filename) and NotADirectoryError branches, asserting the full fallback CompletedProcess fields (args, returncode, stdout, stderr).
#37 Add a test for the filename-less FileNotFoundError guard in run(), confirming that the cwd fallback is returned rather than re-raised and that args mirrors the command.

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

@leynos leynos changed the title Kill mutation survivors in the post-turn quality stop hook Kill mutation survivors in the post-turn quality stop hook (#36, #37, #38, #40) Jul 17, 2026
Add issue references to the docstrings of the new tests and tightened
assertions in the post-turn quality stop hook suite, so each kill can
be traced back to #36, #37, #38, and #40.
@leynos
leynos marked this pull request as ready for review July 25, 2026 14:53

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

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

Please try again later or upgrade to continue using Sourcery

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mutation testing: run() subprocess wrapper survivors

1 participant