From 8a6e2927ef034ef30c8c2d40670d2120f5487de7 Mon Sep 17 00:00:00 2001 From: Maks Oleksyuk Date: Fri, 7 Aug 2026 14:59:53 +0300 Subject: [PATCH] fix: support the php-code-coverage 14.3 line coverage shape Co-Authored-By: Claude Opus 5 --- src/Tester/MutationTestRunner.php | 59 +++++++++++++++++++- tests/Unit/Tester/MutationTestRunnerTest.php | 35 ++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Tester/MutationTestRunnerTest.php diff --git a/src/Tester/MutationTestRunner.php b/src/Tester/MutationTestRunner.php index 01898b7..c3b9beb 100644 --- a/src/Tester/MutationTestRunner.php +++ b/src/Tester/MutationTestRunner.php @@ -130,8 +130,7 @@ public function run(): int } } - $coveredLines = array_map(fn (array $lines): array => array_filter($lines, fn (?array $tests): bool => $tests !== [] && $tests !== null), $coverageData->lineCoverage()); - $coveredLines = array_filter($coveredLines, fn (array $lines): bool => $lines !== []); + $coveredLines = $this->coveredLines($coverageData); $files = FileFinder::files($this->getConfiguration()->paths, $this->getConfiguration()->pathsToIgnore); @@ -192,6 +191,62 @@ classesToMutate: $this->getConfiguration()->everything ? [] : $this->getConfigur return $this->isMinScoreIsReached($mutationSuite) ? 0 : 1; } + /** + * Returns the tests covering each line, as `[file => [line => [testId, ...]]]`. + * + * @return array>> + */ + private function coveredLines(ProcessedCodeCoverageData $coverageData): array + { + /** @var array $testIds */ + $testIds = method_exists($coverageData, 'testIds') ? $coverageData->testIds() : []; // @phpstan-ignore function.alreadyNarrowedType + + $coveredLines = []; + + foreach ($coverageData->lineCoverage() as $file => $lines) { + foreach ($lines as $line => $tests) { + $ids = $this->testIdsCoveringLine($tests ?? [], $testIds); + + if ($ids === []) { + continue; + } + + $coveredLines[$file][$line] = $ids; + } + } + + return $coveredLines; + } + + /** + * Resolves the test ids a single line holds. + * + * Up to phpunit/php-code-coverage 14.2 a line held the test ids themselves. Since + * 14.3 it holds hit counts, keyed by an index into the coverage data's test ids. + * + * @param array $tests + * @param array $testIds + * @return array + */ + private function testIdsCoveringLine(array $tests, array $testIds): array + { + $ids = []; + + foreach ($tests as $index => $test) { + if (is_string($test)) { + $ids[] = $test; + + continue; + } + + if (is_int($index) && isset($testIds[$index])) { + $ids[] = $testIds[$index]; + } + } + + return $ids; + } + private function getConfiguration(): Configuration { return Container::getInstance()->get(ConfigurationRepository::class)->mergedConfiguration(); // @phpstan-ignore-line diff --git a/tests/Unit/Tester/MutationTestRunnerTest.php b/tests/Unit/Tester/MutationTestRunnerTest.php new file mode 100644 index 0000000..8f13429 --- /dev/null +++ b/tests/Unit/Tester/MutationTestRunnerTest.php @@ -0,0 +1,35 @@ + $tests + * @param array $testIds + * @param array $expected + */ +it('resolves the tests covering a line from either code coverage shape', function (array $tests, array $testIds, array $expected): void { + $runner = new MutationTestRunner; + + $method = new ReflectionClass($runner)->getMethod('testIdsCoveringLine'); + + expect($method->invoke($runner, $tests, $testIds))->toBe($expected); +})->with([ + 'ids in the values, up to php-code-coverage 14.2' => [ + ['P\Tests\Unit\FooTest::__pest_evaluable_it_works', 'Tests\Unit\BarTest::test_baz'], + [], + ['P\Tests\Unit\FooTest::__pest_evaluable_it_works', 'Tests\Unit\BarTest::test_baz'], + ], + 'hit counts keyed by index, since php-code-coverage 14.3' => [ + [0 => 1, 2 => 4], + [ + 0 => 'P\Tests\Unit\FooTest::__pest_evaluable_it_works', + 1 => 'Tests\Unit\OtherTest::test_untouched', + 2 => 'Tests\Unit\BarTest::test_baz', + ], + ['P\Tests\Unit\FooTest::__pest_evaluable_it_works', 'Tests\Unit\BarTest::test_baz'], + ], + 'an index with no test id is dropped' => [[7 => 1], [0 => 'Tests\Unit\FooTest::test_baz'], []], + 'nothing covers the line' => [[], [], []], +]);