diff --git a/src/Support/FileFinder.php b/src/Support/FileFinder.php index e4eb8c0..37305a8 100644 --- a/src/Support/FileFinder.php +++ b/src/Support/FileFinder.php @@ -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)) { @@ -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 + * 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 $pathsToIgnore * @param array $dirs diff --git a/tests/Unit/Support/FileFinderTest.php b/tests/Unit/Support/FileFinderTest.php new file mode 100644 index 0000000..8b91655 --- /dev/null +++ b/tests/Unit/Support/FileFinderTest.php @@ -0,0 +1,63 @@ +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 + // 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'); +});