Skip to content

ci: mutation-test the package lines a PR changes (warn-only) - #739

Open
napetrov wants to merge 16 commits into
intel:mainfrom
napetrov:ci-diff-mutation
Open

napetrov wants to merge 16 commits into
intel:mainfrom
napetrov:ci-diff-mutation

Conversation

@napetrov

Copy link
Copy Markdown
Contributor

AGENTS.md asks authors to break the code each test covers and confirm that the test fails. Until now that happened only by hand. CI gates coverage of changed lines, but not whether a test notices when one of those lines breaks. mutation.yml runs mutmut weekly, on three modules only.

Change

  • dokimi/scripts/mutate_changed.py works through these steps:

    1. It takes the changed lines from git diff -U0 BASE...HEAD over dokimi/dokimi/**.py.
    2. Candidate tests are the test files that import a changed module, with files named after the module first.
    3. A coverage pass with per-test contexts (--cov-context=test, COVERAGE_CORE=ctrace) runs one pytest process per file and stops at half of --budget. It maps each changed line to the tests that execute it.
    4. A small AST mutator builds mutants on changed lines only: flip a comparison, swap and/or, negate an if/while condition, return None, flip a bool, bump an int, swap +/-, drop a not. Each mutant is spliced into the source text, so the rest of the file is untouched. At most 40 are made, spread round-robin over the changed lines.
    5. Each mutant runs only its covering tests, with -x, in a temp copy of the tracked tree, so subprocess tests see the mutant too. If pytest exits 0, the mutant survived.

    Tests that fail without a mutant are left out and listed in the report. The output is a markdown summary, a JSON file, and a ::warning annotation per survivor. --fail-on-survivor turns those into ::error annotations and exits 1.

  • ci.yml has a new job, mutation-diff: pull_request only, after static, py3.13. The step stops at 22 min (no new mutant after 900 s, at most 120 s per mutant) and the job at 30. The summary is written in its own !cancelled() step, so it survives a step timeout. Warn-only for now.

  • make ci-local runs the job, because ci_local.py reads the workflows (checked: ci_local.py run --jobs mutation-diff). To leave it out locally, pass a --jobs list without it.

  • AGENTS.md documents the job next to the by-hand step. test_ci_label_triggers: a push runs every PR job except mutation-diff, which has no base to diff against on a push. .gitignore covers its output files.

  • The weekly mutmut pilot stays as is. It scores whole core modules; this job looks only at what a PR changes.

Measured on 6 recent merged PRs

Run locally (Windows, Python 3.15, serial) at each PR's merge commit against its parent. A GitHub Linux runner should be faster.

PR changed pkg lines candidate test files (not run: out of time) mutants run killed survived wall
#721 4 0 0 (no mutable operator) – – 0.1 s
#641 31 21 (11) 16 14 2 1125 s
#635 9 18 (14) 7 6 1 781 s
#634 3 1 1 1 0 35 s
#665 6 8 1 1 0 398 s
#666 18 48 (31) 6 6 0 715 s

In total 31 mutants ran: 28 were killed and 3 survived. All 3 survivors are equivalent or cosmetic:

  • zip(..., strict=True) → False, on inputs whose lengths are already checked (stats.py:403);
  • and → or in an overflow fallback that returns the same mean on normal input (stats.py:167);
  • sha[:12] → [:13] in an error message (workspace.py:665).

Since every survivor was noise, the job starts warn-only. Two more things showed up while measuring:

  • pytest-cov's per-test contexts break under xdist (1338 setup errors), so the coverage pass is serial and bounded per file.
  • On Python 3.14+, the default sys.monitoring core records no contexts, hence COVERAGE_CORE=ctrace.

Not run / limits

  • zizmor (not installed locally), and the job on a real GitHub runner: the first CI run of this PR is the first real measurement.
  • The operator set does not touch call arguments or string literals.
  • The candidate tests are found from imports, so a module that is tested only through cli.py can show its lines as uncovered.

Local self-review (pr-review, lenses: correctness, silent-failures, tests, comments, design): no blocking findings. A design finding, that the step could overrun the job timeout and lose the summary, is fixed in the second commit.

🤖 Generated with Claude Code

scripts/mutate_changed.py diffs the PR against its base, finds the tests
covering each changed line through per-test coverage contexts, and runs up to
40 AST mutants (flip a comparison, swap and/or, negate a condition, return None,
flip a bool, bump an int, swap +/-, drop not) against only those tests, in a
copy of the tree. Survivors go to the step summary and as warnings.

New ci.yml job mutation-diff (PR only, after static, 30 min cap); make ci-local
runs it from the workflow. AGENTS.md documents it next to the by-hand step.

Signed-off-by: Petrov, Nikolay A <nikolay.a.petrov@intel.com>
…y in its own step

Signed-off-by: Petrov, Nikolay A <nikolay.a.petrov@intel.com>
@napetrov
napetrov requested a review from a team as a code owner November 13, 2026 23:56
Copilot AI balanced review requested due to automatic review settings November 13, 2026 23:56

Copilot AI 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.

🟡 Changes recommended

Shared temporary-tree bytecode caches can cause mutants to execute stale code and produce incorrect results, while incomplete timeout cleanup can leave child processes running during subsequent runs.

2 open issues
What changed in this PR

2 new findings.

This PR adds warn-only mutation testing for changed package lines, complementing changed-line coverage and the existing weekly whole-module mutation job.

Changes:

  • Adds a budgeted mutation runner that selects covering tests and reports survivors in Markdown, JSON, and annotations.
  • Adds a pull-request-only CI job with bounded runtime and a separate summary step.
  • Documents local usage, adds focused tests, and ignores generated reports.
File Description
dokimi/​tests/​test_mutate_changed.py Tests mutation generation, source edits, selection, diff parsing, and reporting.
dokimi/​tests/​test_ci_label_triggers.py Excludes the PR-only mutation job from expected push jobs.
dokimi/​scripts/​mutate_changed.py Implements changed-line mutation testing and reports.
AGENTS.md Documents the warn-only job and local command.
.gitignore Ignores generated mutation reports.
.github/​workflows/​ci.yml Adds the mutation job and summary publication step.

Review effort: Balanced

Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
@exai-bot

exai-bot commented Nov 14, 2026 •

Copy link
Copy Markdown

OpenCodeReview: Commit 0c8e87d was not reviewed. This PR has reached its review limit (6 reviews).

CI at `0c8e87d`: 12 still running, 4 passing, 1 skipped
Check Status
Build docs (strict) still running
CodeQL (actions) still running
CodeQL (python) still running
External catalog smoke test (uxlfoundation/skills) still running
Platforms and Python versions still running
Static checks (ruff, schema, zizmor, licenses, pre-commit, bandit, secrets, changelog, schema com... still running
benchmark still running
conda-forge dependency solve (py3.11) still running
conda-forge dependency solve (py3.14) still running
mypy still running

2 more not listed.

To review this commit: re-request review from exai-bot in the Reviewers list.

<details><summary>Last review (19619fa): no findings</summary>

Open threads from the bot: 0

</details>

Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
The script has a shebang, so it is committed executable like its
siblings (EXE001). deptry skips it the way it skips the other
CI-only scripts. _edits splits into two helpers to get under the
complexity cap of 10.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
Mutants are written into one tree. A same-size rewrite within the
same second matched the cached bytecode of the previous text, so a
mutant could be judged on the original code. Drop the module's .pyc
on every write; the test forces the same mtime and expects kills.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
--max-tests ran the first 30 covering node ids by name, so a mutant
that only a later test catches was reported as survived (on intel#641's
merge, two stats.py:167 mutants its own new test kills). pytest
reads the full list from a file; -x still stops at the first failure.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
No test ran run(): an inverted survived/killed mapping, a test
that fails unmutated left in the covering set, or a lost
--fail-on-survivor exit code all kept the suite green.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
subprocess.run's timeout killed only pytest: a test's child kept
running the mutant, and on Windows the call waited on the pipes
that child held. dokimi.process.run kills the group (job object on
Windows) when the run ends or times out.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
@xaleryb

xaleryb commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
@codecov-commenter

codecov-commenter commented Nov 14, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.72%. Comparing base (f12057b) to head (0c8e87d).
⚠️ Report is 2 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #739   +/-   ##
=======================================
  Coverage   95.71%   95.72%           
=======================================
  Files         415      415           
  Lines       55346    55362   +16     
  Branches     9514     9516    +2     
=======================================
+ Hits        52977    52993   +16     
  Misses       1466     1466           
  Partials      903      903           

see 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread dokimi/tests/test_mutate_changed.py Resolved
Comment thread dokimi/tests/test_mutate_changed.py Resolved
The conda test environments have no coverage, and ci/test.py's
PYTHONSAFEPATH made pytest in the copy import the installed dokimi.
The run() tests now give the covering-test map in place of the coverage
pass; covering_tests' exclusion of failing tests gets its own test.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
@xaleryb

xaleryb commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Pushed f26652d (c36d0a4..f26652d): CI was red on our four run() tests for two reasons. The conda test environments do not install coverage (ci/config.yml leaves the coverage tools out), and in the wheel environments the inherited PYTHONSAFEPATH=1 made the copied tree import the installed dokimi, so the test file did not collect. The tests now pass the covering-test map directly and clear PYTHONSAFEPATH (as tests/test_intel_ci_conda_lock.py does); the exclusion of tests that fail unmutated has its own test with a stand-in coverage module. Checked in a conda env built from ci/conda-lock/linux-64-py3.11.txt without coverage and in a wheel-style venv: 14 passed in both; each earlier mutant still turns a test red.

Comment thread dokimi/scripts/mutate_changed.py Resolved

@antonwolfy antonwolfy 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.

Review at f26652d. The process-tree kill and the stale-bytecode fixes hold up. Two defects make the report wrong: a mutant is counted as killed when no test ran, and import-time lines are never mutated. The inline comments also cover candidate discovery, a local-run gap and test gaps that let exit-code and annotation changes through.

What I ran, on a Linux review host at f26652d:

  • make ci-local: 30 passed, 4 failed, 14 skipped; 19,764 tests passed. The failures are not the PR's. Install gitleaks hit an HTTP 504 and Scan commits failed with it. pre-commit fails on check-added-large-files for dokimi/docs/reference/cli-interface.json (501 KB), which this PR does not touch. Run tests shows 2 pytest-timeout errors in test_audit_test_quality.py, which passes alone (172 passed).
  • mutation-diff runs under ci-local and reports "no mutable changed lines", since the PR changes no package file. mypy, deptry, ruff, zizmor and schema_compat pass.
  • Reproductions for the code findings, driving run(), covering_tests and candidate_tests on a scratch git repo with real pytest and coverage runs, no model calls.
  • A mutation run of test_mutate_changed.py against 96 one-line mutants of the script: 49 pass the file. Four of them decide an exit code, an annotation level or a measurement (inline).
  • All 8 commits carry Signed-off-by.

GitHub checks red at this head: Static checks (...) and Test Linux Python 3.11. I could not reproduce either here. schema_compat passes in my run, and the 3.11 failure is a wall-clock assertion in code the PR does not touch. Keep them in view until a re-run is green.

Open for the author: @xaleryb's suggestion that a survivor whose file still has unrun candidate tests be reported as inconclusive, not survived, waits for the author. It still holds at this head: candidate_tests_not_run is listed, but such a survivor is reported as survived.

Bot points I checked that no longer apply at this head (6)
  • Copilot's point on stale bytecode from equal-size edits in the same second does not apply: _write_source deletes the module's .pyc on every write, and test_a_same_size_mutant_in_the_same_second_runs_its_own_source forces the same-mtime case.
  • Copilot's point on descendants left running after a timeout does not apply: _pytest goes through dokimi.process.run, which kills the whole process tree, and a test checks that no grandchild survives a mutant timeout.
  • exai-bot's point on the shebang and file mode (Ruff EXE001) does not apply: the script is mode 100755 at this head and ruff check passes.
  • exai-bot's point on the Windows timeout and pipe drain does not apply at this head: _pytest no longer uses subprocess.run, it uses dokimi.process.run, which kills the process tree on timeout; I checked this on Linux only.
  • exai-bot's point on deptry reporting coverage as a transitive dependency does not apply: pyproject.toml now excludes the script, and deptry passes.
  • exai-bot's point on cached bytecode running a previous mutant does not apply: the .pyc is removed on each write and restore, as above.

Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/tests/test_mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
…utation

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…, mutate import-time lines

A from-import wrapped over several lines or with a backslash now makes its test a candidate.
Exit codes other than 0 and 1 (nothing collected, usage, internal error) are reported as
errors, not kills. A line run at collection (a constant or a default) is attributed to the
file's passing tests. changed_lines reads the working tree, so uncommitted edits count.
The tests pin the exit code, the annotation level, the restore of the mutated file and the
clean exit with no mutable line; the help text says a timeout is neither killed nor survived.

Thread: PRRT_kwDOVBg7g863b_Iw
Thread: PRRT_kwDOVBg7g863eoff
Thread: PRRT_kwDOVBg7g863c2VO
Thread: PRRT_kwDOVBg7g863eofh
Thread: PRRT_kwDOVBg7g863b_Iz
Thread: PRRT_kwDOVBg7g863eofj
Thread: PRRT_kwDOVBg7g863eofo
Thread: PRRT_kwDOVBg7g863eofq
Thread: PRRT_kwDOVBg7g863eoft
Thread: PRR_kwDOVBg7g88AAAABX_OVNg
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The commits named here were pushed before this round; this commit changes no file.

Thread: PRRT_kwDOVBg7g863VktL: already fixed in 39c554c
Thread: PRRT_kwDOVBg7g863VveQ: already fixed in 39c554c
Thread: PRRT_kwDOVBg7g863Vkta: already fixed in c36d0a4
Thread: PRRT_kwDOVBg7g863VveE: already fixed in c36d0a4
Thread: PRRT_kwDOVBg7g863Vvdu: already fixed in 312bcc4
Thread: PRRT_kwDOVBg7g863V30l: already fixed in 312bcc4
Thread: PRRT_kwDOVBg7g863VveK: already fixed in 312bcc4
Thread: PRRT_kwDOVBg7g863V30r: already fixed in 312bcc4
Thread: PRRT_kwDOVBg7g863V30q: already fixed in 312bcc4
Thread: PRRT_kwDOVBg7g863ccVt: already fixed in f26652d
Thread: PRRT_kwDOVBg7g863ccVy: already fixed in f26652d
Thread: IC_kwDOVBg7g88AAAABmdXp1Q: already fixed in f26652d
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… whose tests all skip attributes no import-time line

A collection error caused by the mutant (pytest exits 2 or 4 by version) is a kill,
not an error. An import-time line is attributed to the file's tests only when one ran.
The merge-base failure names the command that failed.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov

Copy link
Copy Markdown
Contributor Author

Re: review by antonwolfy

Two defects make the report wrong: a mutant counted as killed when no test ran, and import-time lines never mutated.
Both fixed in a714bbf; the inline replies have the tests.

@napetrov

Copy link
Copy Markdown
Contributor Author

Re: comment by xaleryb

CI was red on our four run() tests for two reasons.
Both fixed in 6aea1d3: coverage is optional, PYTHONSAFEPATH cleared.

@napetrov

Copy link
Copy Markdown
Contributor Author

Re: comment by xaleryb

a survivor whose file still has unrun candidate tests could be reported inconclusive rather than survived.
Waiting on a decision before I change this: should it be inconclusive? My recommendation: yes, since no test ran, so it is not evidence of a gap. Option b keeps today's report. The thread stays open until then.

Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
…me line in a mixed file to its passing tests

Thread: PRRT_kwDOVBg7g863iVpb: fixed in this commit: a failing id with ' - ' in a parameter is matched whole.
Thread: PRRT_kwDOVBg7g863iVpe: fixed in this commit: import-time lines of a file with a failure go to its passing tests.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/scripts/mutate_changed.py Outdated
return any(line == i or line.startswith(i + " - ") for i in ids)

lines = re.findall(r"^(?:FAILED|ERROR) (.+)$", log, re.MULTILINE)
named = {i for i in ids if any(line == i or line.startswith(i + " - ") for line in lines)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix — Impact: a passing test can be reported as baseline-failing and removed from both per-test and import-time coverage, leaving its changed lines unmutated. Trigger: parameter IDs a and a] - b produce nodes tests/test_zz.py::test_f[a] and tests/test_zz.py::test_f[a] - b]; only the latter fails. Evidence: FAILED tests/test_zz.py::test_f[a] - b] - AssertionError satisfies startswith(i + " - ") for both nodes, so named contains both; ids - bad and passed - bad then discard the passing node, and run() skips mutants without remaining coverage. Match each summary line to its longest matching known node rather than independently accepting every prefix, and add a two-ID regression. Workaround: avoid colliding parameter IDs. Severity: medium because this materially corrupts the bounded advisory mutation measurement, without affecting package runtime.

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.

Fixed in 0c8e87d: _failed attributes each summary line to the longest known id it starts with. FAILED ...test_f[a] - b] - AssertionError now returns only test_f[a] - b]. tests/test_mutate_changed.py::test_a_passing_id_that_prefixes_a_failing_id_is_not_failed pins it and fails without the change.

@antonwolfy antonwolfy 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.

Review at 337f794. The changes since my last review hold up: wrapped imports are read through ast, only exit 1 is a kill, and import-time lines are mutated. One bug is left in the new ' - ' handling, and three test gaps let a change to a verdict or to the choice of mutated lines through. Two nits inline. The merge also waits for the author on @xaleryb's question.

What I ran, on a Linux review host at 337f794:

  • make ci-local: 35 passed, 0 failed, 14 skipped; 20,463 tests passed. The changed-line coverage gate passed; mutation-diff runs under ci-local and reports no mutable changed lines, since the PR changes no package file.
  • Reproductions for the code finding, calling _failed directly and with real pytest output, with no model calls.
  • A mutation run of test_mutate_changed.py against 45 one-line mutants of the script: 12 pass the file. Three decide a verdict or which lines get mutated (inline).
  • All 13 non-merge commits carry Signed-off-by.

Still open from the author: @xaleryb's suggestion that a survivor whose file still has unrun candidate tests be reported as inconclusive, not survived, waits for the author. The author handed the choice back ("waiting on a decision") and the code is unchanged.

Checked, no issues

  • Workflow wiring: needs: static, if: pull_request, the 30 and 22 minute timeouts and the flags passed to the script match the argparse flags; fetch-depth: 0 makes git merge-base origin/<base> work. .gitignore covers both output files.
  • Exit codes: 0 is survived, 1 and a collection error (2 or 4 with ERROR collecting) are killed, -1 is timeout, and 3, 5 and a bare 2 or 4 are error. I ran each.
  • Mixed files: an import-time line in a file with a failing test is credited to the tests that passed, and a file where nothing ran credits nothing.
  • Docs: AGENTS.md names the job, the script, the cap of 40 and the local command correctly. No changelog fragment is required; no generated file is committed.

Comment thread dokimi/scripts/mutate_changed.py Outdated
Comment on lines +270 to +275
def names(line: str) -> bool:
return any(line == i or line.startswith(i + " - ") for i in ids)

lines = re.findall(r"^(?:FAILED|ERROR) (.+)$", log, re.MULTILINE)
named = {i for i in ids if any(line == i or line.startswith(i + " - ") for line in lines)}
return named, [line.split(" - ", 1)[0] for line in lines if not names(line)]

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.

_failed marks a passing id as failed when it is a prefix of a failing id. I agree with exai-bot here; what I add is the run with pytest's own output. @pytest.mark.parametrize("p", ["a", "a] - b"]) gives test_f[a] (passes) and test_f[a] - b] (fails). With -rfEp pytest prints FAILED tests/test_zz.py::test_f[a] - b] - AssertionError, and _failed returns both ids:

_failed("FAILED tests/test_zz.py::test_f[a] - b] - AssertionError\n",
        {"tests/test_zz.py::test_f[a]", "tests/test_zz.py::test_f[a] - b]"})
# ({'tests/test_zz.py::test_f[a]', 'tests/test_zz.py::test_f[a] - b]'}, [])

line.startswith(i + " - ") is true for both ids (names at :271 and named at :274). covering_tests then removes the passing test from every line, so a changed line only that test covers is reported as unexecuted and never mutated, and the test is listed as failing without a mutant.

Suggested fix: attribute each summary line to the longest id that matches it, max((i for i in ids if line == i or line.startswith(i + " - ")), key=len, default=None), and add a two-id regression with the log above.

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.

Fixed in 0c8e87d: _failed attributes each summary line to the longest known id it starts with. FAILED ...test_f[a] - b] - AssertionError now returns only test_f[a] - b]. tests/test_mutate_changed.py::test_a_passing_id_that_prefixes_a_failing_id_is_not_failed pins it and fails without the change.

Comment thread dokimi/scripts/mutate_changed.py Outdated
if mb.returncode:
raise SystemExit(f"git merge-base {base} HEAD failed: {mb.stderr.strip()}")
out = subprocess.run(
["git", "diff", "-U0", "--no-color", "--diff-filter=AMR", mb.stdout.strip(), "--", f"{PKG}*.py"],

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.

Nothing pins the merge base: diffing against base itself passes all 30 tests. Mutation check: replacing mb.stdout.strip() with base in this git diff call survives. test_changed_lines_* only uses HEAD~1, where the merge base and the base tip are the same commit. In CI, --base origin/main is the tip of main as it is when the job runs. If main advanced after the PR branched, diffing against the tip makes main's later edits to the same files look like the PR's changed lines, and they get mutated and reported.

Suggestion: add a test with a base branch that gains a commit editing another line of the same file after the PR branch was cut, and assert changed_lines(<base branch>) returns only the branch's lines.

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.

Fixed in 0c8e87d: Added a case where the base branch gains a commit after the branch was cut; changed_lines('main') returns only the branch's line. Diffing against base fails it. tests/test_mutate_changed.py::test_changed_lines_diffs_against_the_merge_base_not_the_base_tip pins it and fails without the change.

return "survived"
if rc == -1:
return "timeout"
if rc == 1 or (rc in (2, 4) and _COLLECTION_ERROR in log):

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.

Only exit 2 is tested as a collection-error kill. Mutation check: rc in (4,) and rc in (2, 3, 4) both pass the full file. The docstring says the exit code varies by pytest version, so dropping 2 (an older pytest) or adding 3 would turn broken-import kills into error, or an internal error into a kill, with no test red. That is the killed/error verdict --fail-on-survivor and the summary follow from.

Suggestion: a table test of _mutant_status: (4, "ERROR collecting x") is killed, (3, "ERROR collecting x") is error, (2, "") and (4, "") are error, (5, "") is error.

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.

Fixed in 0c8e87d: Added a table test of _mutant_status over exit codes 0, -1, 1, 2, 3, 4, 5 with and without ERROR collecting. rc in (4,) fails it. tests/test_mutate_changed.py::test_mutant_status_by_exit_code pins it and fails without the change.

return by_file, failing, tests[i:]
data_file.unlink(missing_ok=True)
rc, log = _pytest(
work / "dokimi", ["-rfEp", "--cov=dokimi", "--cov-context=test", "--cov-report=", test], left,

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.

No test checks that the coverage pass asks for PASSED ids. Mutation check: changing -rfEp to -rfE passes all 30 tests. test_covering_tests_* stubs _pytest and ignores its arguments, and the one real-pytest test has no failing test. Without p, passed is empty on a real run, so in a file with one failing test the import-time constants and defaults are credited to no test and silently dropped from mutation. This is the case the latest commit fixed.

Suggestion: run covering_tests for real on a file with one failing and one passing test and an import-time FLAG = True, and assert line 1 is credited to the passing test.

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.

Fixed in 0c8e87d: Added a real covering_tests run on a file with one passing and one failing test and an import-time FLAG; line 1 is credited to the passing test. -rfE fails it. tests/test_mutate_changed.py::test_the_coverage_pass_asks_for_passed_ids pins it and fails without the change.

Comment thread dokimi/scripts/mutate_changed.py Resolved
Comment thread dokimi/scripts/mutate_changed.py Resolved
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

@antonwolfy antonwolfy 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.

I pushed a commit per nit, up to 19619fa, which fix the two nits from my review:

  • 19619fa: dokimi/tests/test_mutate_changed.py:121: the summary test now has two killed mutants and one error mutant, and asserts 2 killed and 1 errors; printing the killed count or a hardcoded 0 as errors now fails it.
  • 56e1109: dokimi/scripts/mutate_changed.py:11: step 1 of the docstring now says the working tree is diffed against the merge base of BASE and HEAD.

make ci-local at 19619fa: 35 passed, 0 failed, 14 skipped.

Not approving yet. Still open: the bug and the three risks from my review; @xaleryb on report a survivor whose file still has unrun candidate tests as inconclusive, not survived.

…CII paths

Fixes:
- _failed credited a passing id that prefixes a failing id holding ' - '
  (test_f[a] vs test_f[a] - b]) with the failure; the longest match wins.
- changed_lines dropped a package file with a non-ASCII path (quoted by git).
- The coverage pass kept only a capped log, so failures past the cap were
  not seen; the pytest output is now captured whole.
Tests pin the merge-base diff, the exit-code table of _mutant_status and
the -rp passed ids of a real coverage run.
Fixes:
- _failed credited a passing id that prefixes a failing id holding ' - '
  (test_f[a] vs test_f[a] - b]) with the failure; the longest match wins.
- A failure message can complete a longer passing id
  (test_f[a] - Failed: b]); ids with a PASSED line are tried last.
- changed_lines dropped a package file with a non-ASCII path (quoted by git).
- The coverage pass kept only a capped log, so failures past the cap were
  not seen; the pytest output is now captured whole.
Tests pin the merge-base diff, the exit-code table of _mutant_status and
the -rp passed ids of a real coverage run.

Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
@vchamarthi

Copy link
Copy Markdown
Contributor

Pushed 1 commit to ci-diff-mutation (19619fa..0c8e87d).

  • _failed matches each summary line to the longest known id.
  • _failed matches each summary line to the longest known id, preferring ids with no PASSED line.
  • changed_lines passes -c core.quotePath=false to git diff.
  • _pytest captures the whole output (capture_limit=None).
  • _failed prefers ids without a PASSED line among those a summary line matches.

Threads: 5 fixed, 0 answered as not a defect.
Run: the full suite with main merged in, 20531 passed.

This branch has not been deployed

No deployments
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.

7 participants