Skip to content

Incorrect caching of line coverage with UsesClassesThatExtendClass #1130

@no-simpler

Description

@no-simpler
Q A
PHPUnit version 12.5.3
PHP version 8.4.15
Installation Method Composer

Summary

When using #[UsesClassesThatExtendClass(BaseHelper::class)], PHPUnit caches coverage data (\SebastianBergmann\CodeCoverage\StaticAnalysis\AnalysisResult). This includes \SebastianBergmann\CodeCoverage\StaticAnalysis\Class_ that contains absolute file path in $file.

Sometimes tests may be run in multiple environments but in the same filesystem (e.g., against locally installed PHP and inside Docker container from compose.yaml). This is common in developer environments. In this case first run (that creates cache) stores absolute paths from one environment, and subsequent run reads cache and expects absolute paths from another environment.

This affects specifically UsesClassesThatExtendClass, as this attribute is effectively not taken into account in one of the environments.

How to reproduce

Minimal project I managed to achieve is below.

Requirement:

  • PHP 8.4 available on local machine. (Because we need to run tests twice: in Docker and locally.)

composer.json

{
    "name": "bug/repro",
    "require": {
        "php": ">=8.4"
    },
    "require-dev": {
        "phpunit/phpunit": "^12.5.3"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    }
}

phpunit.dist.xml

<?xml version="1.0" encoding="UTF-8"?>

<!-- https://docs.phpunit.de/en -->
<phpunit
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
    colors="true"
    failOnAllIssues="true"
    bootstrap="vendor/autoload.php"
    cacheDirectory="var/cache/phpunit"
    requireCoverageMetadata="true"
    beStrictAboutCoverageMetadata="true"
>
    <testsuites>
        <testsuite name="full">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>src</directory>
        </include>
    </source>
    <coverage>
        <report>
            <text outputFile="php://stdout" showOnlySummary="true"/>
        </report>
    </coverage>
</phpunit>

compose.yaml

services:
  app:
    build:
      context: .
      dockerfile_inline: |
        FROM php:8.4-fpm-bullseye

        # Install git and unzip (required for Composer)
        RUN apt-get update && apt-get install -y \
            git \
            unzip \
            && rm -rf /var/lib/apt/lists/*

        # Install PCOV extension
        RUN pecl install pcov \
            && docker-php-ext-enable pcov

        # Install Composer globally
        COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

        # Configure PCOV for optimal performance
        # Using "all" includes all files by default which is typical for small test setups
        RUN echo "pcov.enabled=1" >> /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini \
            && echo "pcov.directory=." >> /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini

        WORKDIR /app

    volumes:
      - .:/app

src/BaseHelper.php

<?php

namespace App;

abstract class BaseHelper
{
    abstract public function getConcreteValue(): string;

    public function getBaseValue(): string
    {
        return 'base';
    }
}

src/ConcreteHelper.php

<?php

namespace App;

class ConcreteHelper extends BaseHelper
{
    public function getConcreteValue(): string
    {
        return $this->getBaseValue() . '_concrete';
    }
}

src/ClassUnderTest.php

<?php

declare(strict_types=1);

namespace App;

class ClassUnderTest
{
    public function action(): string
    {
        $concreteHelper = new ConcreteHelper();

        return $concreteHelper->getConcreteValue();
    }
}

tests/ReproTest.php

<?php

namespace App\Tests;

use App\BaseHelper;
use App\ClassUnderTest;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\Attributes\UsesClassesThatExtendClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(ClassUnderTest::class)]
#[UsesClass(BaseHelper::class)]
#[UsesClassesThatExtendClass(BaseHelper::class)]
class ReproTest extends TestCase
{
    public function testValue(): void
    {
        $classUnderTest = new ClassUnderTest();
        self::assertSame('base_concrete', $classUnderTest->action());
    }
}

Then:

  • docker compose up
  • docker compose exec app composer install
  • Run tests twice (order doesn’t matter):
    • docker compose exec app vendor/bin/phpunit
    • vendor/bin/phpunit
  • First run succeeds without issues.
  • Second run reports:
1) App\Tests\ReproTest::testValue
This test executed code that is not listed as code to be covered or used:
- App\ConcreteHelper

This is because PHPUnit caches absolute file paths:

  • Inside the container: /app/src/BaseHelper.php
  • Outside the container: /Users/username/Developer/phpunit-bug/src/BaseHelper.php

When cached and real paths mismatch, UsesClassesThatExtendClass is not taken into account.

vendor/bin/phpunit --check-php-configuration

PHPUnit 12.5.3 by Sebastian Bergmann and contributors.

Checking whether PHP is configured according to https://docs.phpunit.de/en/12.5/installation.html#configuring-php-for-development

display_errors = On             ... ok
display_startup_errors = On     ... ok
error_reporting = -1            ... ok
xdebug.show_exception_trace = 0 ... ok
zend.assertions = 1             ... ok
assert.exception = 1            ... ok
memory_limit = -1               ... ok

composer info | sort

myclabs/deep-copy            1.13.4 Create deep copies (clones) of your objects
nikic/php-parser             5.7.0  A PHP parser written in PHP
phar-io/manifest             2.0.4  Component for reading phar.io manifest information from a PHP Archive (PHAR)
phar-io/version              3.2.1  Library for handling version information and constraints
phpunit/php-code-coverage    12.5.1 Library that provides collection, processing, and rendering functionality for PHP code coverage information.
phpunit/php-file-iterator    6.0.0  FilterIterator implementation that filters files based on a list of suffixes.
phpunit/php-invoker          6.0.0  Invoke callables with a timeout
phpunit/php-text-template    5.0.0  Simple template engine.
phpunit/php-timer            8.0.0  Utility class for timing
phpunit/phpunit              12.5.3 The PHP Unit Testing framework.
sebastian/cli-parser         4.2.0  Library for parsing CLI options
sebastian/comparator         7.1.3  Provides the functionality to compare PHP values for equality
sebastian/complexity         5.0.0  Library for calculating the complexity of PHP code units
sebastian/diff               7.0.0  Diff implementation
sebastian/environment        8.0.3  Provides functionality to handle HHVM/PHP environments
sebastian/exporter           7.0.2  Provides the functionality to export PHP variables for visualization
sebastian/global-state       8.0.2  Snapshotting of global state
sebastian/lines-of-code      4.0.0  Library for counting the lines of code in PHP source code
sebastian/object-enumerator  7.0.0  Traverses array structures and object graphs to enumerate all referenced objects
sebastian/object-reflector   5.0.0  Allows reflection of object attributes, including inherited and non-public ones
sebastian/recursion-context  7.0.1  Provides functionality to recursively process PHP variables
sebastian/type               6.0.3  Collection of value objects that represent the types of the PHP type system
sebastian/version            6.0.0  Library that helps with managing the version number of Git-hosted PHP projects
staabm/side-effects-detector 1.0.5  A static analysis tool to detect side effects in PHP code
theseer/tokenizer            2.0.1  A small library for converting tokenized PHP source code into XML and potentially other formats

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions