Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/php.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,43 @@ jobs:
files: ./coverage/unit.xml
fail_ci_if_error: true
flags: unittests

mutation-tests:
name: Mutation tests (${{ matrix.php }}, ${{ matrix.prefer }})
runs-on: ubuntu-latest
strategy:
matrix:
include:
- service: phpfpm
php: "8.3"
prefer: prefer-stable
steps:
- uses: actions/checkout@v6

- name: Create docker network
run: |
docker network create frontend

# The GITHUB_* variables are passed into the container so
# Infection's CI detection works: --logger-github emits inline PR
# annotations for escaped mutants, and the Stryker dashboard
# logger (configured in infection.json5) publishes the report on
# pushes to develop. The minimum mutation score is enforced via
# minCoveredMsi in infection.json5.
- name: Run Infection
env:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
run: |
docker compose run --rm ${{ matrix.service }} composer install
docker compose run --rm \
-e XDEBUG_MODE=coverage \
-e GITHUB_ACTIONS \
-e GITHUB_REF \
-e GITHUB_HEAD_REF \
-e GITHUB_SHA \
-e GITHUB_REPOSITORY \
-e GITHUB_EVENT_NAME \
-e GITHUB_RUN_ID \
-e GITHUB_SERVER_URL \
-e STRYKER_DASHBOARD_API_KEY \
${{ matrix.service }} vendor/bin/infection --logger-github
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ yarn.lock

# Per-developer Claude Code context — local tooling, not part of the public source.
/CLAUDE.md

###> infection/infection ###
/infection.log
/infection.html
###< infection/infection ###
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Mutation testing with [Infection](https://infection.github.io/)
(`task test:mutation`), run in CI and reported to the Stryker dashboard
(mutation score badge in README)

## [5.0.0] - 2026-06-02

Reworked exception hierarchy and tightened IdP-payload validations. The runtime
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[![PHP Version](https://img.shields.io/packagist/php-v/itk-dev/openid-connect.svg?style=flat-square&colorB=%238892BF)](https://www.php.net/downloads)
[![Build Status](https://img.shields.io/github/actions/workflow/status/itk-dev/openid-connect/php.yaml?branch=develop&label=CI&logo=github&style=flat-square)](https://github.com/itk-dev/openid-connect/actions/workflows/php.yaml?query=branch%3Adevelop)
[![Codecov Code Coverage](https://img.shields.io/codecov/c/gh/itk-dev/openid-connect?label=codecov&logo=codecov&style=flat-square)](https://codecov.io/gh/itk-dev/openid-connect)
[![Mutation Score](https://img.shields.io/endpoint?style=flat-square&label=mutation%20score&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fitk-dev%2Fopenid-connect%2Fdevelop)](https://dashboard.stryker-mutator.io/reports/github.com/itk-dev/openid-connect/develop)
[![Read License](https://img.shields.io/packagist/l/itk-dev/openid-connect.svg?style=flat-square&colorB=darkcyan)](https://github.com/itk-dev/openid-connect/blob/master/LICENSE.md)
[![Package downloads on Packagist](https://img.shields.io/packagist/dt/itk-dev/openid-connect.svg?style=flat-square&colorB=darkmagenta)](https://packagist.org/packages/itk-dev/openid-connect/stats)

Expand Down Expand Up @@ -280,6 +281,27 @@ The test suite uses [Mockery](https://github.com/mockery/mockery) to mock
[public static methods](http://docs.mockery.io/en/latest/reference/public_static_properties.html?highlight=static)
in 3rd party libraries like the `JWT::decode` method from `firebase/jwt`.

### Mutation Testing

Line coverage shows which code the tests _execute_; mutation testing shows
which code they actually _verify_. [Infection](https://infection.github.io/)
applies small changes (mutants) to the source code — flipping a comparison,
removing a method call — and runs the test suite against each one. If the
tests still pass, the mutant "escaped": a potential bug the tests would not
catch.

```shell
task test:mutation
```

The minimum mutation score (`minCoveredMsi`) is defined in `infection.json5`
and enforced both locally and in CI — no command line flags needed. CI
annotates escaped mutants inline on pull requests, and results for `develop`
are published to the
[Stryker dashboard](https://dashboard.stryker-mutator.io/reports/github.com/itk-dev/openid-connect/develop),
which also feeds the mutation score badge above. Detailed reports are written
to `infection.log` and `infection.html` on each run.

### PHPStan Static Analysis

```shell
Expand Down
6 changes: 6 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ tasks:
cmds:
- "{{.DOCKER_COMPOSE}} exec -e XDEBUG_MODE=coverage phpfpm vendor/bin/phpunit --coverage-text --coverage-clover=coverage/unit.xml"

test:mutation:
desc: Run Infection mutation testing (threshold configured in infection.json5)
cmds:
- "{{.DOCKER_COMPOSE}} exec -e XDEBUG_MODE=coverage phpfpm vendor/bin/infection"

test:run:
desc: "Run tests for a PHP version and dependency set (e.g. task test:run PHP=8.4 DEPS=lowest)"
vars:
Expand Down Expand Up @@ -280,3 +285,4 @@ tasks:
- task: lint
- task: analyze:php
- task: test:matrix
- task: test:mutation
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"require-dev": {
"ergebnis/composer-normalize": "^2.50",
"friendsofphp/php-cs-fixer": "^3.75",
"infection/infection": "^0.33.2",
"mockery/mockery": "^1.6.12",
"phpstan/phpstan": "^2.1.41",
"phpstan/phpstan-mockery": "^2.0",
Expand All @@ -48,7 +49,8 @@
},
"config": {
"allow-plugins": {
"ergebnis/composer-normalize": true
"ergebnis/composer-normalize": true,
"infection/extension-installer": true
}
},
"scripts": {
Expand Down
23 changes: 23 additions & 0 deletions infection.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "vendor/infection/infection/resources/schema.json",
"source": {
"directories": ["src"]
},
"threads": "max",
// Minimum mutation score for covered code; enforced locally and in CI.
// Baseline measured at 71% — ratchet up as surviving mutants are killed.
"minCoveredMsi": 68,
"logs": {
"text": "infection.log",
"html": "infection.html",
// Publishes results for the develop branch to the Stryker dashboard
// (feeds the README badge). Requires STRYKER_DASHBOARD_API_KEY; only
// active on CI, skipped locally and on non-matching branches.
"stryker": {
"report": "develop"
}
},
"mutators": {
"@default": true
}
}
Loading