Skip to content

Commit 559770f

Browse files
committed
ci: add line-coverage measurement and threshold gate (closes #9)
PCOV on both 8.4/8.5 legs, new test:coverage + coverage:check composer scripts, standalone bin/coverage-check.php (no extra runtime dep on a static-analysis package), clover.xml uploaded as a per-leg artifact. Initial threshold 83% — measured baseline is 83.92% line coverage; the gap to 90% (the issue's speculative starting point) audits as defensive guard clauses on unexpected AST shapes. Audit + ratchet is filed as a follow-up.
1 parent cfc032c commit 559770f

4 files changed

Lines changed: 77 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
uses: shivammathur/setup-php@v2
2121
with:
2222
php-version: ${{ matrix.php }}
23-
coverage: none
23+
coverage: pcov
2424
tools: composer:v2
2525

2626
- name: Install dependencies
@@ -35,5 +35,17 @@ jobs:
3535
- name: Static analysis (self)
3636
run: composer phpstan
3737

38-
- name: Tests
39-
run: composer test
38+
- name: Tests with coverage
39+
run: composer test:coverage
40+
41+
- name: Coverage threshold gate
42+
run: composer coverage:check
43+
44+
- name: Upload coverage report
45+
if: always()
46+
uses: actions/upload-artifact@v4
47+
with:
48+
name: clover-php-${{ matrix.php }}
49+
path: build/logs/clover.xml
50+
retention-days: 14
51+
if-no-files-found: ignore

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
1010

1111
- **`LogRule` (BREAKING):** extended to cover the static-call shapes `Model::destroy(...)` and `Model::forceDestroy(...)` on Log-named classes. `getNodeType()` broadened from `MethodCall::class` to `CallLike::class` and `processNode` branches on `MethodCall` vs `StaticCall`. Both shapes emit the same `logRule.logModification` identifier so consumer `phpstan.neon` `ignoreErrors` entries cover the whole rule with one identifier (the previous rule's compliance teeth depended on `delete`/`forceDelete` instance shapes; on a non-soft-delete log model `Model::destroy([1])` purges and `Model::forceDestroy([1])` always purges — both slipped through). `DB::table('logs')->truncate()` is intentionally still out of scope — Builder receiver type carries no Log-named class reference and the table name lives in a string argument; matching that needs a shape-specific call-chain rule. Tracked separately. Versioning: per ADR-0021 §Versioning, this is a Major bump (new errors in code that previously passed); within 0.x this ships as `v0.3.0`. **Pre-cascade audit required across emmie, kendo, entreezuil, ublgenie before tagging** — surface any `::destroy(`/`::forceDestroy(` calls on Log-named classes and route operational-log false positives to consumer-side `phpstan.neon` `ignoreErrors` (same convention used in v0.2.0 for `ublgenie/app/Actions/DeleteBranch.php`). Resolves issue #4.
1212
- **CI:** added PHP 8.5 to the `ci.yml` and `release.yml` test matrices alongside 8.4 (`['8.4']``['8.4', '8.5']`). PHP 8.5.0 was released 2025-11-20; the war-room dev environment already runs 8.5.5 locally, so PRs were getting ad-hoc 8.5 coverage during pre-push but no CI signal. Adding (rather than replacing) keeps 8.4 — the `composer.json` `^8.4` contractual minimum — covered. `shivammathur/setup-php@v2` supports 8.5 since GA. Resolves issue #5.
13+
- **CI:** added line-coverage measurement and a threshold gate. `ci.yml` switches `coverage: none` → `coverage: pcov` on both 8.4 and 8.5 matrix legs (PCOV is line-coverage-only and faster than Xdebug — debugger features aren't needed). New composer scripts: `test:coverage` (runs PHPUnit with `--coverage-clover=build/logs/clover.xml --coverage-text`) and `coverage:check` (runs `bin/coverage-check.php`, a standalone clover parser — no extra runtime dependency added to a static-analysis package for a single CI gate). Two new CI steps replace the `Tests` step: **Tests with coverage** and **Coverage threshold gate**. Clover XML is uploaded as a per-leg artifact (`clover-php-${{ matrix.php }}`, 14-day retention) so reviewers can inspect uncovered lines without spelunking through workflow logs. **Initial threshold: 83%** — the measured baseline is 83.92% (240/286 lines across `src/`), set 0.92 percentage points lower to absorb trivial fluctuation on equivalent-but-renamed code. Class coverage (0/6) and method coverage (39%) are intentionally unmeasured by the gate v1; per the issue's deliberation, line coverage is the right v1 signal and branch/method coverage is a follow-up after the line gate is bedded in. The 16-percentage-point gap to 100% audits as defensive guard clauses on unexpected node shapes (the kind of branch the issue itself flagged as "genuinely hard to fixture" — `LogRule`'s static-call branch falls back when `$node->class` is `Expr` rather than `Name`); a follow-up issue will audit and ratchet the threshold upward to 90%+. Versioning: none (pure CI/test-infra, no consumer-visible behaviour). Resolves issue #9.
1314

1415
## [0.2.0] — 2026-05-04
1516

bin/coverage-check.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
/**
6+
* Parse a PHPUnit clover XML report and fail if line coverage falls below
7+
* a threshold. Standalone script — no Composer/runtime dependency on a
8+
* coverage-check library, since the package itself is consumed as a static-
9+
* analysis library and adding a runtime dep for a single CI gate is overhead
10+
* without commensurate value.
11+
*
12+
* Usage: php bin/coverage-check.php <clover.xml> <threshold>
13+
*/
14+
$cloverPath = $argv[1] ?? null;
15+
$threshold = isset($argv[2]) ? (float) $argv[2] : null;
16+
17+
if ($cloverPath === null || $threshold === null) {
18+
fwrite(\STDERR, "Usage: coverage-check.php <clover.xml> <threshold>\n");
19+
exit(2);
20+
}
21+
22+
if (!is_file($cloverPath)) {
23+
fwrite(\STDERR, "Clover report not found: {$cloverPath}\n");
24+
exit(2);
25+
}
26+
27+
$xml = simplexml_load_file($cloverPath);
28+
29+
if ($xml === false) {
30+
fwrite(\STDERR, "Failed to parse clover XML: {$cloverPath}\n");
31+
exit(2);
32+
}
33+
34+
$metrics = $xml->project->metrics ?? null;
35+
36+
if ($metrics === null) {
37+
fwrite(\STDERR, "No <project><metrics> element in clover XML\n");
38+
exit(2);
39+
}
40+
41+
$statements = (int) $metrics['statements'];
42+
$covered = (int) $metrics['coveredstatements'];
43+
44+
if ($statements === 0) {
45+
fwrite(\STDERR, "No statements measured — cannot evaluate coverage\n");
46+
exit(2);
47+
}
48+
49+
$percent = ($covered / $statements) * 100.0;
50+
51+
printf("Line coverage: %.2f%% (%d/%d) — threshold: %.2f%%\n", $percent, $covered, $statements, $threshold);
52+
53+
if ($percent + 1e-9 < $threshold) {
54+
fwrite(\STDERR, "FAIL: line coverage below threshold\n");
55+
exit(1);
56+
}
57+
58+
echo "PASS\n";
59+
exit(0);

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
},
4848
"scripts": {
4949
"test": "phpunit",
50+
"test:coverage": "phpunit --coverage-clover=build/logs/clover.xml --coverage-text",
51+
"coverage:check": "@php bin/coverage-check.php build/logs/clover.xml 83",
5052
"phpstan": "phpstan analyse",
5153
"format": "pint",
5254
"format:check": "pint --test"

0 commit comments

Comments
 (0)