##Bug: #[CoversNothing] is ignored in Cest tests with PHPUnit 13 / php-code-coverage 12
Environment
codeception/codeception: ^5.3.5
phpunit/phpunit: ^13.0
phpunit/php-code-coverage: ^12.0
Steps to reproduce
use PHPUnit\Framework\Attributes\CoversNothing;
#[CoversNothing]
class SomeCest
{
public function someTest(\AcceptanceTester $I): void
{
// ...
}
}
Run: vendor/bin/codecept run --coverage
Expected: test does not contribute to code coverage.
Actual: TypeError crash — or, if getLinesToBeCovered() returns [],
the test silently contributes to coverage as if the attribute were absent.
Root cause (two-part)
Part 1 — Cest::getLinesToBeCovered() does not handle CoversNothing
PHPUnit\Metadata\Api\CodeCoverage::coversTargets() in PHPUnit 13 handles only
CoversClass, CoversMethod, CoversTrait, etc.
It does not handle #[CoversNothing] — it simply returns an empty TargetCollection.
The correct PHPUnit 13 API for CoversNothing is shouldCodeCoverageBeCollectedFor():
// PHPUnit 13 — PHPUnit\Metadata\Api\CodeCoverage
public function shouldCodeCoverageBeCollectedFor(TestCase $test): bool
{
if ($parser->forClass($test::class)->isCoversNothing()->isNotEmpty()) {
return false;
}
return true;
}
But Cest::getLinesToBeCovered() never calls it, so CoversNothing is lost.
Part 2 — CodeCoverage trait does not handle false return value for php-code-coverage ≥ 12
Historically getLinesToBeCovered() returned false for @coversNothing.
In Feature/CodeCoverage.php, when php-code-coverage >= 12, the result is passed
directly to TargetCollection::fromArray(), which does not accept false:
Proposed fix
1. src/Codeception/Test/Cest.php — check CoversNothing before delegating to coversTargets():
public function getLinesToBeCovered(): array|bool
{
if (PHPUnitVersion::series() < 10) {
return TestUtil::getLinesToBeCovered($this->testClass, $this->testMethod);
}
$metadata = \PHPUnit\Metadata\Parser\Registry::parser()
->forClassAndMethod($this->testClass, $this->testMethod);
if ($metadata->isCoversNothing()->isNotEmpty()) {
return false;
}
if (version_compare(CodeCoverageVersion::id(), '12', '>=')) {
return (new CodeCoverage())->coversTargets($this->testClass, $this->testMethod)->asArray();
}
return (new CodeCoverage())->linesToBeCovered($this->testClass, $this->testMethod);
}
2. src/Codeception/Test/Feature/CodeCoverage.php — handle false before calling TargetCollection::fromArray():
if (version_compare(CodeCoverageVersion::id(), '12', '>=')) {
$tcClass = 'SebastianBergmann\\CodeCoverage\\Test\\Target\\TargetCollection';
if (class_exists($tcClass) && method_exists($tcClass, 'fromArray')) {
if ($linesToBeCovered === false) {
$codeCoverage->stop(false, $status);
return;
}
$linesToBeCovered = $tcClass::fromArray($linesToBeCovered);
$linesToBeUsed = $tcClass::fromArray($linesToBeUsed);
}
}
$codeCoverage->stop(true, $status, $linesToBeCovered, $linesToBeUsed);
Both changes are needed: the first makes CoversNothing detectable again,
the second prevents the TypeError crash when the false value reaches fromArray().
##Bug:
#[CoversNothing]is ignored in Cest tests with PHPUnit 13 / php-code-coverage 12Environment
codeception/codeception: ^5.3.5phpunit/phpunit: ^13.0phpunit/php-code-coverage: ^12.0Steps to reproduce
Run:
vendor/bin/codecept run --coverageExpected: test does not contribute to code coverage.
Actual:
TypeErrorcrash — or, ifgetLinesToBeCovered()returns[],the test silently contributes to coverage as if the attribute were absent.
Root cause (two-part)
Part 1 —
Cest::getLinesToBeCovered()does not handleCoversNothingPHPUnit\Metadata\Api\CodeCoverage::coversTargets()in PHPUnit 13 handles onlyCoversClass,CoversMethod,CoversTrait, etc.It does not handle
#[CoversNothing]— it simply returns an emptyTargetCollection.The correct PHPUnit 13 API for
CoversNothingisshouldCodeCoverageBeCollectedFor():But
Cest::getLinesToBeCovered()never calls it, soCoversNothingis lost.Part 2 —
CodeCoveragetrait does not handlefalsereturn value for php-code-coverage ≥ 12Historically
getLinesToBeCovered()returnedfalsefor@coversNothing.In
Feature/CodeCoverage.php, whenphp-code-coverage >= 12, the result is passeddirectly to
TargetCollection::fromArray(), which does not acceptfalse:Proposed fix
1.
src/Codeception/Test/Cest.php— checkCoversNothingbefore delegating tocoversTargets():2.
src/Codeception/Test/Feature/CodeCoverage.php— handlefalsebefore callingTargetCollection::fromArray():Both changes are needed: the first makes
CoversNothingdetectable again,the second prevents the
TypeErrorcrash when thefalsevalue reachesfromArray().