Skip to content

Option to suppress code coverage target validation #6472

Open
infection/infection
#3043

Description

@AegirLeet

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

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

  1. theofidry commented on Jan 21, 2026

    @theofidry
    Contributor

    Subscribing to the issue, I need to have a proper look at it.

  2. sebastianbergmann commented on Apr 2, 2026

    @sebastianbergmann
    Owner

    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-validation CLI option.

    When used, PHPUnit would skip the validate() calls in CodeCoverage::stop() and also neutralize the $covers/$uses parameters before passing them to php-code-coverage's stop() method. This second part is important because php-code-coverage's Mapper::mapTarget() throws InvalidCodeCoverageTargetException for targets that are not in the source map, and that exception is not caught in the mapTargets() → append() call chain — only in the separate validate() 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.

  3. theofidry commented on Apr 2, 2026

    @theofidry
    Contributor

    @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

  4. theofidry commented on Apr 4, 2026

    @theofidry
    Contributor

    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.


    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.

    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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions