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
27 changes: 26 additions & 1 deletion src/Support/FileFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ private static function separateDirectoriesAndFiles(array $paths): array
$dirs = [];
$files = [];
foreach ($paths as $path) {
if (! str_starts_with($path, DIRECTORY_SEPARATOR)) {
if (! self::isAbsolutePath($path)) {
$path = getcwd().DIRECTORY_SEPARATOR.$path;
}
if (is_dir($path)) {
Expand All @@ -50,6 +50,31 @@ private static function separateDirectoriesAndFiles(array $paths): array
return ['directories' => $dirs, 'files' => $files];
}

/**
* Determine whether a path is absolute, in a cross-platform way.
*
* The previous check — str_starts_with($path, DIRECTORY_SEPARATOR) — only
* recognised a leading separator. On Windows that misread a drive-letter
* absolute path (e.g. "C:\project\app", which PHPUnit hands us for every
* <source> include directory) as *relative*: getcwd() was then prepended,
* producing a non-existent doubled path that is_dir() rejected, so the
* directory was silently dropped and no mutations were generated. This
* mirrors PHPUnit's own drive-letter-aware absoluteness check.
*/
private static function isAbsolutePath(string $path): bool
{
// A leading separator: POSIX "/…", or "\…" / UNC "\\server\share" on Windows.
if (str_starts_with($path, '/') || str_starts_with($path, '\\')) {
return true;
}

// A Windows drive-letter prefix: "C:\…" or "C:/…".
return strlen($path) >= 3
&& ctype_alpha($path[0])
&& $path[1] === ':'
&& ($path[2] === '/' || $path[2] === '\\');
}

/**
* @param array<int, string> $pathsToIgnore
* @param array<int, string> $dirs
Expand Down
63 changes: 63 additions & 0 deletions tests/Unit/Support/FileFinderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

use Pest\Mutate\Support\FileFinder;
use Symfony\Component\Finder\SplFileInfo;

/**
* Invoke the private cross-platform absolute-path classifier.
*
* Reflection keeps the helper private (no public API change) while letting the
* Linux CI runner verify the Windows drive-letter cases it cannot exercise
* through real directories. Matches the reflection usage already in tests/Arch.php.
*/
function isAbsolutePath(string $path): bool
{
return (bool) (new ReflectionMethod(FileFinder::class, 'isAbsolutePath'))->invoke(null, $path);
}

describe('isAbsolutePath()', function (): void {
it('treats leading-separator paths as absolute', function (string $path): void {
expect(isAbsolutePath($path))->toBeTrue();
})->with([
'POSIX root' => '/var/www/app',
'POSIX short' => '/app',
'UNC share' => '\\\\server\\share',
'leading backslash' => '\\app',
]);

it('treats Windows drive-letter paths as absolute', function (string $path): void {
expect(isAbsolutePath($path))->toBeTrue();
})->with([
'drive + backslash' => 'C:\\project\\app', // the path PHPUnit hands the plugin on Windows
'drive + forward slash' => 'C:/project/app',
'lower-case drive' => 'd:\\app',
]);

it('treats relative paths as not absolute', function (string $path): void {
expect(isAbsolutePath($path))->toBeFalse();
})->with([
'bare directory' => 'app',
'nested' => 'tests/Unit',
'src' => 'src',
'drive-relative (no separator)' => 'C:app',
'empty' => '',
]);
});

it('finds php files when given an absolute source directory', function (): void {
// Regression for the Windows bug: PHPUnit supplies an ABSOLUTE <source>
// directory; FileFinder must not prepend getcwd() to it (which doubled the
// path and dropped the directory, yielding "0 Mutations for 0 Files created").
$absolute = dirname(__DIR__, 2).DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'Classes';

expect($absolute)->toBeDirectory();

$names = array_map(
fn (SplFileInfo $file): string => $file->getFilename(),
iterator_to_array(FileFinder::files([$absolute], [])),
);

expect($names)->toContain('AgeHelper.php', 'SizeHelper.php');
});