From fb003c5e88086725e7cb0aa83aac45dc0d5a3518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Wed, 8 Apr 2026 22:34:37 -0300 Subject: [PATCH 1/3] Add EcsConfigFactory for extensible ECS configuration - Introduce EcsConfigFactory class with create() and loadBaseConfiguration() methods - Update ecs.php to return ECSConfigBuilder directly for consumer extensibility - Add usage examples in docblocks for require + extend pattern - Add tests for EcsConfigFactory Implements: #6 --- ecs.php | 38 ++++++- src/Config/EcsConfigFactory.php | 146 ++++++++++++++++++++++++++ tests/Config/EcsConfigFactoryTest.php | 76 ++++++++++++++ 3 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 src/Config/EcsConfigFactory.php create mode 100644 tests/Config/EcsConfigFactoryTest.php diff --git a/ecs.php b/ecs.php index a808d47..b92f42d 100644 --- a/ecs.php +++ b/ecs.php @@ -26,16 +26,44 @@ use PhpCsFixer\Fixer\Phpdoc\PhpdocToCommentFixer; use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer; use Symplify\EasyCodingStandard\Config\ECSConfig; +use Symplify\EasyCodingStandard\Configuration\ECSConfigBuilder; use function Safe\getcwd; +/** + * Base ECS configuration for dev-tools. + * + * This configuration can be extended in consumer projects using one of the following patterns: + * + * 1. Use directly (recommended): + * ```php + * return require __DIR__ . '/vendor/fast-forward/dev-tools/ecs.php'; + * ``` + * + * 2. Extend with custom rules: + * ```php + * $builder = require __DIR__ . '/vendor/fast-forward/dev-tools/ecs.php'; + * return $builder->withRules([CustomRule::class]); + * ``` + * + * 3. Using the factory (recommended for advanced use cases): + * ```php + * use FastForward\DevTools\Config\EcsConfigFactory; + * + * return EcsConfigFactory::create(); + * ``` + * + * @return ECSConfigBuilder the pre-configured ECS configuration builder + */ +$cwd = getcwd(); + return ECSConfig::configure() - ->withPaths([getcwd()]) + ->withPaths([$cwd]) ->withSkip([ - getcwd() . '/public', - getcwd() . '/resources', - getcwd() . '/vendor', - getcwd() . '/tmp', + $cwd . '/public', + $cwd . '/resources', + $cwd . '/vendor', + $cwd . '/tmp', PhpdocToCommentFixer::class, NoSuperfluousPhpdocTagsFixer::class, NoEmptyPhpdocFixer::class, diff --git a/src/Config/EcsConfigFactory.php b/src/Config/EcsConfigFactory.php new file mode 100644 index 0000000..ec64bad --- /dev/null +++ b/src/Config/EcsConfigFactory.php @@ -0,0 +1,146 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Config; + +use PhpCsFixer\Fixer\Import\GlobalNamespaceImportFixer; +use PhpCsFixer\Fixer\Phpdoc\GeneralPhpdocAnnotationRemoveFixer; +use PhpCsFixer\Fixer\Phpdoc\PhpdocAlignFixer; +use PhpCsFixer\Fixer\Phpdoc\NoEmptyPhpdocFixer; +use PhpCsFixer\Fixer\Phpdoc\NoSuperfluousPhpdocTagsFixer; +use PhpCsFixer\Fixer\Phpdoc\PhpdocAddMissingParamAnnotationFixer; +use PhpCsFixer\Fixer\Phpdoc\PhpdocNoEmptyReturnFixer; +use PhpCsFixer\Fixer\Phpdoc\PhpdocToCommentFixer; +use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer; +use Symplify\EasyCodingStandard\Config\ECSConfig; +use Symplify\EasyCodingStandard\Configuration\ECSConfigBuilder; + +use function Safe\getcwd; + +/** + * Factory for creating and extending the ECS (EasyCodingStandard) configuration. + * + * This factory allows consumers to build on top of the base configuration provided + * by the dev-tools package instead of replacing it entirely. Consumers can extend + * the configuration with custom rules, paths, skips, and other options while + * preserving the ability to receive upstream updates automatically. + * + * Usage examples: + * + * 1. Using the factory method (recommended): + * ```php + * use FastForward\DevTools\Config\EcsConfigFactory; + * + * return EcsConfigFactory::create(); + * ``` + * + * 2. Using the require pattern: + * ```php + * return require __DIR__ . '/vendor/fast-forward/dev-tools/ecs.php'; + * ``` + * + * 3. Extending the base configuration: + * ```php + * $builder = require __DIR__ . '/vendor/fast-forward/dev-tools/ecs.php'; + * return $builder->withRules([CustomRule::class]); + * ``` + */ +final class EcsConfigFactory +{ + /** + * Creates and returns a pre-configured ECSConfigBuilder with the base dev-tools rules. + * + * This method returns the base configuration builder that consumers can further + * customize with additional rules, paths, skips, or configured rules. + * + * @return ECSConfigBuilder the pre-configured ECS configuration builder + */ + public static function create(): ECSConfigBuilder + { + $cwd = getcwd(); + + return ECSConfig::configure() + ->withPaths([$cwd]) + ->withSkip([ + $cwd . '/public', + $cwd . '/resources', + $cwd . '/vendor', + $cwd . '/tmp', + PhpdocToCommentFixer::class, + NoSuperfluousPhpdocTagsFixer::class, + NoEmptyPhpdocFixer::class, + PhpdocNoEmptyReturnFixer::class, + GlobalNamespaceImportFixer::class, + GeneralPhpdocAnnotationRemoveFixer::class, + ]) + ->withRootFiles() + ->withPhpCsFixerSets(symfony: true, symfonyRisky: true, auto: true, autoRisky: true) + ->withPreparedSets(psr12: true, common: true, symplify: true, strict: true, cleanCode: true) + ->withConfiguredRule(PhpdocAlignFixer::class, [ + 'align' => 'left', + ]) + ->withConfiguredRule(PhpUnitTestCaseStaticMethodCallsFixer::class, [ + 'call_type' => 'self', + ]) + ->withConfiguredRule(PhpdocAddMissingParamAnnotationFixer::class, [ + 'only_untyped' => false, + ]); + } + + /** + * Loads and returns the base ECS configuration from the dev-tools package. + * + * This method can be used to require the base configuration file directly, + * providing full control over the configuration object. + * + * @param string|null $workingDirectory the working directory to use (defaults to cwd) + * + * @return ECSConfigBuilder the pre-configured ECS configuration builder + */ + public static function loadBaseConfiguration(?string $workingDirectory = null): ECSConfigBuilder + { + $cwd = $workingDirectory ?? getcwd(); + + return ECSConfig::configure() + ->withPaths([$cwd]) + ->withSkip([ + $cwd . '/public', + $cwd . '/resources', + $cwd . '/vendor', + $cwd . '/tmp', + PhpdocToCommentFixer::class, + NoSuperfluousPhpdocTagsFixer::class, + NoEmptyPhpdocFixer::class, + PhpdocNoEmptyReturnFixer::class, + GlobalNamespaceImportFixer::class, + GeneralPhpdocAnnotationRemoveFixer::class, + ]) + ->withRootFiles() + ->withPhpCsFixerSets(symfony: true, symfonyRisky: true, auto: true, autoRisky: true) + ->withPreparedSets(psr12: true, common: true, symplify: true, strict: true, cleanCode: true) + ->withConfiguredRule(PhpdocAlignFixer::class, [ + 'align' => 'left', + ]) + ->withConfiguredRule(PhpUnitTestCaseStaticMethodCallsFixer::class, [ + 'call_type' => 'self', + ]) + ->withConfiguredRule(PhpdocAddMissingParamAnnotationFixer::class, [ + 'only_untyped' => false, + ]); + } +} diff --git a/tests/Config/EcsConfigFactoryTest.php b/tests/Config/EcsConfigFactoryTest.php new file mode 100644 index 0000000..a2ab104 --- /dev/null +++ b/tests/Config/EcsConfigFactoryTest.php @@ -0,0 +1,76 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Config; + +use FastForward\DevTools\Config\EcsConfigFactory; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Symplify\EasyCodingStandard\Configuration\ECSConfigBuilder; + +#[CoversClass(EcsConfigFactory::class)] +final class EcsConfigFactoryTest extends TestCase +{ + use ProphecyTrait; + + /** + * @return void + */ + #[Test] + public function createWillReturnEcsConfigBuilderInstance(): void + { + $config = EcsConfigFactory::create(); + + self::assertInstanceOf(ECSConfigBuilder::class, $config); + } + + /** + * @return void + */ + #[Test] + public function loadBaseConfigurationWillReturnEcsConfigBuilderInstance(): void + { + $config = EcsConfigFactory::loadBaseConfiguration(); + + self::assertInstanceOf(ECSConfigBuilder::class, $config); + } + + /** + * @return void + */ + #[Test] + public function loadBaseConfigurationWithCustomWorkingDirectory(): void + { + $config = EcsConfigFactory::loadBaseConfiguration('/custom/path'); + + self::assertInstanceOf(ECSConfigBuilder::class, $config); + } + + /** + * @return void + */ + #[Test] + public function createWithCustomWorkingDirectoryWillReturnEcsConfigBuilderInstance(): void + { + $config = EcsConfigFactory::create(); + + self::assertInstanceOf(ECSConfigBuilder::class, $config); + } +} From c2b9f24cb8e01d6243e6a92aa2f3c3cb67f5e90f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Wed, 8 Apr 2026 22:37:31 -0300 Subject: [PATCH 2/3] Add RectorConfigFactory for extensible Rector configuration - Introduce RectorConfigFactory class with configure() and configureWithWorkingDirectory() methods - Update rector.php to return configuration closure for consumer extensibility - Add usage examples in docblocks for require + extend pattern - Add tests for RectorConfigFactory Implements: #7 --- rector.php | 33 +++- src/Config/RectorConfigFactory.php | 196 +++++++++++++++++++++++ tests/Config/RectorConfigFactoryTest.php | 67 ++++++++ 3 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 src/Config/RectorConfigFactory.php create mode 100644 tests/Config/RectorConfigFactoryTest.php diff --git a/rector.php b/rector.php index 254eb18..22e60e6 100644 --- a/rector.php +++ b/rector.php @@ -16,12 +16,12 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -use Rector\Configuration\PhpLevelSetResolver; use Composer\InstalledVersions; use Ergebnis\Rector\Rules\Faker\GeneratorPropertyFetchToMethodCallRector; use FastForward\DevTools\Rector\AddMissingMethodPhpDocRector; use FastForward\DevTools\Rector\RemoveEmptyDocBlockRector; use Rector\Config\RectorConfig; +use Rector\Configuration\PhpLevelSetResolver; use Rector\DeadCode\Rector\ClassMethod\RemoveUselessParamTagRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector; use Rector\Php\PhpVersionResolver\ComposerJsonPhpVersionResolver; @@ -29,6 +29,37 @@ use function Safe\getcwd; +/** + * Base Rector configuration for dev-tools. + * + * This configuration can be extended in consumer projects using one of the following patterns: + * + * 1. Use directly (recommended): + * ```php + * return require __DIR__ . '/vendor/fast-forward/dev-tools/rector.php'; + * ``` + * + * 2. Extend with custom rules: + * ```php + * $configure = require __DIR__ . '/vendor/fast-forward/dev-tools/rector.php'; + * return static function (RectorConfig $rectorConfig) use ($configure): void { + * $configure($rectorConfig); + * $rectorConfig->rules([CustomRule::class]); + * }; + * ``` + * + * 3. Using the factory: + * ```php + * use FastForward\DevTools\Config\RectorConfigFactory; + * + * return static function (RectorConfig $rectorConfig): void { + * RectorConfigFactory::configure($rectorConfig); + * // Add custom configuration + * }; + * ``` + * + * @return callable(RectorConfig): void the configuration closure that configures Rector + */ return static function (RectorConfig $rectorConfig): void { $rectorConfig->sets([ SetList::DEAD_CODE, diff --git a/src/Config/RectorConfigFactory.php b/src/Config/RectorConfigFactory.php new file mode 100644 index 0000000..8a468ee --- /dev/null +++ b/src/Config/RectorConfigFactory.php @@ -0,0 +1,196 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Config; + +use Composer\InstalledVersions; +use Ergebnis\Rector\Rules\Faker\GeneratorPropertyFetchToMethodCallRector; +use FastForward\DevTools\Rector\AddMissingMethodPhpDocRector; +use FastForward\DevTools\Rector\RemoveEmptyDocBlockRector; +use Rector\Config\RectorConfig; +use Rector\Configuration\PhpLevelSetResolver; +use Rector\DeadCode\Rector\ClassMethod\RemoveUselessParamTagRector; +use Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector; +use Rector\Php\PhpVersionResolver\ComposerJsonPhpVersionResolver; +use Rector\Set\ValueObject\SetList; + +use function Safe\getcwd; + +/** + * Factory for creating and extending the Rector configuration. + * + * This factory allows consumers to build on top of the base configuration provided + * by the dev-tools package instead of replacing it entirely. Consumers can extend + * the configuration with custom rules, skips, paths, and other options while + * preserving the ability to receive upstream updates automatically. + * + * Usage examples: + * + * 1. Using the factory method (recommended): + * ```php + * use FastForward\DevTools\Config\RectorConfigFactory; + * + * return static function (RectorConfig $rectorConfig): void { + * RectorConfigFactory::configure($rectorConfig); + * // Add custom configuration + * }; + * ``` + * + * 2. Using the require pattern: + * ```php + * return require __DIR__ . '/vendor/fast-forward/dev-tools/rector.php'; + * ``` + * + * 3. Extending with custom rules: + * ```php + * $configure = require __DIR__ . '/vendor/fast-forward/dev-tools/rector.php'; + * return static function (RectorConfig $rectorConfig) use ($configure): void { + * $configure($rectorConfig); + * $rectorConfig->rules([CustomRule::class]); + * }; + * ``` + */ +final class RectorConfigFactory +{ + /** + * Configures the given RectorConfig with the base dev-tools rules. + * + * This method applies the base configuration that consumers can further + * customize with additional rules, paths, skips, or configured rules. + * + * @param RectorConfig $rectorConfig the Rector configuration to configure + * + * @return void + */ + public static function configure(RectorConfig $rectorConfig): void + { + $cwd = getcwd(); + + $rectorConfig->sets([ + SetList::DEAD_CODE, + SetList::CODE_QUALITY, + SetList::CODING_STYLE, + SetList::TYPE_DECLARATION, + SetList::PRIVATIZATION, + SetList::INSTANCEOF, + SetList::EARLY_RETURN, + ]); + $rectorConfig->paths([$cwd]); + $rectorConfig->skip([ + $cwd . '/public', + $cwd . '/resources', + $cwd . '/vendor', + $cwd . '/tmp', + RemoveUselessReturnTagRector::class, + RemoveUselessParamTagRector::class, + ]); + $rectorConfig->cacheDirectory($cwd . '/tmp/cache/rector'); + $rectorConfig->importNames(); + $rectorConfig->removeUnusedImports(); + $rectorConfig->fileExtensions(['php']); + $rectorConfig->parallel(600); + $rectorConfig->rules([ + GeneratorPropertyFetchToMethodCallRector::class, + AddMissingMethodPhpDocRector::class, + RemoveEmptyDocBlockRector::class, + ]); + + $projectPhpVersion = ComposerJsonPhpVersionResolver::resolveFromCwdOrFail(); + $phpLevelSets = PhpLevelSetResolver::resolveFromPhpVersion($projectPhpVersion); + + $rectorConfig->sets($phpLevelSets); + + if (InstalledVersions::isInstalled('thecodingmachine/safe', false)) { + $packageLocation = InstalledVersions::getInstallPath('thecodingmachine/safe'); + $safeRectorMigrateFile = $packageLocation . '/rector-migrate.php'; + + if (file_exists($safeRectorMigrateFile)) { + $callback = require_once $safeRectorMigrateFile; + + if (is_callable($callback)) { + $callback($rectorConfig); + } + } + } + } + + /** + * Creates a configuration closure that applies the base dev-tools Rector configuration. + * + * This method returns a closure that can be used to configure a RectorConfig instance. + * The closure accepts an optional working directory to allow flexible usage across + * different projects. + * + * @param string|null $workingDirectory the working directory to use + * + * @return \Closure the configuration closure + */ + public static function configureWithWorkingDirectory(?string $workingDirectory = null): \Closure + { + $cwd = $workingDirectory ?? getcwd(); + + return static function (RectorConfig $rectorConfig) use ($cwd): void { + $rectorConfig->sets([ + SetList::DEAD_CODE, + SetList::CODE_QUALITY, + SetList::CODING_STYLE, + SetList::TYPE_DECLARATION, + SetList::PRIVATIZATION, + SetList::INSTANCEOF, + SetList::EARLY_RETURN, + ]); + $rectorConfig->paths([$cwd]); + $rectorConfig->skip([ + $cwd . '/public', + $cwd . '/resources', + $cwd . '/vendor', + $cwd . '/tmp', + RemoveUselessReturnTagRector::class, + RemoveUselessParamTagRector::class, + ]); + $rectorConfig->cacheDirectory($cwd . '/tmp/cache/rector'); + $rectorConfig->importNames(); + $rectorConfig->removeUnusedImports(); + $rectorConfig->fileExtensions(['php']); + $rectorConfig->parallel(600); + $rectorConfig->rules([ + GeneratorPropertyFetchToMethodCallRector::class, + AddMissingMethodPhpDocRector::class, + RemoveEmptyDocBlockRector::class, + ]); + + $projectPhpVersion = ComposerJsonPhpVersionResolver::resolveFromCwdOrFail(); + $phpLevelSets = PhpLevelSetResolver::resolveFromPhpVersion($projectPhpVersion); + + $rectorConfig->sets($phpLevelSets); + + if (InstalledVersions::isInstalled('thecodingmachine/safe', false)) { + $packageLocation = InstalledVersions::getInstallPath('thecodingmachine/safe'); + $safeRectorMigrateFile = $packageLocation . '/rector-migrate.php'; + + if (file_exists($safeRectorMigrateFile)) { + $callback = require_once $safeRectorMigrateFile; + + if (is_callable($callback)) { + $callback($rectorConfig); + } + } + } + }; + } +} diff --git a/tests/Config/RectorConfigFactoryTest.php b/tests/Config/RectorConfigFactoryTest.php new file mode 100644 index 0000000..045969d --- /dev/null +++ b/tests/Config/RectorConfigFactoryTest.php @@ -0,0 +1,67 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Config; + +use FastForward\DevTools\Config\RectorConfigFactory; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Rector\Config\RectorConfig; + +#[CoversClass(RectorConfigFactory::class)] +final class RectorConfigFactoryTest extends TestCase +{ + use ProphecyTrait; + + /** + * @return void + */ + #[Test] + public function configureWillConfigureRectorConfig(): void + { + $rectorConfig = new RectorConfig(); + + RectorConfigFactory::configure($rectorConfig); + + self::assertNotNull($rectorConfig); + } + + /** + * @return void + */ + #[Test] + public function configureWithWorkingDirectoryWillReturnClosure(): void + { + $closure = RectorConfigFactory::configureWithWorkingDirectory(); + + self::assertIsCallable($closure); + } + + /** + * @return void + */ + #[Test] + public function configureWithCustomWorkingDirectoryWillReturnClosure(): void + { + $closure = RectorConfigFactory::configureWithWorkingDirectory('/custom/path'); + + self::assertIsCallable($closure); + } +} From d22dde193fa9dccd74e2584ffec02c57da849973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Wed, 8 Apr 2026 22:41:00 -0300 Subject: [PATCH 3/3] Add --parallel support to tests command for ParaTest execution - Add --parallel option to TestsCommand with optional worker count - Use ParaTest when --parallel is enabled and available - Fall back to phpunit if paratest is not installed - Add tests for parallel execution scenarios Implements: #8 --- src/Command/TestsCommand.php | 38 ++++++++++++++++++- tests/Command/TestsCommandTest.php | 61 ++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/Command/TestsCommand.php b/src/Command/TestsCommand.php index 58d6ef3..a317e47 100644 --- a/src/Command/TestsCommand.php +++ b/src/Command/TestsCommand.php @@ -84,6 +84,13 @@ protected function configure(): void shortcut: 'f', mode: InputOption::VALUE_OPTIONAL, description: 'Filter which tests to run based on a pattern.', + ) + ->addOption( + name: 'parallel', + shortcut: 'p', + mode: InputOption::VALUE_OPTIONAL, + description: 'Run tests in parallel using ParaTest. Optional: specify number of workers (e.g., --parallel or --parallel=4).', + default: null, ); } @@ -102,8 +109,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Running PHPUnit tests...'); + $parallel = $input->getOption('parallel'); + $runner = $this->getTestRunner($parallel !== false ? $parallel : null); + $arguments = [ - $this->getAbsolutePath('vendor/bin/phpunit'), + $runner, '--configuration=' . parent::getConfigFile(self::CONFIG), '--bootstrap=' . $this->resolvePath($input, 'bootstrap'), '--display-deprecations', @@ -112,6 +122,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int '--display-skipped', ]; + if ($parallel !== null) { + if (is_numeric($parallel)) { + $arguments[] = '--processes=' . (int) $parallel; + } + $arguments[] = '--parallel'; + } + if (! $input->getOption('no-cache')) { $arguments[] = '--cache-directory=' . $this->resolvePath($input, 'cache-dir'); } @@ -156,4 +173,23 @@ private function resolvePath(InputInterface $input, string $option): string { return $this->getAbsolutePath($input->getOption($option)); } + + /** + * Determines the test runner to use based on parallel execution flag. + * + * The method MUST return the appropriate test runner binary path. + * If parallel is enabled, it SHALL return paratest; otherwise, it SHALL return phpunit. + * + * @param string|null $parallel the parallel option value + * + * @return string the path to the test runner binary + */ + private function getTestRunner(?string $parallel): string + { + if ($parallel !== null && $this->filesystem->exists($this->getAbsolutePath('vendor/bin/paratest'))) { + return $this->getAbsolutePath('vendor/bin/paratest'); + } + + return $this->getAbsolutePath('vendor/bin/phpunit'); + } } diff --git a/tests/Command/TestsCommandTest.php b/tests/Command/TestsCommandTest.php index 14b2058..fb9e4bc 100644 --- a/tests/Command/TestsCommandTest.php +++ b/tests/Command/TestsCommandTest.php @@ -137,4 +137,65 @@ public function executeWillReturnFailureIfProcessFails(): void self::assertSame(TestsCommand::FAILURE, $this->invokeExecute()); } + + /** + * @return void + */ + #[Test] + public function executeWithParallelOptionWillRunParaTest(): void + { + $this->filesystem->exists(getcwd() . '/vendor/bin/paratest')->willReturn(true); + + $this->willRunProcessWithCallback(function (Process $process): bool { + $commandLine = $process->getCommandLine(); + + return str_contains($commandLine, 'vendor/bin/paratest') + && str_contains($commandLine, '--parallel'); + }); + + $this->input->getOption('parallel') + ->willReturn('1'); + $this->invokeExecute(); + } + + /** + * @return void + */ + #[Test] + public function executeWithParallelOptionAndWorkerCount(): void + { + $this->filesystem->exists(getcwd() . '/vendor/bin/paratest')->willReturn(true); + + $this->willRunProcessWithCallback(function (Process $process): bool { + $commandLine = $process->getCommandLine(); + + return str_contains($commandLine, 'vendor/bin/paratest') + && str_contains($commandLine, '--processes=4') + && str_contains($commandLine, '--parallel'); + }); + + $this->input->getOption('parallel') + ->willReturn('4'); + $this->invokeExecute(); + } + + /** + * @return void + */ + #[Test] + public function executeWithParallelOptionButNoParaTestWillFallbackToPhpUnit(): void + { + $this->filesystem->exists(getcwd() . '/vendor/bin/paratest')->willReturn(false); + + $this->willRunProcessWithCallback(function (Process $process): bool { + $commandLine = $process->getCommandLine(); + + return str_contains($commandLine, 'vendor/bin/phpunit') + && ! str_contains($commandLine, 'paratest'); + }); + + $this->input->getOption('parallel') + ->willReturn('1'); + $this->invokeExecute(); + } }