Skip to content

Commit 9b118ac

Browse files
authored
Set up mutation testing
1 parent 38fc20d commit 9b118ac

File tree

4 files changed

+144
-0
lines changed

4 files changed

+144
-0
lines changed

.github/workflows/build.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,55 @@ jobs:
145145
- name: "Tests"
146146
run: "make tests"
147147

148+
mutation-testing:
149+
name: "Mutation Testing"
150+
runs-on: "ubuntu-latest"
151+
needs: ["tests", "static-analysis"]
152+
153+
strategy:
154+
fail-fast: false
155+
matrix:
156+
php-version:
157+
- "8.2"
158+
- "8.3"
159+
- "8.4"
160+
161+
steps:
162+
- name: "Checkout"
163+
uses: actions/checkout@v5
164+
165+
- name: "Install PHP"
166+
uses: "shivammathur/setup-php@v2"
167+
with:
168+
coverage: "pcov"
169+
php-version: "${{ matrix.php-version }}"
170+
ini-file: development
171+
extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb
172+
tools: infection:0.31.4
173+
174+
- name: "Allow installing on PHP 8.4"
175+
if: matrix.php-version == '8.4'
176+
run: "composer config platform.php 8.3.99"
177+
178+
- name: "Install dependencies"
179+
run: "composer install --no-interaction --no-progress"
180+
181+
- uses: "actions/download-artifact@v4"
182+
with:
183+
name: "result-cache-${{ matrix.php-version }}"
184+
path: "tmp/"
185+
186+
- name: "Run infection"
187+
run: |
188+
git fetch --depth=1 origin $GITHUB_BASE_REF
189+
infection --git-diff-base=origin/$GITHUB_BASE_REF --git-diff-lines --ignore-msi-with-no-mutations --min-msi=100 --min-covered-msi=100 --log-verbosity=all --debug
190+
191+
- uses: "actions/upload-artifact@v4"
192+
if: always()
193+
with:
194+
name: "infection-log-${{ matrix.php-version }}"
195+
path: "tmp/infection.log"
196+
148197
static-analysis:
149198
name: "PHPStan"
150199
runs-on: "ubuntu-latest"
@@ -189,3 +238,9 @@ jobs:
189238

190239
- name: "PHPStan"
191240
run: "make phpstan"
241+
242+
- uses: "actions/upload-artifact@v4"
243+
with:
244+
# "update-packages" is not relevant for the download-artifact counterpart, but we need it here to get unique artifact names across all jobs
245+
name: "result-cache-${{ matrix.php-version }}${{ matrix.update-packages && '-packages-updated' || '' }}"
246+
path: "tmp/resultCache.php"

infection.json5

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"$schema": "vendor/infection/infection/resources/schema.json",
3+
"timeout": 30,
4+
"source": {
5+
"directories": [
6+
"src"
7+
]
8+
},
9+
"staticAnalysisTool": "phpstan",
10+
"logs": {
11+
"text": "tmp/infection.log"
12+
},
13+
"mutators": {
14+
"@default": false,
15+
"PHPStan\\Infection\\TrinaryLogicMutator": true
16+
}
17+
}

phpstan.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ parameters:
1818
- src
1919
- tests
2020

21+
resultCachePath: tmp/resultCache.php
22+
2123
excludePaths:
2224
- tests/*/data/*
2325
- tests/*/data-attributes/*
2426
- tests/*/data-php-*/*
2527
- tests/Rules/Doctrine/ORM/entity-manager.php
28+
- tests/Infection/
2629

2730
reportUnmatchedIgnoredErrors: false
2831

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Infection;
4+
5+
use Infection\Mutator\Definition;
6+
use Infection\Mutator\Mutator;
7+
use Infection\Mutator\MutatorCategory;
8+
use LogicException;
9+
use PhpParser\Node;
10+
use function in_array;
11+
12+
/**
13+
* @implements Mutator<Node\Expr\MethodCall>
14+
*/
15+
final class TrinaryLogicMutator implements Mutator
16+
{
17+
18+
public static function getDefinition(): Definition
19+
{
20+
return new Definition(
21+
<<<'TXT'
22+
Replaces TrinaryLogic->yes() with !TrinaryLogic->no() and vice versa.
23+
TXT
24+
,
25+
MutatorCategory::ORTHOGONAL_REPLACEMENT,
26+
null,
27+
<<<'DIFF'
28+
- $type->isBoolean()->yes();
29+
+ !$type->isBoolean()->no();
30+
DIFF,
31+
);
32+
}
33+
34+
public function getName(): string
35+
{
36+
return 'TrinaryLogicMutator';
37+
}
38+
39+
public function canMutate(Node $node): bool
40+
{
41+
if (!$node instanceof Node\Expr\MethodCall) {
42+
return false;
43+
}
44+
45+
if (!$node->name instanceof Node\Identifier) {
46+
return false;
47+
}
48+
49+
if (!in_array($node->name->name, ['yes', 'no'], true)) {
50+
return false;
51+
}
52+
53+
return true;
54+
}
55+
56+
public function mutate(Node $node): iterable
57+
{
58+
if (!$node->name instanceof Node\Identifier) {
59+
throw new LogicException();
60+
}
61+
62+
if ($node->name->name === 'yes') {
63+
yield new Node\Expr\BooleanNot(new Node\Expr\MethodCall($node->var, 'no'));
64+
} else {
65+
yield new Node\Expr\BooleanNot(new Node\Expr\MethodCall($node->var, 'yes'));
66+
}
67+
}
68+
69+
}

0 commit comments

Comments
 (0)