Search by

dvaknheo / libcoverage

dvaknheo

Test-coverage toolkit for PHP libraries: generates PHPUnit test templates and merges every test into one HTML coverage report.

Package info

github.com/dvaknheo/libcoverage

Homepage

pkg:composer/dvaknheo/libcoverage

Statistics

Installs: 223

Dependents: 4

Suggesters: 0

Stars: 3

Open Issues: 0

v1.1.0 2026-10-31 01:27 UTC

This package is auto-updated.

Last update: 2026-10-31 01:33:02 UTC


README

English | 中文

*** v1.1.0 ***

Author QQ: 85811616

Official QQ Group: 714610448

LibCoverage helps PHP library developers achieve full code-coverage testing, making your PHP code more robust.

Using an AI coding agent? Point it at AGENTS.md (in an installed project: vendor/dvaknheo/libcoverage/AGENTS.md). It covers the per-class loop, the machine-readable output and the pitfalls. The per-class check is one command: LIBCOVERAGE_PER_CLASS=1 php -d xdebug.mode=coverage vendor/bin/phpunit tests/AppTest.php.

Requirements

  • PHP 7.4 or newer.
  • A coverage driver: Xdebug with xdebug.mode=coverage, PCOV, or phpdbg. Without one, help, setup and cloze still work, but nothing is traced.
  • PHPUnit 9.6 in the project. phpunit/php-code-coverage is installed as a dependency of this package and keeps PHPUnit on the 9.x line.

Usage

composer require --dev dvaknheo/libcoverage ## install
composer exec libcoverage          # show help
composer exec libcoverage setup    # set up the project
phpunit                            # run PHPUnit unit tests
cat test_reports/index.html        # view the report; you can also open it in a browser
cat test_reports/index.jsonl       # the same report, machine-readable, one JSON object per line
# composer exec libcoverage cloze  # new classes added; fill in test templates
# phpunit tests/AppTest.php && phpunit tests/support.php # regenerate when only a single class has changed
# composer exec libcoverage report  # alternative report generation; already generated during phpunit

composer exec libcoverage setup creates phpunit.xml (if it does not exist) and the supporting files tests/bootstrap.php and tests/support.php.

Run phpunit, then open index.html in the test_reports directory.

The setup command also generates corresponding tests/*Test.php test templates based on the class files in the src directory.

For example, for src/App.php:

<?php
namespace MyProject;

class App
{
    public function foo()
    {
        var_dump(DATE(DATE_ATOM));
    }
}

tests/AppTest.php will be generated:

<?php 
namespace tests\MyProject;

use MyProject\App;

use LibCoverage\LibCoverage;

class AppTest extends \PHPUnit\Framework\TestCase
{
    public function testAll()
    {
        LibCoverage::Begin(App::class);
        
        /* //

        // public methods
        App::_()->foo();

        //*/
        
        LibCoverage::End();
    }
}

The test code in the middle is up to you — write tests that achieve 100% coverage for the current class.

Methods are taken from reflection and grouped by visibility: public methods are written as usable calls, while protected / private ones are listed as commented-out calls, because a test cannot call them from the outside. Methods inherited from a parent class are not listed — they belong to the parent's own test template.

composer exec libcoverage cloze is used to fill in test files that were added later. Existing test files will not be overwritten.

phpunit tests/AppTest.php && phpunit tests/support.php is a trick for when only a single file has been modified.

Below is a screenshot of LibCoverage running PHPUnit with full coverage. Example of test_reports/index.html:

capture

JSONL Report for AI Agents

Every time the HTML report is generated, LibCoverage also writes test_reports/index.jsonl next to index.html. It is one JSON object per line: one line per source file, then a final summary line.

{"type":"file","path":"src/App.php","lines_total":8,"lines_tested":6,"lines_percent":75,"uncovered_lines":[7,9],"uncovered_methods":[{"name":"MyProject\\App::bar","lines_total":2,"lines_tested":1,"start_line":7,"end_line":9}]}
{"type":"summary","schema":1,"files":1,"lines_total":8,"lines_tested":6,"lines_percent":75,"generated_at":"2026-10-30T20:40:00+08:00","html_report":"test_reports/index.html"}
  • path is relative to the project root; a file outside the project keeps its absolute path.
  • uncovered_lines lists the executable lines that no test has reached.
  • uncovered_methods comes from static analysis, so methods that never ran are listed too, with their line range.
  • lines_percent is a number from 0 to 100; a file with no executable line counts as 100.
  • File rows are sorted by path and the summary is always the last line, so the file diffs and streams cleanly.

Because it is plain JSONL, grep, jq or an AI agent can read it without touching the HTML:

grep '"type":"file"' test_reports/index.jsonl | jq -c 'select(.lines_tested < .lines_total)'

LibCoverage Class Options

Default options:

    public $options = [
        'namespace' => null,
        'path' => null,
        'path_src' => 'src',
        'path_dump' => 'test_coveragedumps',
        'path_report' => 'test_reports',
        'path_test' => 'tests',
        'path_data' => 'tests/data_for_tests',
        'auto_detect_namespace' => true, 
        'report_per_class' => false,
    ];

When running composer exec libcoverage, these options can be passed in, for example:

vendor/bin/libcoverage --path='abc' --path-test=test

Option names may be spelled with - or _, so --path-test=test and --path_test=test are the same option.

You can also adjust these options in tests/bootstrap.php.

namespace is the base namespace of the classes to be tested. If left empty, auto_detect_namespace will detect and fill it.

path is the base path.

path_data is used to store test data.

report_per_class (default false): when enabled, End() prints the coverage of the class it just traced — Coverage: 12/15 (80.00%), plus the uncovered lines and the methods that still have gaps. The LIBCOVERAGE_PER_CLASS environment variable takes precedence over this option, so a one-off run does not need to touch tests/bootstrap.php:

LIBCOVERAGE_PER_CLASS=1 vendor/bin/phpunit tests/AppTest.php

On Windows PowerShell: $env:LIBCOVERAGE_PER_CLASS=1; vendor/bin/phpunit tests/AppTest.php. Keep the switch per run — do not put it in phpunit.xml: a full run does not need per-class output and would print a block for every class.

True values are 1, true, on, yes (case-insensitive); anything else is false. The dump is still written, so tests/support.php keeps building the merged HTML report.

Every per-class run also ends with one machine-readable line, prefixed with LIBCOVERAGE_CLASS , carrying the class totals and a files array (same row shape as the JSONL report). For tooling, grep that prefix instead of tailing the console.

When PHPUnit is invoked with its own --coverage-* option, LibCoverage disables its own tracing and prints a warning. Two coverage collectors cannot share one Xdebug — a nested start/stop wipes the outer session — so LibCoverage's dumps would be empty. Run PHPUnit without --coverage-* when you want LibCoverage to trace.

LibCoverage Class Public Method Reference

Static methods — common needs are met by calling static methods:

    LibCoverage::_($object=null); // mutable singleton; pass $object to replace the singleton
    LibCoverage::Begin($class);    // start full-coverage tracing for a class
    LibCoverage::End();  // end tracing for a class
    LibCoverage::Report();  // used by support.php to display all reports
    LibCoverage::NewProject();  // create project files; used by the setup command
    LibCoverage::Cloze();  // create test files; used by the cloze command
    LibCoverage::CreateTestDir($class=null);  // create the test data directory path_data/[ClassName]; $class defaults to the class of Begin()
    LibCoverage::CleanTestDir($class=null);   // clean that directory

Additional methods:

    LibCoverage::_()->init(array $options, ?object $context = null); // used by bootstrap.php for initialization

Other less commonly used public methods:

    LibCoverage::_()->doPause() // pause
    LibCoverage::_()->doResume() // resume
    LibCoverage::_()->hasCoverageDriver() // whether a coverage driver (Xdebug, PCOV or phpdbg) is available
    LibCoverage::_()->addExtFile($extFile); // add extra test files, such as global function files
    LibCoverage::_()->getClassTestPath($class); // get the dedicated directory for the test class; default is tests/data_for_tests/[ClassName]
    LibCoverage::_()->doCreateTestDir($class=null); // create that directory, returns its path
    LibCoverage::_()->doCleanTestDir($class=null);  // clean that directory, returns its path
    LibCoverage::_()->cleanDirectory($dir);  // helper method for deleting test directories, etc.

Full Coverage Passing Does Not Equal All Functional Tests Passing

The code may use @codeCoverageIgnore, @codeCoverageIgnoreStart, and @codeCoverageIgnoreEnd to skip sections.

Also, conditional short-circuiting may skip subsequent branches.

Also, someone forced to achieve full coverage may not necessarily exercise all logic paths.

Changelog

See CHANGELOG.md for the changes in every release.

Contributing

  • AGENTS.md is the workflow guide for AI coding agents working in a project that depends on LibCoverage: the per-class loop, the machine-readable output, and the common pitfalls. Requirements, the report schema and every option stay here in the README.
  • DEVELOPMENT.md documents the internals, the rules for changes, the commands and the release checklist for maintainers.