Skip to content
Open
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
41 changes: 41 additions & 0 deletions src/Support/CoveragePaths.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Pest\Mutate\Support;

use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData;

class CoveragePaths
{
/**
* Restores the absolute paths of the covered files: since phpunit/php-code-coverage 14,
* `--coverage-php` writes the file keys relative to `basePath`.
*/
public static function restore(ProcessedCodeCoverageData $coverageData, string $basePath): void
{
foreach ($coverageData->coveredFiles() as $relativePath) {
$coverageData->renameFile($relativePath, self::absolute($relativePath, $basePath));
}
}

/**
* @param non-empty-string $relativePath
* @return non-empty-string
*/
private static function absolute(string $relativePath, string $basePath): string
{
if ($basePath !== '') {
return $basePath.DIRECTORY_SEPARATOR.$relativePath;
}

// the base path is also empty when the covered files share only the root directory,
// and their keys then lost the leading separator; files without a common ancestor at
// all (e.g. on different Windows drives) keep their absolute paths
if (preg_match('/^[A-Za-z]:/', $relativePath) === 1) {
return $relativePath;
}

return DIRECTORY_SEPARATOR.$relativePath;
}
}
8 changes: 2 additions & 6 deletions src/Tester/MutationTestRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\Mutate\Repositories\TelemetryRepository;
use Pest\Mutate\Support\Configuration\Configuration;
use Pest\Mutate\Support\CoveragePaths;
use Pest\Mutate\Support\FileFinder;
use Pest\Mutate\Support\MutationGenerator;
use Pest\Support\Container;
Expand Down Expand Up @@ -121,13 +122,8 @@ public function run(): int
// since phpunit/php-code-coverage 14, `--coverage-php` writes an array instead of a
// serialized CodeCoverage, and its file keys are relative to `basePath`
$coverageData = $loadedCoverage['codeCoverage'];
$basePath = $loadedCoverage['basePath'];

if ($basePath !== '') {
foreach ($coverageData->coveredFiles() as $relativePath) {
$coverageData->renameFile($relativePath, $basePath.DIRECTORY_SEPARATOR.$relativePath);
}
}
CoveragePaths::restore($coverageData, $loadedCoverage['basePath']);
}

$coveredLines = $this->coveredLines($coverageData);
Expand Down
17 changes: 17 additions & 0 deletions tests/.tests/CoveragePaths/SharedRootTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

use Tests\Fixtures\Classes\AgeHelper;
use Tests\Fixtures\CoveragePaths\Generated\Threshold;

require_once getenv('PEST_MUTATE_GENERATED_COVERAGE_PATHS').DIRECTORY_SEPARATOR.'Threshold.php';

it('checks a class outside the repository', function (): void {
expect(Threshold::reached(10))->toBeTrue()
->and(Threshold::reached(9))->toBeFalse();
});

it('checks a class inside the repository', function (): void {
expect(AgeHelper::isSenior(65))->toBeTrue();
});
86 changes: 86 additions & 0 deletions tests/Features/CoveragePathsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

use Symfony\Component\Process\Process;
use Tests\Fixtures\CoveragePaths\Generated\Threshold;

beforeEach(function (): void {
$root = dirname(__DIR__, 2);

// outside the repository: on Linux and macOS, the covered files then share only the root
// directory, and php-code-coverage reports them without their leading separator
$this->directory = sys_get_temp_dir().DIRECTORY_SEPARATOR.'pest-mutate-coverage-paths';

if (! is_dir($this->directory)) {
mkdir($this->directory);
}

file_put_contents($this->directory.DIRECTORY_SEPARATOR.'Threshold.php', <<<'PHP'
<?php

namespace Tests\Fixtures\CoveragePaths\Generated;

class Threshold
{
public static function reached(int $value): bool
{
return $value >= 10;
}
}
PHP);

file_put_contents($this->directory.DIRECTORY_SEPARATOR.'phpunit.xml', <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="{$root}/vendor/autoload.php" cacheDirectory="{$root}/.phpunit.cache" colors="true">
<testsuites>
<testsuite name="coverage-paths">
<directory suffix="Test.php">{$root}/tests/.tests/CoveragePaths</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">{$root}/tests/Fixtures/Classes</directory>
<directory suffix=".php">{$this->directory}</directory>
</include>
</source>
</phpunit>
XML);
});

it('mutates the covered files that share only the root directory', function (array $arguments): void {
$process = new Process(
[
PHP_BINARY,
'vendor/bin/pest',
'--configuration='.$this->directory.DIRECTORY_SEPARATOR.'phpunit.xml',
'tests/.tests/CoveragePaths',
'--mutate',
'--mutator=EqualityGreaterOrEqualToGreater',
// mutates() would make Pest write a lock file shared with this test suite
'--class='.Threshold::class,
'--colors=never',
...$arguments,
],
dirname(__DIR__, 2),
[
'COLUMNS' => '120',
'XDEBUG_MODE' => 'coverage',
'PEST_PLUGIN_INTERNAL_TEST_SUITE' => '1',
'PEST_MUTATE_GENERATED_COVERAGE_PATHS' => $this->directory,
// when this test suite runs in parallel, the mutation tests must not run as a worker
'PARATEST' => false,
],
timeout: 120,
);

$process->run();

$output = $process->getOutput().$process->getErrorOutput();

expect(preg_match('/Mutations: *(.+)/', $output, $matches))->toBe(1, $output)
->and(trim($matches[1] ?? ''))->toBe('1 tested', $output);
})->with([
'all the mutations' => [[]],
'covered mutations only' => [['--covered-only']],
])->skipOnWindows(); // the file finder takes the paths with a drive letter for relative paths
50 changes: 50 additions & 0 deletions tests/Unit/Support/CoveragePathsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

use Pest\Mutate\Support\CoveragePaths;
use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData;
use SebastianBergmann\CodeCoverage\Util\PathReducer;

beforeEach(function (): void {
$this->restore = function (array $files): array {
$coverageData = new ProcessedCodeCoverageData;
$coverageData->setLineCoverage(array_fill_keys($files, [1 => [0 => 1]]));

// reduces the paths the same way `--coverage-php` does before writing the report
$basePath = new PathReducer()->reduce($coverageData);

CoveragePaths::restore($coverageData, $basePath);

return $coverageData->coveredFiles();
};
});

it('restores the absolute paths of the covered files', function (array $files): void {
expect(($this->restore)($files))->toEqualCanonicalizing($files);
})->with([
'single file' => [[
DIRECTORY_SEPARATOR.'project'.DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.'A.php',
]],
'files sharing a directory' => [[
DIRECTORY_SEPARATOR.'project'.DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.'A.php',
DIRECTORY_SEPARATOR.'project'.DIRECTORY_SEPARATOR.'tests'.DIRECTORY_SEPARATOR.'B.php',
]],
'files sharing only the root directory' => [[
DIRECTORY_SEPARATOR.'project'.DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.'A.php',
DIRECTORY_SEPARATOR.'tmp'.DIRECTORY_SEPARATOR.'B.php',
]],
]);

it('restores the absolute paths of the covered files on windows', function (array $files): void {
expect(($this->restore)($files))->toEqualCanonicalizing($files);
})->with([
'files sharing only the drive' => [[
'C:\\project\\src\\A.php',
'C:\\tmp\\B.php',
]],
'files on different drives' => [[
'C:\\project\\src\\A.php',
'D:\\tmp\\B.php',
]],
])->onlyOnWindows();