Aerial view of a winding blue river meandering through dense green forested mountain terrain, with several smaller tributary streams flowing into the main river.
Many tributaries, one stream: code coverage data from parallel CI jobs flows together into a single report.

The problem with merging

PHPUnit can write serialized code coverage data to a file using the --coverage-phpCLI option. This is useful, for example, when you want to collect code coverage data from multiple test suite runs and then merge it into a single report.

This is where PHPCOV comes in: it has long provided a merge command for combining multiple serialized code coverage data files as well as a patch-coverage command for calculating the code coverage of lines changed by a patch.

However, PHPCOV has not been as useful as it could have been due to limitations in the serialized code coverage data format used by phpunit/php-code-coverage. These limitations made it difficult or even impossible to merge code coverage data files in common real-world scenarios.

There were three main problems:

  • Serialized code coverage data files contained absolute paths. This meant that merging files generated on different machines (or even in different directories on the same machine) would fail or produce incorrect results. This was issue #925.
  • When PHPUnit was used as a PHAR, the serialized data contained internal PHPUnitPHAR\ namespace prefixes that are added during the PHAR build process. This made it impossible to merge coverage data generated using PHPUnit's PHAR with data from a Composer-installed PHPUnit, or to use PHPCOV (itself distributed as a PHAR) with data files generated by PHPUnit's PHAR. This was PHPCOV issue #109.
  • The old merging approach loaded all coverage data into a single CodeCoverage object in memory. For large projects with many test suite runs, this required a lot of memory and was slow.

A new serialization format

phpunit/php-code-coverage 14.0 introduces a completely redesigned serialization layer. The old PHP class, which simply serialized the entire CodeCoverage object, has been replaced by a new Serializer class that writes a structured, versioned format.

The new format includes the following information:

  • Build information: a timestamp, the PHP runtime name and version, the phpunit/php-code-coverage version, and the code coverage driver name and version
  • Base path: the longest common path prefix of all covered files, which is stripped from the file paths in the coverage data
  • Code coverage data: the actual line (and optionally branch and path) coverage data, with relative file paths
  • Test results: information about the tests that were run, including their size, status, and execution time
  • Git information (optional): the repository's origin URL, branch, commit hash, and working tree status

The use of relative paths instead of absolute paths is the key improvement for merging. The new PathReducer determines the longest common path prefix of all covered files, strips it from every file path in the coverage data, and stores it as basePath. This means that coverage data files generated on different machines or in different directories can be merged without issues, as long as the relative structure of the source files is the same.

The PHPUnitPHAR\ namespace prefix is now automatically stripped from serialized data when PHPUnit runs as a PHAR. This ensures that coverage data files generated using PHPUnit's PHAR are fully compatible with those from a Composer-installed PHPUnit and with PHPCOV's PHAR.

A new merger

phpunit/php-code-coverage 14.0 also introduces a new Merger class that replaces the old CodeCoverage::merge() method for external merging. Instead of requiring the caller to load each file into a CodeCoverage object and then merge objects one by one, the new Merger operates directly on the serialized files.

The Merger validates that all files being merged were generated with the same PHP runtime version and the same code coverage driver. When the files include Git information, it also verifies that they all refer to the same repository state (same origin URL, branch, commit, and working tree status). If any of these checks fail, a descriptive exception is thrown.

Memory usage has been improved as well: the merging process avoids creating unnecessary intermediate arrays, which reduces the memory footprint when combining a large number of coverage data files.

The Unserializer and Merger classes work exclusively with coverage data artifacts, not with the source files that were covered. However, a tool such as PHPCOV still requires access to the covered source code in order to generate code coverage reports based on serialized code coverage data.

Git information

Since PHPUnit 13.1, if the --include-git-information CLI option is used alongside the --coverage-php CLI option, the serialised code coverage data file will contain details of the current state of the Git repository: the origin URL, the branch name, the commit hash, whether the working tree is clean, and the output of git status.

This information serves two purposes. First, it provides traceability: when you look at a coverage data file, you can see exactly which version of the code it was generated from. Second, and more importantly, it enables the Merger to verify that all files being merged were generated for the same state of the software. This prevents accidentally merging coverage data from different versions of the code, which would produce misleading results.

PHPCOV

PHPCOV 13.0 builds on the new capabilities of phpunit/php-code-coverage 14.0. Its mergecommand now uses the new Merger class, which means that the long-standing limitations around absolute paths and PHAR compatibility are resolved.

The merge command takes a directory containing .cov files and produces a combined code coverage report. In PHPCOV 13.0, this works reliably even when the individual files were generated on different machines (for instance, in parallel CI jobs) or using different PHPUnit installation methods (PHAR vs. Composer). The merged result can be written in any of the supported report formats: Clover XML, OpenClover XML, Cobertura XML, Crap4j XML, HTML, plain text, or PHPUnit's XML format.

A typical CI workflow for merging code coverage from parallel test runs looks like this:

  1. Each CI job runs PHPUnit with --coverage-php /path/to/directory/$(uuidgen).cov --include-git-information
  2. The .cov files from all jobs are collected into a single directory, for example /path/to/directory
  3. phpcov merge --html report /path/to/directory produces a combined HTML report in the directory report from the files in /path/to/directory

This workflow now works reliably regardless of whether the CI jobs run on different machines, use different directory structures, or whether PHPUnit is installed as a PHAR or via Composer.

Summary

The combination of phpunit/php-code-coverage 14.0 and PHPCOV 13.0 resolves long-standing limitations that made merging code coverage data fragile and error-prone. The new serialization format with relative paths, automatic PHAR prefix stripping, build metadata, optional Git state verification, and a dedicated Merger class provide a solid foundation for reliable code coverage merging in real-world CI environments. PHPCOV's patch coverage feature helps teams focus their testing efforts on the code that actually changed.