diff --git a/src/Plugins/Shard.php b/src/Plugins/Shard.php index 814125f20..e9f1ece1d 100644 --- a/src/Plugins/Shard.php +++ b/src/Plugins/Shard.php @@ -187,15 +187,14 @@ private function handleUpdateShards(array $arguments): array */ private function allTests(array $arguments): array { - $output = (new Process([ - 'php', - ...$this->removeParallelArguments($arguments), - '--list-tests', - ]))->setTimeout(120)->mustRun()->getOutput(); + $command = $this->buildListTestsCommand( + $arguments, + TestSuite::getInstance()->testPath, + ); - preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches); + $output = (new Process($command))->setTimeout(120)->mustRun()->getOutput(); - return array_values(array_unique($matches[1])); + return $this->parseListTestsOutput($output); } /** @@ -204,7 +203,36 @@ private function allTests(array $arguments): array */ private function removeParallelArguments(array $arguments): array { - return array_filter($arguments, fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true)); + return array_values(array_filter( + $arguments, + fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true) + && ! str_starts_with($argument, '--processes'), + )); + } + + /** + * Builds the subprocess command used to enumerate tests via `--list-tests`. + * + * @param list $arguments + * @return list + */ + private function buildListTestsCommand(array $arguments, string $testPath): array + { + $filtered = $this->removeParallelArguments($arguments); + + return ['php', ...$filtered, '--test-directory='.$testPath, '--list-tests']; + } + + /** + * Parses `--list-tests` output into a unique list of test class FQCNs. + * + * @return list + */ + private function parseListTestsOutput(string $output): array + { + preg_match_all('/ - (?:P\\\\)?([A-Za-z_]\w*(?:\\\\[A-Za-z_]\w*)*)::/', $output, $matches); + + return array_values(array_unique($matches[1])); } /** diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 58dbeddf4..177581058 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1938,4 +1938,4 @@ ✓ pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value') - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1329 passed (3010 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1329 passed (3010 assertions) diff --git a/tests/Unit/Plugins/Shard.php b/tests/Unit/Plugins/Shard.php new file mode 100644 index 000000000..53766b5d6 --- /dev/null +++ b/tests/Unit/Plugins/Shard.php @@ -0,0 +1,144 @@ +invoke($shard, ...$args); +}; + +it('parses Tests\\ namespaced classes from --list-tests output', function () use ($invoke) { + $output = <<<'OUT' + INFO Available tests: + + - P\Tests\Features\After::__pest_evaluable_it_runs + - P\Tests\Features\After::__pest_evaluable_it_runs_twice + - P\Tests\Unit\Foo::test_bar +OUT; + + expect($invoke('parseListTestsOutput', $output))->toBe([ + 'Tests\\Features\\After', + 'Tests\\Unit\\Foo', + ]); +}); + +it('deduplicates repeated class names from multiple test methods', function () use ($invoke) { + $output = <<<'OUT' + - P\Tests\Same::method_a + - P\Tests\Same::method_b + - P\Tests\Same::method_c +OUT; + + expect($invoke('parseListTestsOutput', $output))->toBe(['Tests\\Same']); +}); + +it('returns an empty list for output with no matching lines', function () use ($invoke) { + expect($invoke('parseListTestsOutput', ''))->toBe([]) + ->and($invoke('parseListTestsOutput', 'some random text'))->toBe([]); +}); + +it('parses non-Tests namespaced classes', function () use ($invoke) { + $output = <<<'OUT' + - P\Acme\Sharding\OneTest::test_foo + - P\Acme\Sharding\TwoTest::test_bar + - App\Suite\BazTest::test_qux +OUT; + + expect($invoke('parseListTestsOutput', $output))->toBe([ + 'Acme\\Sharding\\OneTest', + 'Acme\\Sharding\\TwoTest', + 'App\\Suite\\BazTest', + ]); +}); + +it('parses unnamespaced top-level classes', function () use ($invoke) { + $output = ' - P\FooTest::test_bar'; + + expect($invoke('parseListTestsOutput', $output))->toBe(['FooTest']); +}); + +it('strips the P\\ Pest prefix but keeps the rest of the FQCN', function () use ($invoke) { + $output = <<<'OUT' + - P\Acme\OneTest::a + - Acme\TwoTest::b +OUT; + + expect($invoke('parseListTestsOutput', $output))->toBe([ + 'Acme\\OneTest', + 'Acme\\TwoTest', + ]); +}); + +it('ignores junk lines that lack the " - …::" framing', function () use ($invoke) { + $output = <<<'OUT' + INFO Available tests: + +There were errors: +garbage ::: not a test + - P\Acme\RealTest::method +OUT; + + expect($invoke('parseListTestsOutput', $output))->toBe(['Acme\\RealTest']); +}); + +it('builds the list-tests command with the forwarded --test-directory', function () use ($invoke) { + $command = $invoke('buildListTestsCommand', ['bin/pest', '--update-shards'], 'custom/suite'); + + expect($command)->toBe([ + 'php', + 'bin/pest', + '--update-shards', + '--test-directory=custom/suite', + '--list-tests', + ]); +}); + +it('strips --parallel and -p when building the list-tests command', function () use ($invoke) { + $command = $invoke('buildListTestsCommand', + ['bin/pest', '--parallel', '--update-shards', '-p'], + 'tests', + ); + + expect($command)->toBe([ + 'php', + 'bin/pest', + '--update-shards', + '--test-directory=tests', + '--list-tests', + ]); +}); + +it('forwards --test-directory even when input arguments include one', function () use ($invoke) { + $command = $invoke('buildListTestsCommand', ['bin/pest'], 'suites'); + + expect($command)->toContain('--test-directory=suites'); +}); + +it('strips --processes=N when building the list-tests command', function () use ($invoke) { + $command = $invoke('buildListTestsCommand', + ['bin/pest', '--parallel', '--processes=4', '--update-shards'], + 'tests', + ); + + expect($command)->toBe([ + 'php', + 'bin/pest', + '--update-shards', + '--test-directory=tests', + '--list-tests', + ]); +}); + +it('strips --processes N (space-separated) when building the list-tests command', function () use ($invoke) { + $command = $invoke('buildListTestsCommand', + ['bin/pest', '--parallel', '--processes', '4', '--update-shards'], + 'tests', + ); + + expect($command)->not->toContain('--processes') + ->and($command)->toContain('--update-shards') + ->and($command)->toContain('--test-directory=tests'); +});