What happened?
On a large suite (~1700+ test classes), running pest --shard=1/4 without a tests/.pest/shards.json timings file makes shard 1 report No tests found and exit 1, even though hundreds of classes were assigned to it. Other shards run normally.
Root cause
In src/Plugins/Shard.php::handleArguments, when no timings file exists, tests are split into contiguous chunks:
// line 135
$testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? [];
and each chunk becomes a single |-joined regex:
// line 215
return addslashes(implode('|', $testsToRun));
Because --list-tests returns classes in discovery/alphabetical order, the first contiguous chunk is a cluster of classes sharing long common prefixes (e.g. Tests\Feature\Api\Admin\…). Joined into one unanchored alternation (~36 KB, ~436 alternatives), matching it triggers catastrophic backtracking: preg_match returns false with preg_last_error() === PREG_INTERNAL_ERROR. PHPUnit treats the errored filter as "matched nothing", so the shard runs zero tests and exits 1.
Why a committed shards.json hides it
The timings path distributes new (untimed) tests round-robin:
// line 128
$partitions[$i % $total][] = $test;
Interleaving scatters the clustered prefixes across shards, so no single filter is large/homogeneous enough to hit the limit. Any present timings file — even an empty {"timings":{}} — therefore avoids the bug; only the no-cache fallback is affected.
How to reproduce it
- A suite with enough classes that
ceil(total/shards) exceeds ~400 contiguous prefix-clustered classes.
- Ensure
tests/.pest/shards.json does not exist.
- Run
pest --shard=1/4.
- Shard 1 →
No tests found, exit 1.
Minimal confirmation of the mechanism:
$pat = '/'.addslashes(implode('|', $first436ContiguousClasses)).'/';
preg_match($pat, $someClass.'::x'); // false
echo preg_last_error(); // 1 (PREG_INTERNAL_ERROR)
// the same 436 classes interleaved → matches fine
Suggested fix
Make the no-cache fallback interleave like the new-tests path instead of using contiguous array_chunk — e.g. round-robin assignment ($i % $total). That breaks up the prefix clusters and keeps each shard's filter within PCRE limits. (Chunking/anchoring the emitted --filter, or writing it to a --filter file, would also help.)
Package Version
4.7.2 (fallback code identical since at least 4.3.2)
PHP Version
8.3
Operation System
Linux / macOS
What happened?
On a large suite (~1700+ test classes), running
pest --shard=1/4without atests/.pest/shards.jsontimings file makes shard 1 reportNo tests foundand exit 1, even though hundreds of classes were assigned to it. Other shards run normally.Root cause
In
src/Plugins/Shard.php::handleArguments, when no timings file exists, tests are split into contiguous chunks:and each chunk becomes a single
|-joined regex:Because
--list-testsreturns classes in discovery/alphabetical order, the first contiguous chunk is a cluster of classes sharing long common prefixes (e.g.Tests\Feature\Api\Admin\…). Joined into one unanchored alternation (~36 KB, ~436 alternatives), matching it triggers catastrophic backtracking:preg_matchreturnsfalsewithpreg_last_error() === PREG_INTERNAL_ERROR. PHPUnit treats the errored filter as "matched nothing", so the shard runs zero tests and exits 1.Why a committed shards.json hides it
The timings path distributes new (untimed) tests round-robin:
Interleaving scatters the clustered prefixes across shards, so no single filter is large/homogeneous enough to hit the limit. Any present timings file — even an empty
{"timings":{}}— therefore avoids the bug; only the no-cache fallback is affected.How to reproduce it
ceil(total/shards)exceeds ~400 contiguous prefix-clustered classes.tests/.pest/shards.jsondoes not exist.pest --shard=1/4.No tests found, exit 1.Minimal confirmation of the mechanism:
Suggested fix
Make the no-cache fallback interleave like the new-tests path instead of using contiguous
array_chunk— e.g. round-robin assignment ($i % $total). That breaks up the prefix clusters and keeps each shard's filter within PCRE limits. (Chunking/anchoring the emitted--filter, or writing it to a--filterfile, would also help.)Package Version
4.7.2 (fallback code identical since at least 4.3.2)
PHP Version
8.3
Operation System
Linux / macOS