Skip to content

test: kill eight surviving mutants in formatter, parser and blocker analysis - #52

Merged
fohara merged 1 commit into
mainfrom
tests/kill-mutation-survivors-formatter-parser
Aug 11, 2026
Merged

fohara merged 1 commit into
mainfrom
tests/kill-mutation-survivors-formatter-parser

Conversation

@fohara

@fohara fohara commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Eight new tests in lash-core. No production code changes.

Each one covers a conditional whose two branches produced the same result for the existing suite, so inverting the condition left every test green.

Where these came from

A mutation testing run against 0.3.1 sampled 400 mutants out of 3528 and left 86 survivors once each had been re-confirmed against the full suite. These eight are the ones in three files I thought were worth acting on first.

Formatter

normalize_whitespace already had a test, but it calls the helper directly. Nothing exercised the flag that decides whether the helper runs at all, so inverting the flag passed. Two nearby conditions were also unasserted: the trim that stops a formatted file ending on a blank line, and the guard that stops a synthesized tasks section re-emitting a description the source already has.

Parser

A blank line is allowed between a checkbox and its annotation block. Asserting the parsed values does not pin that, because the annotations are recovered further down either way, which is why the existing test still passes with the condition inverted. What actually changes is annotation_line_count, which drops to zero.

That count is how the write paths find where a task's annotations end, and a wrong count there is how lash add used to splice a new task into the middle of the previous task's block (#49). The new test asserts the count rather than the values.

Blocker analysis

find_blocker_chains was tested on task1 to task2 to task3. That chain is only one hop deep: the walk pushes task3 and stops because task3 is not itself blocked, so the loop's own "did I find anything this pass" guard is never what ends it. Inverting that guard produced the same two-element chain and nothing failed. A fourth node forces a second hop, which is the first case that tells them apart.

The per-blocker status word and the chains section in BlockerReport::format were unasserted too, so two tests now cover it.

Verification

Every test was checked in both directions. Applied to its mutation it fails, and without the mutation it passes. The workspace suite, clippy and fmt are all clean.

One survivor left alone

In the parser's blank-line branch, } else if !annotation_lines.is_empty() { break; } is followed by an unconditional break. Both paths break, so nothing can observe the difference. That is an equivalent mutant rather than a gap in coverage. The redundant branch could go, but that is a production change and does not belong in a test PR.


Found with Flawd, the mutation testing tool we build at Fixture. It is available now, with a 14-day evaluation, at https://fixture.dev/flawd.

…nalysis

Found by mutation-testing lash 0.3.1 with Flawd 0.8.0 (400 mutants sampled
from 3528, per-test targeting, every survivor re-confirmed against the full
suite). These eight survivors are all cases where a conditional's two branches
produce the same test outcome, so inverting it changed nothing that any test
observed.

Three sit in code 0.3.1 just changed:

- formatter: `normalize_whitespace` was only ever exercised through the helper
  it gates, never through the flag itself, so inverting the flag passed. The
  trailing-blank-line trim and the "don't re-emit a description that is
  already in the source" guard were likewise unasserted.
- parser: a blank line between a checkbox and its annotation block. The parsed
  values did not pin this — annotations are recovered downstream either way —
  but `annotation_line_count` silently drops to 0, and that count is what the
  write paths use to find where a task's annotations end. A wrong count there
  is how `lash add` splices a task into a previous task's block, the data-loss
  class fixed in 0.3.1 (#49).
- blocker analysis: `find_blocker_chains` was only tested on a chain one hop
  deep, where the walk exits on the "current task is not itself blocked" check
  before its own progress guard is ever what stops it. A four-node chain
  distinguishes them. The report's per-blocker status word and its chains
  section were also unpinned.

Each test was verified to fail against its mutation and pass without it.

No production code changed.

One further survivor is left deliberately untested: in the parser's blank-line
branch, `} else if !annotation_lines.is_empty() { break; }` is followed by an
unconditional `break`, so both paths break identically and no test can observe
the difference. It is an equivalent mutant, not a gap; the redundant branch
could be removed but that is a change to production code and not this commit's
business.
@fohara
fohara merged commit 6c4ca86 into main Aug 11, 2026
21 checks passed
@fohara
fohara deleted the tests/kill-mutation-survivors-formatter-parser branch August 11, 2026 20:48
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.

1 participant