Skip to content

Bug: #[CoversNothing] is ignored in Cest tests with PHPUnit 13 / php-code-coverage 12 #6940

@alex-lar-newgen-vision

Description

@alex-lar-newgen-vision

##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().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions