Repository navigation
Option to suppress code coverage target validation #6472
Description
References: infection/infection#2440 and infection/infection#2848
Quick summary:
Infection is a mutation testing tool. It makes small changes to a project's source code (such as inverting the condition inside an if), runs the test suite and checks whether the tests still pass.
Before doing any of this, Infection needs to do an initial run of the test suite to gather coverage information. It does this by generating a new, temporary PHPUnit config (based on the project's config) and invoking PHPUnit.
Infection supports a --filter option that allows users to specifcy a list of files or directories to consider. It will only generate mutations for those files.
If this option is used, the generated PHPUnit config is modified to only list the relevant files or directories in <source><include>.
Problem:
If a project uses #[CoversClass()]/#[UsesClass()], PHPUnit (since version 12 I believe) will run some validation on those attributes in
phpunit/src/Runner/CodeCoverage.php
Lines 228 to 256 in 197fc34
| if ($covers instanceof TargetCollection) { | |
| $result = $this->codeCoverage->validate($covers); | |
| if ($result->isFailure()) { | |
| assert($result instanceof ValidationFailure); | |
| EventFacade::emitter()->testTriggeredPhpunitWarning( | |
| $this->test->valueObjectForEvents(), | |
| $result->message(), | |
| ); | |
| $append = false; | |
| } | |
| } | |
| if ($uses instanceof TargetCollection) { | |
| $result = $this->codeCoverage->validate($uses); | |
| if ($result->isFailure()) { | |
| assert($result instanceof ValidationFailure); | |
| EventFacade::emitter()->testTriggeredPhpunitWarning( | |
| $this->test->valueObjectForEvents(), | |
| $result->message(), | |
| ); | |
| $append = false; | |
| } | |
| } |
Because the full test suite is being run but the config
<source> includes only a small subset of source files, an exception is thrown in https://github.com/sebastianbergmann/php-code-coverage/blob/3c97001bca11db4a3e6eee52f6e6924617364c3a/src/Target/Mapper.php#L82 and a "X is not a valid target for code coverage" warning is emitted.This, combined with
failOnPhpunitWarning="true" (the default and probably generally advisable setting), leads to a failed initial test run, which in turn leads Infection to abort its run.
Possible solution:
A number of options were explored in the issues I linked above. The TLDR is that this is difficult to properly handle on Infection's end.
The most straightforward solution would be to add a "don't validate code coverage targets" config option to PHPUnit.
Is that something PHPUnit would consider adding?
Activity
theofidry commentedon Jan 21, 2026
Subscribing to the issue, I need to have a proper look at it.
sebastianbergmann commentedon Apr 2, 2026
I looked into this and it is feasible to implement entirely within PHPUnit, no changes to php-code-coverage would be required.
The idea would be to add a
--no-coverage-target-validationCLI option.When used, PHPUnit would skip the
validate()calls inCodeCoverage::stop()and also neutralize the$covers/$usesparameters before passing them to php-code-coverage'sstop()method. This second part is important because php-code-coverage'sMapper::mapTarget()throwsInvalidCodeCoverageTargetExceptionfor targets that are not in the source map, and that exception is not caught in themapTargets()→append()call chain — only in the separatevalidate()path. So simply skipping validation while still passing through unresolvable targets would cause a fatal error.The trade-off: when target validation is disabled, target-based coverage filtering (
#[CoversClass]/#[UsesClass]) is bypassed entirely. All executed lines are recorded as coverage, not just lines belonging to the covered/used classes. For the Infection use case this should be fine, since Infection needs line-level execution data to determine which tests to run for each mutation, regardless of coverage target attributes.A CLI-only option (no XML configuration setting) makes sense here since this is intended for tool integration, not as a project-level setting.
@theofidry What do you think? I would only implement this if Infection will actually use this. If you decide not to use this then I will not implement this as I do not see a use case for this outside of integration with Infection.
theofidry commentedon Apr 2, 2026
@sebastianbergmann I'll have a proper read and look tomorrow/weekend. It's been bugging me for a while and it's high time to address it
theofidry commentedon Apr 4, 2026
So to put recap in my own words on what the problem: in diff mode infection restricts the sources to cover when executing the initial tests, to get the coverage report.
While doing so, if the coverage metadata is marked as required by PHPUnit and tests that cover an excluded piece of source code, then the test is marked as risky.
Doesn't that mean that the source code may end up being covered by a lot more tests as a result? So whilst this would be an efficient fix, this could result in serious performance degradations. It could also cause confusion to the user because that means a greater difference between how the user would use PHPUnit and how infection instruments it (which is already a problem to a degree, at least a source of confusion/surprises).
But I feel like I'm not being super helpful here, and maybe the problem caused by the fix are overblown. Maybe we could try to brainstorm this during a zeegme?
fix(PHPUnit): Do not modify the inital coverage configuration (#3043)
fix(PHPUnit): Do not modify the inital coverage configuration (infect…