Skip to content

Commit f03c47b

Browse files
committed
Collect coverage in subprocesses
1 parent db58937 commit f03c47b

6 files changed

Lines changed: 135 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ jobs:
9696
if: ${{ matrix.php-version != env.HIGHEST_PHP_VERSION }}
9797

9898
- name: Run PHPUnit with coverage
99-
run: vendor/bin/phpunit --coverage-clover clover.xml
99+
run: vendor/bin/phpunit --coverage-php coverage.php
100100
env:
101101
DRIVER: mysql_pdo
102102
MYSQL_HOST: 127.0.0.1
@@ -106,6 +106,10 @@ jobs:
106106
PDO_ERRMODE: ${{ matrix.pdo-errmode }}
107107
if: ${{ matrix.php-version == env.HIGHEST_PHP_VERSION }}
108108

109+
- name: Merge worker coverage
110+
run: php tools/merge-coverage.php coverage.php clover.xml && rm coverage.php
111+
if: ${{ matrix.php-version == env.HIGHEST_PHP_VERSION }}
112+
109113
- name: Upload coverage to Codecov
110114
uses: codecov/codecov-action@v5
111115
env:
@@ -154,13 +158,16 @@ jobs:
154158
dependency-versions: ${{ matrix.deps }}
155159

156160
- name: Run PHPUnit with coverage
157-
run: vendor/bin/phpunit --coverage-clover clover.xml
161+
run: vendor/bin/phpunit --coverage-php coverage.php
158162
env:
159163
DRIVER: mysql_doctrine
160164
MYSQL_HOST: 127.0.0.1
161165
MYSQL_USER: root
162166
MYSQL_PASSWORD: password
163167

168+
- name: Merge worker coverage
169+
run: php tools/merge-coverage.php coverage.php clover.xml && rm coverage.php
170+
164171
- name: Upload coverage to Codecov
165172
uses: codecov/codecov-action@v5
166173
env:
@@ -201,13 +208,16 @@ jobs:
201208
uses: ramsey/composer-install@v3
202209

203210
- name: Run PHPUnit with coverage
204-
run: vendor/bin/phpunit --coverage-clover clover.xml
211+
run: vendor/bin/phpunit --coverage-php coverage.php
205212
env:
206213
DRIVER: mariadb_pdo
207214
MARIADB_HOST: 127.0.0.1
208215
MARIADB_USER: root
209216
MARIADB_PASSWORD: password
210217

218+
- name: Merge worker coverage
219+
run: php tools/merge-coverage.php coverage.php clover.xml && rm coverage.php
220+
211221
- name: Upload coverage to Codecov
212222
uses: codecov/codecov-action@v5
213223
env:
@@ -248,13 +258,16 @@ jobs:
248258
uses: ramsey/composer-install@v3
249259

250260
- name: Run PHPUnit with coverage
251-
run: vendor/bin/phpunit --coverage-clover clover.xml
261+
run: vendor/bin/phpunit --coverage-php coverage.php
252262
env:
253263
DRIVER: mariadb_doctrine
254264
MARIADB_HOST: 127.0.0.1
255265
MARIADB_USER: root
256266
MARIADB_PASSWORD: password
257267

268+
- name: Merge worker coverage
269+
run: php tools/merge-coverage.php coverage.php clover.xml && rm coverage.php
270+
258271
- name: Upload coverage to Codecov
259272
uses: codecov/codecov-action@v5
260273
env:
@@ -293,13 +306,16 @@ jobs:
293306
uses: ramsey/composer-install@v3
294307

295308
- name: Run PHPUnit with coverage
296-
run: vendor/bin/phpunit --coverage-clover clover.xml
309+
run: vendor/bin/phpunit --coverage-php coverage.php
297310
env:
298311
DRIVER: postgres_pdo
299312
POSTGRES_HOST: 127.0.0.1
300313
POSTGRES_USER: postgres
301314
POSTGRES_PASSWORD: postgres
302315

316+
- name: Merge worker coverage
317+
run: php tools/merge-coverage.php coverage.php clover.xml && rm coverage.php
318+
303319
- name: Upload coverage to Codecov
304320
uses: codecov/codecov-action@v5
305321
env:
@@ -338,13 +354,16 @@ jobs:
338354
uses: ramsey/composer-install@v3
339355

340356
- name: Run PHPUnit with coverage
341-
run: vendor/bin/phpunit --coverage-clover clover.xml
357+
run: vendor/bin/phpunit --coverage-php coverage.php
342358
env:
343359
DRIVER: postgres_doctrine
344360
POSTGRES_HOST: 127.0.0.1
345361
POSTGRES_USER: postgres
346362
POSTGRES_PASSWORD: postgres
347363

364+
- name: Merge worker coverage
365+
run: php tools/merge-coverage.php coverage.php clover.xml && rm coverage.php
366+
348367
- name: Upload coverage to Codecov
349368
uses: codecov/codecov-action@v5
350369
env:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
/tools/*
66
!/tools/ecs/composer.json
77
!/tools/ecs/ecs.php
8+
!/tools/merge-coverage.php

tests/Util/Command/Shutdown.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Lock\Tests\Util\Command;
6+
7+
use Brick\Lock\Tests\Util\CommandInterface;
8+
use Brick\Lock\Tests\Util\LockContext;
9+
10+
final class Shutdown implements CommandInterface
11+
{
12+
public function execute(LockContext $context): void
13+
{
14+
exit(0);
15+
}
16+
}

tests/Util/RemoteWorker.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
use PHPUnit\Framework\Assert;
1010
use Symfony\Component\Process\InputStream;
1111
use Symfony\Component\Process\Process;
12+
use Throwable;
1213

14+
use function dirname;
1315
use function explode;
16+
use function extension_loaded;
1417
use function implode;
1518
use function json_encode;
1619
use function microtime;
@@ -36,7 +39,18 @@ final class RemoteWorker
3639

3740
public function __construct()
3841
{
39-
$this->process = new Process(['php', 'worker.php'], __DIR__);
42+
$command = ['php'];
43+
44+
if (extension_loaded('pcov')) {
45+
// pcov.directory defaults to the process CWD (tests/Util/), which excludes src/.
46+
// Set it explicitly so pcov tracks the library source files.
47+
$command[] = '-d';
48+
$command[] = 'pcov.directory=' . dirname(__DIR__, 2) . '/src';
49+
}
50+
51+
$command[] = 'worker.php';
52+
53+
$this->process = new Process($command, __DIR__);
4054
$this->input = new InputStream();
4155

4256
$this->process->setInput($this->input);
@@ -235,6 +249,20 @@ private function parseDuration(string $duration): float
235249

236250
public function __destruct()
237251
{
252+
if (! $this->isKilled && $this->process->isRunning()) {
253+
try {
254+
$this->sendCommand(new Command\Shutdown());
255+
256+
// Give it enough time to collect coverage.
257+
$deadline = microtime(true) + 1.0;
258+
while ($this->process->isRunning() && microtime(true) < $deadline) {
259+
usleep(100_000);
260+
}
261+
} catch (Throwable) {
262+
// Ignore errors.
263+
}
264+
}
265+
238266
$this->process->stop(0);
239267
}
240268
}

tests/Util/worker.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,26 @@
1414
use Brick\Lock\Tests\Util\LockDriverFactoryException;
1515

1616
use function Opis\Closure\unserialize;
17+
use function pcov\collect;
18+
use function pcov\start;
19+
use function pcov\stop;
20+
21+
use const pcov\all;
1722

1823
require __DIR__ . '/../../vendor/autoload.php';
1924

25+
if (extension_loaded('pcov')) {
26+
start();
27+
register_shutdown_function(function (): void {
28+
stop();
29+
$coverage = collect(all);
30+
file_put_contents(
31+
sys_get_temp_dir() . '/brick-lock-worker-coverage-' . getmypid() . '.bin',
32+
serialize($coverage),
33+
);
34+
});
35+
}
36+
2037
try {
2138
$lockDriverWithInfo = LockDriverFactory::getDriver();
2239
} catch (LockDriverFactoryException $e) {

tools/merge-coverage.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/**
4+
* Merges worker subprocess coverage data into the main PHPUnit coverage report.
5+
*
6+
* Usage: php tools/merge-coverage.php <coverage.php> <clover.xml>
7+
*
8+
* Worker processes (tests/Util/worker.php) save their pcov coverage to temp files
9+
* named brick-lock-worker-coverage-<pid>.bin in sys_get_temp_dir(). This script
10+
* loads the serialized CodeCoverage object produced by PHPUnit (--coverage-php),
11+
* appends each worker's raw coverage data, then writes the merged clover.xml.
12+
*/
13+
14+
declare(strict_types=1);
15+
16+
require __DIR__ . '/../vendor/autoload.php';
17+
18+
use SebastianBergmann\CodeCoverage\CodeCoverage;
19+
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
20+
use SebastianBergmann\CodeCoverage\Report\Clover;
21+
22+
$coveragePhpFile = $argv[1] ?? __DIR__ . '/../coverage.php';
23+
$outputFile = $argv[2] ?? __DIR__ . '/../clover.xml';
24+
25+
/** @var CodeCoverage $coverage */
26+
$coverage = require $coveragePhpFile;
27+
28+
$workerFiles = glob(sys_get_temp_dir() . '/brick-lock-worker-coverage-*.bin');
29+
assert($workerFiles !== false);
30+
31+
foreach ($workerFiles as $file) {
32+
$data = file_get_contents($file);
33+
assert($data !== false);
34+
35+
$workerData = unserialize($data);
36+
37+
if (! is_array($workerData) || $workerData === []) {
38+
continue;
39+
}
40+
41+
$rawData = RawCodeCoverageData::fromXdebugWithoutPathCoverage($workerData);
42+
$coverage->append($rawData, basename($file, '.bin'));
43+
44+
unlink($file);
45+
}
46+
47+
(new Clover())->process($coverage, $outputFile);

0 commit comments

Comments
 (0)