Skip to content

Commit ffcedf1

Browse files
committed
feat(tests): add --parallel support using ParaTest for parallel PHPUnit execution
Implements: #8 - Add brianium/paratest as dev dependency - Add --parallel option to tests command - --parallel uses default worker count, --parallel=N specifies N workers - Filter PHPUnit arguments to keep only ParaTest-compatible options - Preserve coverage, filter, and test path options in parallel mode
1 parent e91f42b commit ffcedf1

2 files changed

Lines changed: 109 additions & 1 deletion

File tree

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"symfony/var-dumper": "^7.4",
5555
"symfony/var-exporter": "^7.4",
5656
"symplify/easy-coding-standard": "^13.0",
57-
"thecodingmachine/safe": "^3.4"
57+
"thecodingmachine/safe": "^3.4",
58+
"brianium/paratest": "^7.17"
5859
},
5960
"minimum-stability": "stable",
6061
"autoload": {

src/Command/TestsCommand.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\Console\Input\InputInterface;
2323
use Symfony\Component\Console\Input\InputOption;
2424
use Symfony\Component\Console\Output\OutputInterface;
25+
use Symfony\Component\Process\Exception\ProcessFailedException;
2526
use Symfony\Component\Process\Process;
2627

2728
/**
@@ -84,6 +85,13 @@ protected function configure(): void
8485
shortcut: 'f',
8586
mode: InputOption::VALUE_OPTIONAL,
8687
description: 'Filter which tests to run based on a pattern.',
88+
)
89+
->addOption(
90+
name: 'parallel',
91+
shortcut: 'p',
92+
mode: InputOption::VALUE_OPTIONAL,
93+
description: 'Run tests in parallel using ParaTest. Optional number of workers.',
94+
default: null,
8795
);
8896
}
8997

@@ -136,11 +144,110 @@ protected function execute(InputInterface $input, OutputInterface $output): int
136144
$arguments[] = '--filter=' . $input->getOption('filter');
137145
}
138146

147+
$parallel = $input->getOption('parallel');
148+
149+
if ($parallel !== null) {
150+
return $this->runParallel($input, $output, $parallel, $arguments);
151+
}
152+
139153
$command = new Process([...$arguments, $input->getArgument('path')]);
140154

141155
return parent::runProcess($command, $output);
142156
}
143157

158+
/**
159+
* Executes PHPUnit in parallel mode using ParaTest.
160+
*
161+
* @param InputInterface $input the runtime instruction set from the CLI
162+
* @param OutputInterface $output the console feedback relay
163+
* @param mixed $workers the number of workers or empty for auto
164+
* @param array<int, string> $baseArguments the base PHPUnit arguments
165+
*
166+
* @return int the status integer describing the termination code
167+
*/
168+
private function runParallel(
169+
InputInterface $input,
170+
OutputInterface $output,
171+
mixed $workers,
172+
array $baseArguments,
173+
): int {
174+
$output->writeln('<info>Running PHPUnit tests in parallel mode...</info>');
175+
176+
$validParatestOptions = [
177+
'--bootstrap',
178+
'--configuration',
179+
'--cache-directory',
180+
'--filter',
181+
'--group',
182+
'--exclude-group',
183+
'--testsuite',
184+
'--no-test-tokens',
185+
'--stop-on-defect',
186+
'--stop-on-error',
187+
'--stop-on-failure',
188+
'--stop-on-warning',
189+
'--stop-on-risky',
190+
'--stop-on-skipped',
191+
'--stop-on-incomplete',
192+
'--fail-on-incomplete',
193+
'--fail-on-risky',
194+
'--fail-on-skipped',
195+
'--fail-on-warning',
196+
'--fail-on-deprecation',
197+
];
198+
199+
$arguments = [
200+
$this->getAbsolutePath('vendor/bin/paratest'),
201+
];
202+
203+
if ($workers !== null && $workers !== '') {
204+
$arguments[] = '--processes=' . (int) $workers;
205+
}
206+
207+
foreach ($baseArguments as $arg) {
208+
$isValid = false;
209+
foreach ($validParatestOptions as $option) {
210+
if (str_starts_with($arg, $option)) {
211+
$isValid = true;
212+
213+
break;
214+
}
215+
}
216+
217+
if ($isValid) {
218+
$arguments[] = $arg;
219+
}
220+
}
221+
222+
$coverage = $input->getOption('coverage');
223+
if ($coverage !== null) {
224+
$output->writeln(
225+
'<info>Generating code coverage reports on path: ' . $this->getAbsolutePath($coverage) . '</info>'
226+
);
227+
228+
foreach ($this->getPsr4Namespaces() as $path) {
229+
$arguments[] = '--coverage-filter=' . $this->getAbsolutePath($path);
230+
}
231+
232+
$arguments[] = '--coverage-text';
233+
$arguments[] = '--coverage-html=' . $this->getAbsolutePath($coverage);
234+
$arguments[] = '--testdox-html=' . $this->getAbsolutePath($coverage) . '/testdox.html';
235+
$arguments[] = '--coverage-clover=' . $this->getAbsolutePath($coverage) . '/clover.xml';
236+
$arguments[] = '--coverage-php=' . $this->getAbsolutePath($coverage) . '/coverage.php';
237+
}
238+
239+
$filter = $input->getOption('filter');
240+
if ($filter !== null) {
241+
$arguments[] = '--filter=' . $filter;
242+
}
243+
244+
$arguments[] = $input->getArgument('path') ?? './tests';
245+
246+
$command = new Process($arguments);
247+
248+
return parent::runProcess($command, $output);
249+
}
250+
144251
/**
145252
* Safely constructs an absolute path tied to a defined capability option.
146253
*

0 commit comments

Comments
 (0)