[Bug]: [mutate] Enum case values are never mutation-tested #1978
Description
What Happened
The value of a string-backed enum case is never effectively mutation-tested, even when tests check it. Changing 'paid' in the example below would break the test, but --mutate never reports it. There are four independent reasons, all in pestphp/pest-plugin-mutate:
- The mutator is dropped. The mutator for non-empty strings,
NotEmptyStringToEmpty, is not in a default set. When it is requested explicitly,MutationGenerator::filterMutators()drops it, because it only keeps mutators found inMutatorMap, which is built from the default sets. So no mutation is created at all. mutates()and--classignore enums.MutationGenerator::doesNotContainClassToMutate()matches onlyclassandtrait, somutates(Status::class)creates no mutation for an enum.- The case declarations have no line coverage. The mutation is then reported as uncovered, without running any test. With
--covered-only, the enum file is skipped entirely. - Separately, on Linux and macOS, when the covered files share only
/, every mutation is reported as uncovered (basePathof--coverage-phpis empty and the paths stay relative).
How to Reproduce
enum Status: string
{
case Paid = 'paid';
}it('stores the paid status', function () {
expect(Status::Paid->value)->toBe('paid');
});Run pest --mutate --mutator=StringNotEmptyStringToEmpty: no mutation is created for 'paid'. With mutates(Status::class) in the test file, no mutation is created either.
Sample Repository
mechgw/pest-plugin-mutate@5.x...fix/enum-case-mutations (fixtures and tests for every case)
Pest Version
5.3.2 (pest-plugin-mutate 5.0.2)
PHP Version
8.4.21
Operation System
Linux, Windows
Notes
I would like to fix this in a series of small pull requests to pestphp/pest-plugin-mutate, each one useful on its own:
- fix: generate mutations of mutators requested outside the default sets pest-plugin-mutate#48: keep the mutators requested outside the default sets (point 1). This is a general fix: it applies to any such mutator, not only to enums.
- fix: restore coverage paths when the covered files share only the root directory pest-plugin-mutate#49: restore the coverage paths when the covered files share only
/(point 4). Not related to enums; I found it while running the tests on Linux. - fix: mutate the enums given to mutates() or --class pest-plugin-mutate#50: let
mutates()and--classselect enums (point 2). It changes the same regular expression as Fix (create mutations for uncovered classes): adjust regex to ignore classnames which are prefixed with a classname to create mutations for pest-plugin-mutate#22; the two changes do not contradict each other, and I will rebase Uppercase values in a dataset wrongly displayed in the output #50 on top of Fix typos #22 if Fix typos #22 is merged first. - last: verify the mutations of enum case values (point 3).
The last part is the largest, so I would rather hear your opinion before opening it. The approach is:
- for a mutated case value, the plugin runs the tests whose code references the case (
Status::Paid,self::Paid,static::Paid, andfrom(),tryFrom(),cases()as a reference to every case), including the hooks and the helper functions of the same test file. If those tests do not detect the mutation, it falls back to all the tests originally selected; - the result comes from the JUnit report of that run: a failing test means tested, a passing run means untested, and a missing report or an unknown exit code is reported as uncovered with the reason printed, never as tested;
- with
--covered-only, a case value is mutated only when the selected tests reference it. Cases referenced only from helpers in other files or from closures stored in variables are skipped, and this limitation is covered by a test; - in parallel runs, the workers record the tests they execute, so the plugin knows which tests were actually selected.
The complete change, with tests on Linux and Windows, is on the branch linked above. It is a large diff, so please read it only as a preview. I will split it and adapt it to whatever direction you prefer.
Would you accept this approach, or would you rather solve point 3 differently?
Activity
mechgw commentedon Oct 18, 2026
Opened pestphp/pest-plugin-mutate#50 for point 2 without waiting for pestphp/pest-plugin-mutate#22: both change the same line, but they do not contradict each other, and #50 describes what changes once #22 is merged.
theofidry commentedon Oct 20, 2026
Out of curiosity did you find meaningful tests and mutations from this?
Because:
Would only be writing different a native PHP construct, not testing behaviour. It would be akin to test that this is not a class or whether it's final.