Skip to content

Commit adf904a

Browse files
committed
ci: add Infection mutation testing gate (closes #10)
infection/infection 0.32.7 as dev dep, infection.json5 with @default mutator profile and src/ source scope, composer mutation + mutation:ci scripts, two new CI steps after the coverage gate (--threads=4, --logger-github for inline PR annotations, --min-msi=75 --min-covered-msi=75). Initial thresholds 75% — measured baseline is 78.5% MSI (241/307 mutants, 100% MCC), set 3.5pp below for fluctuation absorption. Audit + ratchet toward 80%/90% targets is filed as a follow-up, same shape as the coverage gate (#9).
1 parent 94fd7b0 commit adf904a

4 files changed

Lines changed: 40 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,15 @@ jobs:
4949
path: build/logs/clover.xml
5050
retention-days: 14
5151
if-no-files-found: ignore
52+
53+
- name: Mutation testing
54+
run: composer mutation:ci
55+
56+
- name: Upload mutation report
57+
if: always()
58+
uses: actions/upload-artifact@v4
59+
with:
60+
name: infection-php-${{ matrix.php }}
61+
path: build/logs/infection.*
62+
retention-days: 14
63+
if-no-files-found: ignore

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
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.
1313
- **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.
14+
- **CI:** added Infection mutation testing gate, layered on top of the line-coverage gate. New `infection/infection ^0.32.7` dev dependency, `infection.json5` config (`@default` mutator profile, `src/` source scope, fixtures stay out via PHPUnit's existing `<source>` block, `--testsuite=Rules`), and two new composer scripts: `mutation` (local, `--threads=max --show-mutations` for inspecting escaped mutants) and `mutation:ci` (CI: `--threads=4 --no-progress --logger-github --min-msi=75 --min-covered-msi=75` — GitHub annotations on escaped mutants surface inline in PR diffs). Two new CI steps after the coverage gate: **Mutation testing** and **Upload mutation report** (per-leg `infection-php-${{ matrix.php }}` artifact, 14-day retention). `composer config allow-plugins.infection/extension-installer true` was set to permit the framework-adapter installer plugin. **Initial thresholds: 75% MSI and 75% Covered Code MSI** — measured baseline is 78.5% MSI (241 killed / 307 mutants, 100% Mutation Code Coverage), set 3.5 percentage points lower to absorb mutator-shape fluctuation on equivalent code. Same shape as the line-coverage gate: lock in current state, audit gaps, ratchet upward. The 22% surviving-mutant population audits as a mix of (a) genuinely-equivalent mutants the issue itself anticipated — `mb_stripos` ↔ `stripos` on PSR-4 ASCII-only class names in `LogRule`, defensive guard inversions (`LogicalNot`/`IfNegation`) on early returns that filter the same nodes by either condition — and (b) genuinely-uncovered branch logic that warrants new fixtures. A follow-up issue will audit each survivor, kill where realistic, `@infection-ignore-for-mutator`-annotate where equivalent, and ratchet thresholds to the issue's target of 80% MSI / 90% Covered Code MSI. Versioning: none (pure CI/test-infra, no consumer-visible behaviour; `infection` is `require-dev` only). Resolves issue #10.
1415

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

composer.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
"psr/log": "^3.0"
2323
},
2424
"require-dev": {
25-
"phpunit/phpunit": "^11.0",
26-
"laravel/pint": "^1.18"
25+
"infection/infection": "^0.32.7",
26+
"laravel/pint": "^1.18",
27+
"phpunit/phpunit": "^11.0"
2728
},
2829
"autoload": {
2930
"psr-4": {
@@ -49,12 +50,17 @@
4950
"test": "phpunit",
5051
"test:coverage": "phpunit --coverage-clover=build/logs/clover.xml --coverage-text",
5152
"coverage:check": "@php bin/coverage-check.php build/logs/clover.xml 83",
53+
"mutation": "infection --threads=max --show-mutations",
54+
"mutation:ci": "infection --threads=4 --no-progress --logger-github --min-msi=75 --min-covered-msi=75",
5255
"phpstan": "phpstan analyse",
5356
"format": "pint",
5457
"format:check": "pint --test"
5558
},
5659
"config": {
57-
"sort-packages": true
60+
"sort-packages": true,
61+
"allow-plugins": {
62+
"infection/extension-installer": true
63+
}
5864
},
5965
"minimum-stability": "stable",
6066
"prefer-stable": true

infection.json5

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
$schema: "./vendor/infection/infection/resources/schema.json",
3+
source: {
4+
directories: ["src"],
5+
},
6+
timeout: 30,
7+
logs: {
8+
text: "build/logs/infection.txt",
9+
summary: "build/logs/infection-summary.txt",
10+
json: "build/logs/infection.json",
11+
},
12+
tmpDir: "build/infection",
13+
mutators: {
14+
"@default": true,
15+
},
16+
testFramework: "phpunit",
17+
testFrameworkOptions: "--testsuite=Rules",
18+
}

0 commit comments

Comments
 (0)