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
70 changes: 67 additions & 3 deletions src/Plugins/Shard.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ final class Shard implements AddsOutput, HandlesArguments

private const string SHARD_OPTION = 'shard';

/**
* The maximum length allowed for the filter argument.
* While ARG_MAX can be 2MB, individual arguments are often limited to 128KB (MAX_ARG_STRLEN).
* Practical limits in CI environments (like Docker or pipeline runners) can be even lower.
*/
private const int MAX_FILTER_LENGTH = 32768;

/**
* The shard index and total number of shards.
*
Expand Down Expand Up @@ -72,7 +79,11 @@ public function handleArguments(array $arguments): array
'testsCount' => count($tests),
];

return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)];
$filter = $this->buildFilterArgument($testsToRun);

$this->ensureFilterLengthIsSafe($filter);

return [...$arguments, '--filter', $filter];
}

/**
Expand Down Expand Up @@ -105,10 +116,63 @@ private function removeParallelArguments(array $arguments): array

/**
* Builds the filter argument for the given tests to run.
*
* @param array<int, string> $testsToRun
*/
private function buildFilterArgument(mixed $testsToRun): string
private function buildFilterArgument(array $testsToRun): string
{
return addslashes(implode('|', $testsToRun));
if ($testsToRun === []) {
return '';
}

/** @var array<string, mixed> $tree */
$tree = [];
foreach ($testsToRun as $class) {
$parts = explode('\\', $class);
$current = &$tree;
foreach ($parts as $part) {
if (! isset($current[$part])) {
$current[$part] = [];
}
$current = &$current[$part];
}
}

$buildRegex = function (array $tree) use (&$buildRegex): string {
$parts = [];
foreach ($tree as $key => $sub) {
$subRegex = $buildRegex($sub);
if ($subRegex === '') {
$parts[] = preg_quote($key, '/');
} else {
$parts[] = preg_quote($key, '/').'\\\\'.(count($sub) > 1 ? '('.$subRegex.')' : $subRegex);
}
}

return implode('|', $parts);
};

return $buildRegex($tree);
}

/**
* Ensures that the filter length is safe for the current environment.
*
* @throws InvalidOption
*/
private function ensureFilterLengthIsSafe(string $filter): void
{
$maxLength = (int) (getenv('PEST_SHARD_MAX_FILTER_LENGTH') ?: self::MAX_FILTER_LENGTH);

if (strlen($filter) > $maxLength) {
throw new InvalidOption(sprintf(
'The generated filter for this shard is too long (%d characters). '.
'This can cause issues with some environments (limit is %d characters). '.
'Please increase the number of shards (e.g., use 1/4 instead of 1/2) to reduce the filter length.',
strlen($filter),
$maxLength
));
}
}

/**
Expand Down
Loading