From d393799d2aa6be734ac098d4576a3c44edc3c858 Mon Sep 17 00:00:00 2001 From: oddvalue Date: Fri, 12 Jun 2026 20:06:50 +0100 Subject: [PATCH] Optimize `buildFilterArgument` in `Shard` plugin for compact regex generation and add comprehensive tests (#1675) --- src/Plugins/Shard.php | 33 ++++++++++++- tests/Unit/Plugins/Shard.php | 92 ++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/Plugins/Shard.php b/src/Plugins/Shard.php index 92b782b6..b08dd1dd 100644 --- a/src/Plugins/Shard.php +++ b/src/Plugins/Shard.php @@ -225,7 +225,38 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable */ private function buildFilterArgument(array $testsToRun): string { - return addslashes(implode('|', $testsToRun)); + if ($testsToRun === []) { + return ''; + } + + /** @var array $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); } /** diff --git a/tests/Unit/Plugins/Shard.php b/tests/Unit/Plugins/Shard.php index f23e8581..bd5a5e06 100644 --- a/tests/Unit/Plugins/Shard.php +++ b/tests/Unit/Plugins/Shard.php @@ -50,6 +50,98 @@ describe('getShard', function () { ])->throws(InvalidOption::class); }); +describe('buildFilterArgument', function () { + it('generates compact filter for single test', function () { + $output = new BufferedOutput; + $shard = new Shard($output); + + $reflection = new ReflectionClass($shard); + $method = $reflection->getMethod('buildFilterArgument'); + + $filter = $method->invoke($shard, ['Tests\\Unit\\ExampleTest']); + + expect($filter)->toBe('Tests\\\\Unit\\\\ExampleTest'); + }); + + it('generates compact filter for multiple tests with common prefix', function () { + $output = new BufferedOutput; + $shard = new Shard($output); + + $reflection = new ReflectionClass($shard); + $method = $reflection->getMethod('buildFilterArgument'); + + $filter = $method->invoke($shard, [ + 'Tests\\Unit\\Foo\\BarTest', + 'Tests\\Unit\\Foo\\BazTest', + ]); + + expect($filter)->toBe('Tests\\\\Unit\\\\Foo\\\\(BarTest|BazTest)'); + }); + + it('generates compact filter for tests with different namespaces', function () { + $output = new BufferedOutput; + $shard = new Shard($output); + + $reflection = new ReflectionClass($shard); + $method = $reflection->getMethod('buildFilterArgument'); + + $filter = $method->invoke($shard, [ + 'Tests\\Unit\\FooTest', + 'Tests\\Feature\\BarTest', + ]); + + expect($filter)->toBe('Tests\\\\(Unit\\\\FooTest|Feature\\\\BarTest)'); + }); + + it('returns empty string for empty test list', function () { + $output = new BufferedOutput; + $shard = new Shard($output); + + $reflection = new ReflectionClass($shard); + $method = $reflection->getMethod('buildFilterArgument'); + + $filter = $method->invoke($shard, []); + + expect($filter)->toBe(''); + }); + + it('generates compact filter for deeply nested namespaces', function () { + $output = new BufferedOutput; + $shard = new Shard($output); + + $reflection = new ReflectionClass($shard); + $method = $reflection->getMethod('buildFilterArgument'); + + $filter = $method->invoke($shard, [ + 'Tests\\Unit\\Plugins\\Concerns\\Foo', + 'Tests\\Unit\\Plugins\\Concerns\\Bar', + 'Tests\\Unit\\Plugins\\Concerns\\Baz', + ]); + + expect($filter)->toBe('Tests\\\\Unit\\\\Plugins\\\\Concerns\\\\(Foo|Bar|Baz)'); + }); + + it('handles mix of nested and flat namespaces', function () { + $output = new BufferedOutput; + $shard = new Shard($output); + + $reflection = new ReflectionClass($shard); + $method = $reflection->getMethod('buildFilterArgument'); + + $tests = [ + 'Tests\\Unit\\SimpleTest', + 'Tests\\Unit\\Plugins\\Concerns\\HandleArguments', + 'Tests\\Unit\\Plugins\\Concerns\\Validation', + 'Tests\\Unit\\Another\\Deep\\Nested\\Test', + ]; + + $filter = $method->invoke($shard, $tests); + + expect($filter) + ->toBe(addslashes('Tests\\Unit\\(SimpleTest|Plugins\\Concerns\\(HandleArguments|Validation)|Another\\Deep\\Nested\\Test)')); + }); +}); + describe('ensureFilterLengthIsSafe', function () { it('accepts filter within length limit', function () { $output = new BufferedOutput;