Skip to content

Run mutation tests on 7.x with Infection - #63

Merged
tattali merged 15 commits into
7.xfrom
mutation-tests-with-infection-7-x
Oct 18, 2026
Merged

tattali merged 15 commits into
7.xfrom
mutation-tests-with-infection-7-x

Conversation

@tattali

@tattali tattali commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #62 (base phpstan-max-level-strict-rules-7-x; retarget to 7.x once #60 and #62 are merged).

Line coverage does not show whether the tests would notice a wrong condition or value. Infection now mutates src/ in the current CI job (PHP 8.4, Symfony 7.4, after PHPUnit) and fails below the score measured today on 7.x:

measured threshold
MSI 85.96% minMsi: 85
Covered code MSI 85.96% minCoveredMsi: 85

356 mutants: 306 killed, 50 escaped, 0 not covered (mutation code coverage 100%). Thresholds are the measured values rounded down; raise them when tests improve.

  • infection.json5 added (all default mutators, text log in build/infection.log).
  • infection/infection ^0.35 joins require-dev and its extension installer plugin is allowed; like the other lint tools, CI only installs it in the current job.
  • .github/workflows/ci.yml regenerated with gen.py --infection (adds the "Mutation tests" step only).

The score was measured again after #62 gained its crash-fix tests and same-site helper (it was 84.64% before).

Local check (PHP 8.4, Symfony 7.4): PHPUnit passes (119 tests) and vendor/bin/infection --threads=max passes with these thresholds.

CI only ran samples on 7.x (PHP 8.2 lowest with Symfony 7.0, then
Symfony 7.4 on PHP 8.4, 8.5 and the 8.6 nightly), so a break on another
PHP or Symfony 7 minor could go unnoticed. The matrix now runs each
Symfony minor from 7.0 to 7.4 on every PHP from the 8.2 floor to the
highest PHP that Symfony minor tests itself (8.4 for 7.0, 8.5 for 7.1
to 7.4): 19 jobs, plus the lowest-dependencies job and the
allowed-to-fail PHP 8.6 nightly.

Jobs run on ubuntu-26.04 with actions/cache v6 and
sonarqube-scan-action v8. The PHPStan options move from the command
line to phpstan.neon.dist (same level 7, same paths), so CI and the
Makefile read the same configuration.

The require-dev constraints already follow the 7.x line (Symfony ^7.0,
PHPUnit ^11.5 for PHP 8.2 to 8.5), so they stay as they are.
Updates this branch with the 6.x merge-up on 7.x (crash fixes and their
tests), so the full matrix runs them too.
Symfony 7.0 to 7.3 no longer get fixes, so their last releases have
open security advisories and Composer 2.10 refuses to install them: the
matrix jobs for those minors failed at "composer update". Composer's
advisory blocking is turned off for this repository. The setting only
applies when installing the bundle's own repository (CI and local
development), not in applications that require the bundle.

The RedirectResponseWithCookie test cookie now uses the bundle's
default path and domain: with a null domain, Symfony 7.2 on PHP 8.5
raised a "null as an array offset" deprecation, which fails the run.
PHPStan ran at level 7 on 7.x. It now runs at level max with
phpstan-strict-rules (next to the phpstan-phpunit and phpstan-symfony
extensions already installed), with phpVersion 80200 (the floor of
this line) and no baseline or ignored errors. Level max found 67
errors; all are fixed.

Public and protected signatures do not change: they get phpdoc types
(the redirect configuration shape, declared once in Configuration, and
the processed bundle configuration). Implicit boolean checks become
explicit null or empty-string comparisons, and the configuration is
read with null coalescing instead of assuming every key is there.

No new crash was found on 7.x: the ones level max found on 6.x came
with the 6.x merge-up.
Line coverage does not show whether the tests would notice a wrong
condition or value. Infection now mutates src/ in the current CI job
(PHP 8.4, Symfony 7.4, after PHPUnit) and fails below the 7.x score
measured today: MSI 84.64% and covered code MSI 84.64% (345 mutants,
292 killed, 53 escaped, none uncovered), so both thresholds are 84.

infection/infection joins require-dev; like the other lint tools, CI
only installs it in the current job.
mobiledetectlib 4.8.0, the lowest release 7.x allows, declares
VERSION_TYPE_STRING and VERSION_TYPE_FLOAT private, so the previous
commit broke the lowest-dependencies job. The test reads them by
reflection again and asserts they are strings, which gives PHPStan the
type it needs.
Symfony 7.0 to 7.3 no longer get fixes, so Composer refuses their last
releases because of open security advisories. composer.json turned the
advisory check off for every install of this repository, which also
hid advisories in the supported Symfony 7.4 jobs and for local
development.

The setting is removed from composer.json. ci.yml, regenerated with
gen.py, now marks the Symfony 7.0 to 7.3 jobs as end of life and passes
--no-security-blocking to composer update only in those jobs; the
Symfony 7.4 jobs keep the advisory check.
createCookie() passed getCookieSameSite(), a ?string, to Cookie::create(),
whose sameSite parameter only accepts the SAMESITE_* constants, '' or
null; an inline ignore hid that PHPStan error.

A private helper now maps the configured value the way Cookie does (case
does not matter, '' means none) and throws the exception Cookie throws
for any other value, so the cookie is the same as before for every
input. Tests pin the result for each accepted value and the exception
for an invalid one.
The changes PHPStan asked for also turn some crashes into defined
results. Each one now has a test that fails on the code before them:

- DeviceDataCollector::getViews() before collect() or after reset()
  returned null from an array method (TypeError); it returns [].
- A redirect configuration without status_code made the listener pass
  null as the status code (TypeError); it redirects with 302, the
  default the configuration gives.
- A DeviceView that leaves the view unset made the listener prepare a
  response change for a null view (TypeError); the response is left
  alone.
- getRedirectUrl() with the "redirect" routing option and no host
  called rtrim(null) (TypeError); it returns null. With
  "redirect_without_path" it still returns the switch query alone, as
  before.
- A configuration without detect_tablet_as_mobile, or a Twig extension
  configuration without desktop, raised "undefined array key" warnings;
  the result is the same, without the warning.
The tests added with the PHPStan changes kill more mutants: MSI and
covered code MSI are now 85.96% (356 mutants, 306 killed, PHP 8.4,
Symfony 7.4). minMsi and minCoveredMsi move from 84 to 85, the measured
score rounded down, so a drop fails CI.
@sonarqubecloud

Copy link
Copy Markdown

This branch was successfully deployed

1 active deployment
main — 38f9ef16 Deployed Oct 18, 2026 by tattali via PHP 8.3 - Symfony 7.1.* #87
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant