├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src └── LiveCodeCoverage │ ├── CodeCoverageFactory.php │ ├── LiveCodeCoverage.php │ ├── RemoteCodeCoverage.php │ └── Storage.php └── test └── functional ├── LiveCodeCoverageTest.php ├── phpunit.xml.dist ├── prepend.php └── src ├── a.php └── b.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | coverage 4 | .idea 5 | /.phpunit.result.cache 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | php: 6 | - 7.3 7 | - 7.4 8 | - 8.0 9 | 10 | install: 11 | - composer install --prefer-dist 12 | 13 | script: 14 | - composer validate --strict 15 | - vendor/bin/phpunit 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Matthias Noback 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Live code coverage 2 | 3 | [![Build Status](https://travis-ci.org/matthiasnoback/live-code-coverage.svg?branch=master)](https://travis-ci.org/matthiasnoback/live-code-coverage) 4 | 5 | This library should help you generate code coverage reports on a live server (it doesn't have to be a production server of course). 6 | 7 | Install this library using: 8 | 9 | ```bash 10 | composer require matthiasnoback/live-code-coverage 11 | ``` 12 | 13 | ## Collecting code coverage data 14 | 15 | In your front controller (e.g. `index.php`), add the following: 16 | 17 | ```php 18 | ` section will be used to configure the code coverage whitelist. For example, this `phpunit.xml.dist` file might look something like this: 37 | 38 | ```xml 39 | 40 | 41 | 42 | 43 | src 44 | 45 | 46 | 47 | ``` 48 | 49 | Most configuration directives that are [available in PHPUnit](https://phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.whitelisting-files) work for this library too. 50 | If you notice that something doesn't work, please submit an issue. 51 | 52 | If you don't provide a PHPUnit configuration file, no filters will be applied, so you will get a coverage report for all the code in your project, including vendor and test code if applicable. 53 | 54 | If your application is a legacy application which `exit()`s or `die()`s before execution reaches the end of your front controller, the bootstrap should be slightly different: 55 | 56 | ```php 57 | $shutDownCodeCoverage = LiveCodeCoverage::bootstrap( 58 | // ... 59 | ); 60 | register_shutdown_function($shutDownCodeCoverage); 61 | 62 | // Run your web application now... 63 | ``` 64 | 65 | ## Generating code coverage reports (HTML, Clover, etc.) 66 | 67 | To merge all the coverage data and generate a report for it, install Sebastian Bergmann's [`phpcov` tool](https://github.com/sebastianbergmann/phpcov). Run it like this (or in any other way you like): 68 | 69 | ```bash 70 | phpcov merge --html=./coverage/html ./var/coverage 71 | ``` 72 | 73 | ## Downsides 74 | 75 | Please note that collecting code coverage data will make your application run much slower. Just see for yourself if that's acceptable. 76 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matthiasnoback/live-code-coverage", 3 | "description": "Generate code coverage reports on a live server", 4 | "license": "MIT", 5 | "require": { 6 | "php": "^7.3 || ^8.0", 7 | "webmozart/assert": "^1.2", 8 | "phpunit/php-code-coverage": "^9.0", 9 | "phpunit/phpunit": "^9.3" 10 | }, 11 | "autoload": { 12 | "psr-4": { 13 | "LiveCodeCoverage\\": "src/LiveCodeCoverage/" 14 | } 15 | }, 16 | "require-dev": { 17 | "symfony/process": "^3.3", 18 | "symfony/finder": "^3.3", 19 | "symfony/filesystem": "^3.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | test 11 | 12 | 13 | 14 | 15 | src 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/LiveCodeCoverage/CodeCoverageFactory.php: -------------------------------------------------------------------------------- 1 | load($phpunitFilePath)); 24 | 25 | return $codeCoverage; 26 | } 27 | 28 | private static function configure(CodeCoverage $codeCoverage, Configuration $configuration) 29 | { 30 | $codeCoverageConfiguration = $configuration->codeCoverage(); 31 | 32 | // The following code is copied from PHPUnit\TextUI\TestRunner 33 | if ($codeCoverageConfiguration->hasNonEmptyListOfFilesToBeIncludedInCodeCoverageReport()) { 34 | if ($codeCoverageConfiguration->includeUncoveredFiles()) { 35 | $codeCoverage->includeUncoveredFiles(); 36 | } else { 37 | $codeCoverage->excludeUncoveredFiles(); 38 | } 39 | 40 | if ($codeCoverageConfiguration->processUncoveredFiles()) { 41 | $codeCoverage->processUncoveredFiles(); 42 | } else { 43 | $codeCoverage->doNotProcessUncoveredFiles(); 44 | } 45 | } 46 | 47 | /* 48 | * `FilterMapper` is not covered by PHPUnit's backward-compatibility promise, but let's use it instead of 49 | * copying it. 50 | */ 51 | (new FilterMapper())->map($codeCoverage->filter(), $configuration->codeCoverage()); 52 | } 53 | 54 | /** 55 | * @return CodeCoverage 56 | */ 57 | public static function createDefault() 58 | { 59 | $filter = new Filter(); 60 | $driverSelector = new Selector(); 61 | $driver = $driverSelector->forLineCoverage($filter); 62 | return new CodeCoverage($driver, $filter); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/LiveCodeCoverage/LiveCodeCoverage.php: -------------------------------------------------------------------------------- 1 | codeCoverage = $codeCoverage; 28 | $this->coverageId = $coverageId; 29 | $this->storageDirectory = $storageDirectory; 30 | } 31 | 32 | /** 33 | * @param bool $collectCodeCoverage 34 | * @param string $storageDirectory 35 | * @param string|null $phpunitConfigFilePath 36 | * @param string $coverageId 37 | * @return callable 38 | */ 39 | public static function bootstrap($collectCodeCoverage, $storageDirectory, $phpunitConfigFilePath = null, $coverageId = 'live-coverage') 40 | { 41 | Assert::boolean($collectCodeCoverage); 42 | if (!$collectCodeCoverage) { 43 | return function () { 44 | // do nothing - code coverage is not enabled 45 | }; 46 | } 47 | 48 | if ($phpunitConfigFilePath !== null) { 49 | Assert::file($phpunitConfigFilePath); 50 | $codeCoverage = CodeCoverageFactory::createFromPhpUnitConfiguration($phpunitConfigFilePath); 51 | } else { 52 | $codeCoverage = CodeCoverageFactory::createDefault(); 53 | } 54 | 55 | $liveCodeCoverage = new self($codeCoverage, $storageDirectory, $coverageId); 56 | 57 | $liveCodeCoverage->start(); 58 | 59 | return [$liveCodeCoverage, 'stopAndSave']; 60 | } 61 | 62 | private function start() 63 | { 64 | $this->codeCoverage->start($this->coverageId); 65 | } 66 | 67 | /** 68 | * Stop collecting code coverage data and save it. 69 | */ 70 | public function stopAndSave() 71 | { 72 | $this->codeCoverage->stop(); 73 | 74 | Storage::storeCodeCoverage( 75 | $this->codeCoverage, 76 | $this->storageDirectory, 77 | uniqid(date('YmdHis'), true) 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/LiveCodeCoverage/RemoteCodeCoverage.php: -------------------------------------------------------------------------------- 1 | process($coverage, $filePath); 33 | } 34 | 35 | /** 36 | * @param string $storageDirectory 37 | * @return CodeCoverage 38 | */ 39 | public static function loadFromDirectory($storageDirectory) 40 | { 41 | Assert::string($storageDirectory); 42 | 43 | $coverage = CodeCoverageFactory::createDefault(); 44 | 45 | if (!is_dir($storageDirectory)) { 46 | return $coverage; 47 | } 48 | 49 | foreach (new DirectoryIterator($storageDirectory) as $file) { 50 | if ($file->isDot()) { 51 | continue; 52 | } 53 | 54 | $partialCodeCoverage = include $file->getPathname(); 55 | Assert::isInstanceOf($partialCodeCoverage, CodeCoverage::class); 56 | 57 | $coverage->merge($partialCodeCoverage); 58 | } 59 | 60 | return $coverage; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/functional/LiveCodeCoverageTest.php: -------------------------------------------------------------------------------- 1 | coverageDirectory = __DIR__ . '/coverage'; 16 | 17 | $filesystem = new Filesystem(); 18 | $filesystem->remove($this->coverageDirectory); 19 | $filesystem->mkdir([$this->coverageDirectory]); 20 | } 21 | 22 | /** 23 | * @test 24 | */ 25 | public function it_generates_cov_files_with_serialized_CodeCoverage_objects() 26 | { 27 | $aProcess = new Process('php ' . __DIR__ . '/src/a.php'); 28 | $bProcess = new Process('php ' . __DIR__ . '/src/b.php'); 29 | 30 | $aProcess->run(); 31 | $bProcess->run(); 32 | 33 | $this->assertProcessSuccessful($aProcess); 34 | $this->assertProcessSuccessful($bProcess); 35 | 36 | /** @var Finder $finder */ 37 | $finder = Finder::create()->name('*.cov')->in([$this->coverageDirectory]); 38 | self::assertCount(2, $finder); 39 | 40 | foreach ($finder as $covFile) { 41 | $filePath = (string)$covFile; 42 | $this->assertIncludedFileReturnsCodeCoverageObject($filePath); 43 | } 44 | } 45 | 46 | /** 47 | * @param $filePath 48 | */ 49 | private function assertIncludedFileReturnsCodeCoverageObject($filePath) 50 | { 51 | $cov = include $filePath; 52 | $this->assertInstanceOf(CodeCoverage::class, $cov); 53 | } 54 | 55 | private function assertProcessSuccessful(Process $process) 56 | { 57 | if (!$process->isSuccessful()) { 58 | $this->fail( 59 | sprintf( 60 | "Process was not successful. Output:\n%s", 61 | $process->getOutput() 62 | ) 63 | ); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/functional/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/functional/prepend.php: -------------------------------------------------------------------------------- 1 |