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
44 changes: 36 additions & 8 deletions src/Plugins/Shard.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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<string> $arguments
* @return list<string>
*/
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<string>
*/
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]));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/.snapshots/success.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1329 passed (3010 assertions)
144 changes: 144 additions & 0 deletions tests/Unit/Plugins/Shard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

use Pest\Plugins\Shard;
use Symfony\Component\Console\Output\NullOutput;

$shard = new Shard(new NullOutput);
$invoke = function (string $method, mixed ...$args) use ($shard): mixed {
$ref = new ReflectionMethod(Shard::class, $method);

return $ref->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');
});