Optimize buildFilterArgument in Shard plugin for compact regex generation and add comprehensive tests (#1675)

This commit is contained in:
oddvalue
2026-06-12 20:06:50 +01:00
committed by GitHub
parent 0d7814ca16
commit d393799d2a
2 changed files with 124 additions and 1 deletions

View File

@ -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<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);
}
/**

View File

@ -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;