├── .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 | [](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 |