From 3f27352560010fb4c16046761af0fb805bf5be58 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 30 Jun 2025 22:15:07 +0100 Subject: [PATCH] feat: adds `--shard` --- composer.json | 9 ++-- src/Plugins/Shard.php | 108 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 src/Plugins/Shard.php diff --git a/composer.json b/composer.json index 9bf41f27..df8febf7 100644 --- a/composer.json +++ b/composer.json @@ -18,14 +18,15 @@ ], "require": { "php": "^8.3.0", - "brianium/paratest": "^7.10.2", + "brianium/paratest": "^7.10.3", "nunomaduro/collision": "^8.8.2", "nunomaduro/termwind": "^2.3.1", "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", "pestphp/pest-plugin-mutate": "^4.0.0", "pestphp/pest-plugin-profanity": "^4.0.0", - "phpunit/phpunit": "^12.2.5" + "phpunit/phpunit": "^12.2.5", + "symfony/process": "^7.3" }, "conflict": { "filp/whoops": "<2.16.0", @@ -56,8 +57,7 @@ "require-dev": { "pestphp/pest-dev-tools": "^4.0.0", "pestphp/pest-plugin-browser": "^4.0.0", - "pestphp/pest-plugin-type-coverage": "^4.0.0", - "symfony/process": "^7.3.0" + "pestphp/pest-plugin-type-coverage": "^4.0.0" }, "minimum-stability": "dev", "prefer-stable": true, @@ -114,6 +114,7 @@ "Pest\\Plugins\\Snapshot", "Pest\\Plugins\\Verbose", "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", "Pest\\Plugins\\Parallel" ] }, diff --git a/src/Plugins/Shard.php b/src/Plugins/Shard.php new file mode 100644 index 00000000..187e11f5 --- /dev/null +++ b/src/Plugins/Shard.php @@ -0,0 +1,108 @@ +hasArgument('--shard', $arguments)) { + return $arguments; + } + + // @phpstan-ignore-next-line + $input = new ArgvInput($arguments); + + if ($input->hasParameterOption('--'.self::SHARD_OPTION)) { + $shard = $input->getParameterOption('--'.self::SHARD_OPTION); + } else { + $shard = null; + } + + if (! is_string($shard) || ! preg_match('/^\d+\/\d+$/', $shard)) { + throw new InvalidOption('The [--shard] option must be in the format "index/total".'); + } + + [$index, $total] = explode('/', $shard); + + if (! is_numeric($index) || ! is_numeric($total)) { + throw new InvalidOption('The [--shard] option must be in the format "index/total".'); + } + + if ($index <= 0 || $total <= 0 || $index > $total) { + throw new InvalidOption('The [--shard] option index must be a non-negative integer less than the total number of shards.'); + } + + $index = (int) $index; + $total = (int) $total; + + $arguments = $this->popArgument("--shard=$index/$total", $this->popArgument('--shard', $this->popArgument( + "$index/$total", + $arguments, + ))); + + /** @phpstan-ignore-next-line */ + $tests = $this->allTests($arguments); + $testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? []; + + return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)]; + } + + /** + * Returns all tests that the test suite would run. + * + * @param list $arguments + * @return list + */ + private function allTests(array $arguments): array + { + $output = (new Process([ + 'php', + ...$this->removeParallelArguments($arguments), + '--list-tests', + ]))->mustRun()->getOutput(); + + preg_match_all('/ - (?:P\\\)?(.+)::/', $output, $matches); + + return array_values(array_unique($matches[1])); + } + + /** + * @param array $arguments + * @return array + */ + private function removeParallelArguments(array $arguments): array + { + return array_filter($arguments, fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true)); + } + + /** + * Builds the filter argument for the given tests to run. + */ + private function buildFilterArgument(mixed $testsToRun): string + { + return addslashes(implode('|', $testsToRun)); + } +}