Skip to content

fix: name the filter that emptied the mutation set - #162

Merged
scolladon merged 8 commits into
mainfrom
fix/no-mutations-diagnostic
Aug 25, 2026
Merged

scolladon merged 8 commits into
mainfrom
fix/no-mutations-diagnostic

Conversation

@scolladon

@scolladon scolladon commented Aug 24, 2026 •

Copy link
Copy Markdown
Owner

Explain your changes


Whenever a run produced no mutants, the command reported the same sentence:

No mutations could be generated for 'PersonDataService'. 63 line(s) covered but no mutable patterns found.

That count is the class-wide covered-line total, taken before --lines narrowing. 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-100 against 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:

Cause Message
--lines excluded every covered line None of the 63 covered line(s) fall within the requested --lines range (90-100).
skip patterns matched every in-range line All 12 candidate line(s) were excluded by --skip-patterns.
a mutator filter left nothing enabled 4 line(s) are eligible but no enabled mutator matched them.
nothing narrowed the set 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. mutatorFilterActive is now answered by mutantGenerator.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": []}}, which filterRegistry() 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 in JSON.parse(content) as MutationTestingConfig — an unchecked cast, so the declared types were a promise the file never had to keep. lines was the dangerous field: both validate() and parseLineRanges() walk it with for...of, and for...of iterates a string character by character, so a scalar where an array belongs was not a type error but a different, silently-accepted input.

{"lines": "42"}       → accepted → allowedLines {4, 2}   ← silently mutates the wrong lines
{"lines": "90-100"}   → rejected → "Invalid line range '-'"
{"lines": ["90-100"]} → accepted → allowedLines {90…100}

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 — lines was 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 MutationTestingConfig is z.infer<typeof configSchema> — one declaration, so the type and the runtime check cannot drift. This adds zod as a direct dependency, but no install weight: it is already in the graph via @salesforce/core and 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:

Invalid entry at 'mutators.include.1' in config file '.mutation-testing.json': expected a string, found number.

Ten user-facing ConfigReader errors 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, the pollOptions.* 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 tests added to cover the fix.
  • NUT tests added to cover the fix.
  • E2E tests added to cover the fix.

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:

node ./bin/run.js apex mutation test run -o <org> -c <Class> -t <TestClass> -l 9000-9100

Previously reported N line(s) covered but no mutable patterns found; now names the --lines window and echoes the ranges as typed.

The config-file path:

echo '{"lines": "42"}' > .mutation-testing.json
node ./bin/run.js apex mutation test run -o <org> -c <Class> -t <TestClass>

Previously ran against lines 4 and 2 with no warning; now rejects the shape and names the field.

Any other comments


Rebased onto main now that #160 has merged. Dependencies upgraded in the same pass (@salesforce/apex-node, @salesforce/core, @oclif/plugin-help, oclif); npm outdated is 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:mutation is broken on main. Stryker 10.0.0 calls ts.parseConfigFileTextToJson, which TypeScript 7 removed. Running it needs a temporary npm install --no-save typescript@6.0.3. Note that test:mutation:incremental masks 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 with undefined. That let several gutted-code-path mutants survive assertions that looked airtight; the tests here now assert rejects.toBeInstanceOf(Error) alongside the message. The hazard applies to every rejects.toThrow in 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 a Stryker disable; a stale one at the first of those sites was found to be hiding four genuinely-killed mutants and was removed.

@scolladon
scolladon force-pushed the fix/no-mutations-diagnostic branch from 6d6bb8b to f614b60 Compare August 25, 2026 11:56
@github-actions

Copy link
Copy Markdown

Preview build for this pull request:

sf plugins install https://pkg.pr.new/apex-mutation-testing@cc98aed

@github-actions

Copy link
Copy Markdown

Performance Comparison (same runner)

Stable

Benchmark Base PR Ratio Change
antlr-lex-small 8354 7527 1.11 -11.0%
antlr-parse-small 127 127 1.00 +0%
antlr-lex-medium 1794 1614 1.11 -11.2%
antlr-parse-medium 25 25 1.00 +0%
antlr-lex-large 546 496 1.10 -10.1%
antlr-parse-large 10 10 1.00 +0%
pipeline-small-compute-mutations 101 126 0.80 +19.8%
pipeline-small-type-discovery 161 160 1.01 -0.6%
pipeline-medium-compute-mutations 29 30 0.97 +3.3%
pipeline-medium-type-discovery 37 37 1.00 +0%
pipeline-large-compute-mutations 10 10 1.00 +0%
pipeline-large-type-discovery 12 12 1.00 +0%
pipeline-apply-all-mutations 3 3 1.00 +0%
antlr-lex-small (mean) 0.1197ms 0.1328ms 1.11 +10.9%
antlr-parse-small (mean) 7.8539ms 7.8969ms 1.01 +0.5%
antlr-lex-medium (mean) 0.5574ms 0.6194ms 1.11 +11.1%
antlr-parse-medium (mean) 39.3603ms 39.5179ms 1.00 +0.4%
antlr-lex-large (mean) 1.8328ms 2.015ms 1.10 +9.9%
antlr-parse-large (mean) 100.4434ms 101.338ms 1.01 +0.9%
pipeline-small-compute-mutations (mean) 9.9196ms 7.9513ms 0.80 -19.8%
pipeline-small-type-discovery (mean) 6.2007ms 6.2373ms 1.01 +0.6%
pipeline-medium-compute-mutations (mean) 34.0879ms 33.039ms 0.97 -3.1%
pipeline-medium-type-discovery (mean) 27.0486ms 27.2888ms 1.01 +0.9%
pipeline-large-compute-mutations (mean) 100.1066ms 102.0351ms 1.02 +1.9%
pipeline-large-type-discovery (mean) 80.0787ms 80.013ms 1.00 -0.1%
pipeline-apply-all-mutations (mean) 321.0909ms 328.4589ms 1.02 +2.3%

@scolladon
scolladon merged commit dd69ee2 into main Aug 25, 2026
22 checks passed
@scolladon
scolladon deleted the fix/no-mutations-diagnostic branch August 25, 2026 12:08
@github-actions

Copy link
Copy Markdown

Shipped in release v1.9.0.
Version v1.9.0 will be assigned to the latest npm channel soon
Install it using either v1.9.0 or the latest-rc npm channel

$ sf plugins install apex-mutation-testing@latest-rc
# Or
$ sf plugins install apex-mutation-testing@v1.9.0

💡 Enjoying apex-mutation-testing?
Your contribution helps us provide fast support 🚀 and high quality features 🔥
Become a sponsor 💙
Happy zombies detection!

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.

Missleading error message when code is not covered | Mutations are not being invoked on a certain method

1 participant