fix: name the filter that emptied the mutation set - #162
Merged
Merged
Conversation
scolladon
force-pushed
the
fix/no-mutations-diagnostic
branch
from
August 25, 2026 11:56
6d6bb8b to
f614b60
Compare
|
Preview build for this pull request: sf plugins install https://pkg.pr.new/apex-mutation-testing@cc98aed |
Performance Comparison (same runner)Stable
|
This was referenced Aug 25, 2026
|
Shipped in release $ sf plugins install apex-mutation-testing@latest-rc
# Or
$ sf plugins install apex-mutation-testing@v1.9.0💡 Enjoying apex-mutation-testing? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Explain your changes
Whenever a run produced no mutants, the command reported the same sentence:
That count is the class-wide covered-line total, taken before
--linesnarrowing. So the message blamed the mutation operators when the real cause was almost always one of the user's own filters. In #161 the reporter ran-l 90-100against a class whose covered lines all sit outside that window, and got exactly this — a dead end pointing at the wrong thing.diagnoseNoMutations()now replays the line filters over the covered set and names the first filter that emptied it, so the message names the flag that has to change:--linesexcluded every covered lineNone of the 63 covered line(s) fall within the requested --lines range (90-100).All 12 candidate line(s) were excluded by --skip-patterns.4 line(s) are eligible but no enabled mutator matched them.4 eligible line(s) but no mutable patterns found.The last row changed too. It used to print the class-wide covered count; it now prints eligible lines, because on a narrowed run the class-wide figure is precisely the misleading number this issue is about. With no flags set the two coincide, so an unfiltered run reports the same number it always did.
Two supporting changes fell out of it:
The mutator filter is asked, not guessed.
mutatorFilterActiveis now answered bymutantGenerator.mutatorFilterNarrows()instead of being re-derived in the service. Two shapes leave the full registry in place and must never be blamed for an empty result: an empty list ({"mutators": {"include": []}}, whichfilterRegistry()treats as no filter at all) and a list whose names match no mutator (a typo in--exclude-mutators). Both previously produced advice that could not help.Config-file shape is validated.
readConfigFile()ended inJSON.parse(content) as MutationTestingConfig— an unchecked cast, so the declared types were a promise the file never had to keep.lineswas the dangerous field: bothvalidate()andparseLineRanges()walk it withfor...of, andfor...ofiterates a string character by character, so a scalar where an array belongs was not a type error but a different, silently-accepted input.The
"42"row is why the check exists: the run mutates lines 4 and 2, reports a score for them, and warns about nothing. Every other array-typed field is consumed by.map(), which strings lack, so those already failed fast —lineswas the only silent one. All fields are now checked, since which ones happen to fail loudly is an accident of how each consumer is written.The check is a zod schema, and
MutationTestingConfigisz.infer<typeof configSchema>— one declaration, so the type and the runtime check cannot drift. This addszodas a direct dependency, but no install weight: it is already in the graph via@salesforce/coreand stays deduped at 4.4.3. It does have to remain pinned to that version to keep the dedupe, which is the one ongoing cost.zod's own error text never reaches the user — the messages stay in the bundle. One improvement fell out of it: zod reports the failing path, so a bad array element names its index rather than just its field:
Ten user-facing
ConfigReadererrors also moved out of hardcoded strings and into the message bundle, so a wording change is caught by the bundle test rather than drifting. Internal invariant errors (MetadataContainer did not return an ID,mutateMany: overlapping token ranges, thepollOptions.*guards) were deliberately left hardcoded — they are programmer errors, not user-actionable messages, and putting them in a user-facing catalogue would imply they are.Not everything reported in #161 was a plugin bug. The method the reporter expected mutants for is genuinely uncovered by the test class they ran: its only caller early-returns when no contact carries a
ReportsToId. The plugin was right to skip it — it just explained itself badly.Does this close any currently open issues?
closes #161
Unit and integration tests only: the change is entirely in diagnosis and validation logic reachable without an org, and the NUT suite already covers the command shell end to end.
Any particular element that can be tested locally
Against any org, with a class whose covered lines sit outside the requested window:
Previously reported
N line(s) covered but no mutable patterns found; now names the--lineswindow and echoes the ranges as typed.The config-file path:
Previously ran against lines 4 and 2 with no warning; now rejects the shape and names the field.
Any other comments
Rebased onto
mainnow that #160 has merged. Dependencies upgraded in the same pass (@salesforce/apex-node,@salesforce/core,@oclif/plugin-help,oclif);npm outdatedis clean. zod stays deduped at 4.4.3 under the bumped@salesforce/core.Two findings worth their own follow-ups, both pre-existing and out of scope here:
npm run test:mutationis broken onmain. Stryker 10.0.0 callsts.parseConfigFileTextToJson, which TypeScript 7 removed. Running it needs a temporarynpm install --no-save typescript@6.0.3. Note thattest:mutation:incrementalmasks this: its|| echo 'No source files changed'fires when Stryker crashes, printing a clean-looking skip and exiting 0.await expect(promise).rejects.toThrow('some message')passes when the promise rejects withundefined. That let several gutted-code-path mutants survive assertions that looked airtight; the tests here now assertrejects.toBeInstanceOf(Error)alongside the message. The hazard applies to everyrejects.toThrowin the suite.Mutation testing on the changed hunks: 182/184 killed (98.91%). The two survivors are proven equivalents, documented inline at their sites — a
[].some(...)short circuit and a?? []default that no reachable code path can observe. Neither carries aStryker disable; a stale one at the first of those sites was found to be hiding four genuinely-killed mutants and was removed.