diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 40861026..5bddb7cb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,17 @@ - + diff --git a/src/Command/ParallelCommand.php b/src/Command/ParallelCommand.php index 6f32ff19..0a6a1954 100644 --- a/src/Command/ParallelCommand.php +++ b/src/Command/ParallelCommand.php @@ -6,7 +6,6 @@ use Paraunit\Configuration\ParallelConfiguration; use Paraunit\Configuration\PHPUnitConfig; -use Paraunit\Configuration\PHPUnitOption; use Paraunit\Runner\Runner; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\FormatterHelper; @@ -19,52 +18,8 @@ class ParallelCommand extends Command { - /** @var PHPUnitOption[] */ - private readonly array $phpunitOptions; - public function __construct(protected ParallelConfiguration $configuration) { - $this->phpunitOptions = [ - new PHPUnitOption('whitelist'), - new PHPUnitOption('disable-coverage-ignore', false), - new PHPUnitOption('no-coverage', false), - - new PHPUnitOption('filter'), - new PHPUnitOption('testsuite'), - new PHPUnitOption('group'), - new PHPUnitOption('exclude-group'), - new PHPUnitOption('test-suffix'), - - new PHPUnitOption('dont-report-useless-tests', false), - new PHPUnitOption('strict-coverage', false), - new PHPUnitOption('strict-global-state', false), - new PHPUnitOption('disallow-test-output', false), - new PHPUnitOption('disallow-resource-usage', false), - new PHPUnitOption('enforce-time-limit', false), - new PHPUnitOption('disallow-todo-tests', false), - - new PHPUnitOption('fail-on-warning', false), - new PHPUnitOption('fail-on-risky', false), - - new PHPUnitOption('process-isolation', false), - new PHPUnitOption('globals-backup', false), - new PHPUnitOption('static-backup', false), - - new PHPUnitOption('loader'), - new PHPUnitOption('repeat'), - new PHPUnitOption('printer'), - - new PHPUnitOption('do-not-cache-result', false), - - new PHPUnitOption('prepend'), - new PHPUnitOption('bootstrap'), - new PHPUnitOption('no-configuration', false), - new PHPUnitOption('no-logging', false), - new PHPUnitOption('no-extensions', false), - new PHPUnitOption('include-path'), - new PHPUnitOption('stderr', false), - ]; - parent::__construct(); } @@ -79,15 +34,7 @@ protected function configure(): void $this->addOption('debug', null, InputOption::VALUE_NONE, 'Print verbose debug output'); $this->addOption('logo', null, InputOption::VALUE_NONE, 'Print the Shark logo at the top'); $this->addOption('pass-through', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Inject options to be passed directly to the underlying PHPUnit processes'); - - foreach ($this->phpunitOptions as $option) { - $this->addOption( - $option->getName(), - $option->getShortName(), - $option->hasValue() ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_NONE, - 'Option carried over to every single PHPUnit process, see PHPUnit docs for usage' - ); - } + $this->addOption('testsuite', null, InputOption::VALUE_REQUIRED, 'Only run tests from the specified test suite'); } /** @@ -101,7 +48,6 @@ protected function execute(InputInterface $input, OutputInterface $output): ?int $config = $container->get(PHPUnitConfig::class); $this->checkForExtension($config, $input, $output); $this->assertExtensionIsInstalled($config, $input); - $this->addPHPUnitOptions($config, $input); /** @var Runner $runner */ $runner = $container->get(Runner::class); @@ -144,39 +90,6 @@ private function checkForExtension(PHPUnitConfig $config, InputInterface $input, } } - private function addPHPUnitOptions(PHPUnitConfig $config, InputInterface $input): PHPUnitConfig - { - foreach ($this->phpunitOptions as $option) { - $cliOption = $input->getOption($option->getName()); - - if (\is_bool($cliOption)) { - $cliOption = null; - } - - if (null !== $cliOption && ! \is_string($cliOption)) { - throw new \InvalidArgumentException('Invalid option format for CLI option ' . $option->getName() . ': ' . gettype($cliOption)); - } - - if ($this->setOptionValue($option, $cliOption)) { - $config->addPhpunitOption($option); - } - } - - return $config; - } - - private function setOptionValue(PHPUnitOption $option, ?string $cliOption): bool - { - if (! $cliOption) { - return false; - } - if ($option->hasValue()) { - $option->setValue($cliOption); - } - - return true; - } - protected function assertExtensionIsInstalled(PHPUnitConfig $config, InputInterface $input): void { if (! $config->isParaunitExtensionRegistered()) { diff --git a/src/Configuration/PHPUnitConfig.php b/src/Configuration/PHPUnitConfig.php index 5940e980..38cc7eca 100644 --- a/src/Configuration/PHPUnitConfig.php +++ b/src/Configuration/PHPUnitConfig.php @@ -14,9 +14,6 @@ class PHPUnitConfig private readonly string $configFilename; - /** @var PHPUnitOption[] */ - private array $phpunitOptions = []; - private readonly Loader $xmlLoader; /** @@ -117,25 +114,6 @@ public function getBaseDirectory(): string return dirname($this->configFilename); } - public function addPhpunitOption(PHPUnitOption $option): void - { - $name = $option->getName(); - $this->phpunitOptions[$name] = $option; - } - - /** - * @return PHPUnitOption[] - */ - public function getPhpunitOptions(): array - { - return $this->phpunitOptions; - } - - public function getPhpunitOption(string $name): ?PHPUnitOption - { - return $this->phpunitOptions[$name] ?? null; - } - /** * @throws \InvalidArgumentException */ diff --git a/src/Configuration/PHPUnitOption.php b/src/Configuration/PHPUnitOption.php deleted file mode 100644 index c64b3dc5..00000000 --- a/src/Configuration/PHPUnitOption.php +++ /dev/null @@ -1,46 +0,0 @@ -name; - } - - public function getShortName(): ?string - { - return $this->shortName; - } - - public function setValue(string $value = null): void - { - $this->value = $value; - } - - public function getValue(): ?string - { - return $this->value; - } - - /** - * @psalm-assert-if-true string $this->value - * @psalm-assert-if-true string $this->getValue() - */ - public function hasValue(): bool - { - return $this->hasValue; - } -} diff --git a/src/Configuration/PassThrough.php b/src/Configuration/PassThrough.php index 72cba51f..83743d9f 100644 --- a/src/Configuration/PassThrough.php +++ b/src/Configuration/PassThrough.php @@ -6,6 +6,23 @@ class PassThrough { + private const DISALLOWED_OPTIONS = [ + '--no-configuration', + '--no-extensions', + '--no-logging', + '--coverage-php', + '--teamcity', + '--testdox', + '--atleast-version', + '--check-version', + '--generate-configuration', + '--migrate-configuration', + '--list-suites', + '--list-groups', + '--list-tests', + '--list-tests-xml', + ]; + /** @var list */ public readonly array $options; @@ -14,6 +31,12 @@ class PassThrough */ public function __construct(?array $options = []) { + foreach ($options ?? [] as $option) { + if (in_array($option, self::DISALLOWED_OPTIONS)) { + throw new \InvalidArgumentException('Invalid passed-through option: ' . $option); + } + } + $this->options = array_values($options ?? []); } } diff --git a/src/Process/CommandLine.php b/src/Process/CommandLine.php index 96e6db7c..4df1c318 100644 --- a/src/Process/CommandLine.php +++ b/src/Process/CommandLine.php @@ -7,7 +7,6 @@ use Paraunit\Configuration\ChunkSize; use Paraunit\Configuration\PHPUnitBinFile; use Paraunit\Configuration\PHPUnitConfig; -use Paraunit\Configuration\PHPUnitOption; class CommandLine { @@ -38,23 +37,9 @@ public function getOptions(PHPUnitConfig $config): array $options[] = '--configuration=' . $config->getFileFullPath(); } - foreach ($config->getPhpunitOptions() as $phpunitOption) { - $options[] = $this->buildPhpunitOptionString($phpunitOption); - } - return $options; } - private function buildPhpunitOptionString(PHPUnitOption $option): string - { - $optionString = '--' . $option->getName(); - if ($option->hasValue()) { - $optionString .= '=' . $option->getValue(); - } - - return $optionString; - } - /** * @return string[] */ diff --git a/tests/BaseIntegrationTestCase.php b/tests/BaseIntegrationTestCase.php index 5cd2dc6f..262fb789 100644 --- a/tests/BaseIntegrationTestCase.php +++ b/tests/BaseIntegrationTestCase.php @@ -140,7 +140,10 @@ protected function getParameter(string $parameterName): bool|int|float|string throw new \RuntimeException('Container not ready'); } - protected function loadContainer(): void + /** + * @param list $passThrough + */ + protected function loadContainer(array $passThrough = []): void { $input = $this->prophesize(InputInterface::class); $input->getArgument('stringFilter') @@ -151,6 +154,8 @@ protected function loadContainer(): void ->willReturn(1); $input->getOption('logo') ->willReturn(false); + $input->getOption('pass-through') + ->willReturn($passThrough); $input->getOption(Argument::cetera()) ->willReturn(null); $input->hasParameterOption(Argument::cetera()) diff --git a/tests/Functional/Command/ParallelCommandTest.php b/tests/Functional/Command/ParallelCommandTest.php index 477e93bc..20806c6b 100644 --- a/tests/Functional/Command/ParallelCommandTest.php +++ b/tests/Functional/Command/ParallelCommandTest.php @@ -146,14 +146,33 @@ public function testExecutionWithLogo(): void $commandTester = new CommandTester($command); $commandTester->execute([ 'command' => $command->getName(), - '--logo' => $configurationPath, - '--filter' => 'doNotExecuteAnyTestSoItsFaster', + '--configuration' => $configurationPath, + '--logo' => true, + 'stringFilter' => 'doNotExecuteAnyTestSoItsFaster', ]); $output = $commandTester->getDisplay(); $this->assertStringContainsString('BBBBbBBBBBBB', $output, 'Shark logo missing'); } + public function testRegressionExecutionWithStringFilter(): void + { + $configurationPath = $this->getConfigForStubs(); + $application = new Application(); + $application->add(new ParallelCommand(new ParallelConfiguration())); + + $command = $application->find('run'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + '--configuration' => $configurationPath, + 'stringFilter' => 'doNotExecuteAnyTestSoItsFaster', + ]); + + $output = $commandTester->getDisplay(); + $this->assertStringContainsString(' 0 tests', $output, 'Filter is not working'); + } + public function testExecutionWithDebugEnabled(): void { $configurationPath = $this->getConfigForStubs(); @@ -197,7 +216,7 @@ public function testExecutionWithParametersWithoutValue(): void 'command' => $command->getName(), '--configuration' => $configurationPath, 'stringFilter' => 'green', - '--dont-report-useless-tests' => true, + '--pass-through' => ['--dont-report-useless-tests'], ]); $this->assertSame(0, $exitCode); @@ -212,7 +231,7 @@ public function testExecutionWithoutConfiguration(): void $commandTester = new CommandTester($command); $exitCode = $commandTester->execute([ 'command' => $command->getName(), - '--filter' => 'do_not_execute_anything', + '--pass-through' => ['--filter=do_not_execute_anything'], ]); $output = $commandTester->getDisplay(); diff --git a/tests/Functional/Configuration/PassThroughTest.php b/tests/Functional/Configuration/PassThroughTest.php new file mode 100644 index 00000000..5afc0a46 --- /dev/null +++ b/tests/Functional/Configuration/PassThroughTest.php @@ -0,0 +1,202 @@ +expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($disallowedOption); + + new PassThrough([$disallowedOption]); + } + + public function testWarnAboutNewUnsupportedOptions(): void + { + $remaining = array_values(array_diff( + $this->getAllPHPUnitOptions(), + array_map(static fn (array $value): string => $value[0], self::allowedOptionsDataProvider()), + array_map(static fn (array $value): string => $value[0], self::disallowedOptionsDataProvider()), + $this->getAlreadySupportedOptions(), + $this->getPossibleFutureOptions(), + )); + + if ($remaining !== []) { + Facade::emitter()->testTriggeredWarning( + TestMethod::fromTestCase($this), + count($remaining) . ' unhandled new PHPUnit options: ' . print_r($remaining, true), + __FILE__, + __LINE__ + ); + } + } + + public function testAvoidOverlaps(): void + { + $overlapFutureOptions = array_intersect( + $this->getAlreadySupportedOptions(), + $this->getPossibleFutureOptions(), + ); + + $this->assertEmpty($overlapFutureOptions, 'Future option has been implemented: ' . print_r($overlapFutureOptions, true)); + } + + /** + * @return non-empty-list + */ + private function getAllPHPUnitOptions(): array + { + $helpText = (new Help(null, false))->generate(); + preg_match_all('/--[\w-]+/', $helpText, $options); + $this->assertNotEmpty($options); + + return $options[0]; + } + + /** + * @return non-empty-list + */ + private function getAlreadySupportedOptions(): array + { + $coverageCommand = new CoverageCommand($this->prophesize(CoverageConfiguration::class)->reveal()); + + $supportedOptions = array_map( + static fn (InputOption $option): string => '--' . $option->getName(), + $coverageCommand->getDefinition()->getOptions(), + ); + + $supportedOptions[] = '--filter'; + $supportedOptions[] = '--help'; + $supportedOptions[] = '--version'; + // TODO - map coverage modes automatically + $supportedOptions[] = '--coverage-clover'; + $supportedOptions[] = '--coverage-html'; + $supportedOptions[] = '--coverage-xml'; + $supportedOptions[] = '--coverage-text'; + $supportedOptions[] = '--coverage-crap4j'; + + return array_values($supportedOptions); + } + + /** + * @return array{string}[] + */ + public static function disallowedOptionsDataProvider(): array + { + return [ + // should be impossible? Paraunit can't identify tests in this way + ['--no-configuration'], + // Paraunit breaks if you use these + ['--no-extensions'], + ['--no-logging'], + ['--coverage-php'], + // not useful - they do not produce meaningful changes + ['--teamcity'], + ['--testdox'], + // not useful - they skip test execution + ['--atleast-version'], + ['--check-version'], + ['--generate-configuration'], + ['--migrate-configuration'], + ['--list-suites'], + ['--list-groups'], + ['--list-tests'], + ['--list-tests-xml'], + ]; + } + + /** + * @return array{0: string, 1?: string}[] + */ + public static function allowedOptionsDataProvider(): array + { + return [ + ['--bootstrap', ''], + ['--include-path '], + ['-d '], + ['--cache-directory', ''], + ['--testsuite', ''], + ['--group', ''], + ['--exclude-group', ''], + ['--covers', ''], + ['--uses', ''], + ['--process-isolation'], + ['--globals-backup'], + ['--static-backup'], + ['--strict-coverage'], + ['--strict-global-state'], + ['--disallow-test-output'], + ['--enforce-time-limit'], + ['--default-time-limit', ''], + ['--cache-result'], + ['--do-not-cache-result'], + ['--colors', ''], + ['--stderr'], + ['--no-progress'], + ['--no-output'], + ['--log-junit', ''], + ['--log-teamcity', ''], + ['--testdox-html', ''], + ['--testdox-text', ''], + ['--log-events-text', ''], + ['--log-events-verbose-text', ''], + ['--coverage-filter', ''], + ['--disable-coverage-ignore'], + ['--path-coverage'], + ['--no-coverage'], + ['--include-path'], + // TODO - add dedicated tests? + ['--fail-on-incomplete'], + ['--fail-on-risky'], + ['--fail-on-skipped'], + ['--fail-on-warning'], + ['--dont-report-useless-tests'], + ]; + } + + /** + * @return list + */ + private function getPossibleFutureOptions(): array + { + return [ + '--coverage-cobertura', + '--exclude-testsuite', + '--test-suffix', + '--columns', + '--display-incomplete', + '--display-skipped', + '--display-deprecations', + '--display-errors', + '--display-notices', + '--display-warnings', + '--reverse-list', + '--no-results', + '--stop-on-defect', + '--stop-on-error', + '--stop-on-failure', + '--stop-on-warning', + '--stop-on-risky', + '--stop-on-skipped', + '--stop-on-incomplete', + '--warm-coverage-cache', + '--order-by', + '--random-order-seed', + ]; + } +} diff --git a/tests/Functional/Runner/RunnerTest.php b/tests/Functional/Runner/RunnerTest.php index 87a18c02..fed97aed 100644 --- a/tests/Functional/Runner/RunnerTest.php +++ b/tests/Functional/Runner/RunnerTest.php @@ -5,8 +5,6 @@ namespace Tests\Functional\Runner; use Paraunit\Bin\Paraunit; -use Paraunit\Configuration\PHPUnitConfig; -use Paraunit\Configuration\PHPUnitOption; use Paraunit\Runner\Runner; use PHPUnit\Framework\Attributes\DataProvider; use Tests\BaseIntegrationTestCase; @@ -117,13 +115,7 @@ public function testWarning(): void public function testNoTestExecutedDoesntGetMistakenAsAbnormalTermination(): void { $this->setTextFilter('ThreeGreenTestStub.php'); - $this->loadContainer(); - - /** @var PHPUnitConfig $phpunitConfig */ - $phpunitConfig = $this->getService(PHPUnitConfig::class); - $option = new PHPUnitOption('group'); - $option->setValue('emptyGroup'); - $phpunitConfig->addPhpunitOption($option); + $this->loadContainer(['--group=emptyGroup']); $this->assertEquals(0, $this->executeRunner()); @@ -181,13 +173,7 @@ public function testRisky(): void public function testSessionStderr(): void { $this->setTextFilter('SessionTestStub.php'); - $this->loadContainer(); - - /** @var PHPUnitConfig $phpunitConfig */ - $phpunitConfig = $this->getService(PHPUnitConfig::class); - $option = new PHPUnitOption('stderr'); - $option->setValue(''); - $phpunitConfig->addPhpunitOption($option); + $this->loadContainer(['--stderr']); $output = $this->getConsoleOutput(); diff --git a/tests/Unit/Command/ParallelCommandTest.php b/tests/Unit/Command/ParallelCommandTest.php index 962149c3..7b2e5170 100644 --- a/tests/Unit/Command/ParallelCommandTest.php +++ b/tests/Unit/Command/ParallelCommandTest.php @@ -21,7 +21,6 @@ public function testExecute(): void $phpunitConfig = $this->prophesize(PHPUnitConfig::class); $phpunitConfig->isParaunitExtensionRegistered() ->willReturn(true); - $phpunitConfig->addPhpunitOption(Argument::cetera()); $runner = $this->prophesize(Runner::class); $runner->run() diff --git a/tests/Unit/Process/CommandLineTest.php b/tests/Unit/Process/CommandLineTest.php index c9709ef4..5ad5c073 100644 --- a/tests/Unit/Process/CommandLineTest.php +++ b/tests/Unit/Process/CommandLineTest.php @@ -7,7 +7,6 @@ use Paraunit\Configuration\ChunkSize; use Paraunit\Configuration\PHPUnitBinFile; use Paraunit\Configuration\PHPUnitConfig; -use Paraunit\Configuration\PHPUnitOption; use Paraunit\Process\CommandLine; use Tests\BaseUnitTestCase; @@ -29,26 +28,14 @@ public function testGetExecutable(): void public function testGetOptionsFor(): void { $config = $this->prophesize(PHPUnitConfig::class); - $config->getPhpunitOption('stderr') - ->willReturn(null); - $config->getFileFullPath() ->willReturn('/path/to/phpunit.xml'); - $optionWithValue = new PHPUnitOption('optVal'); - $optionWithValue->setValue('value'); - $config->getPhpunitOptions()->willReturn([ - new PHPUnitOption('opt', false), - $optionWithValue, - ]); - $phpunit = $this->prophesize(PHPUnitBinFile::class); $cli = new CommandLine($phpunit->reveal(), $this->mockChunkSize(false)); $options = $cli->getOptions($config->reveal()); $this->assertContains('--configuration=/path/to/phpunit.xml', $options); - $this->assertContains('--opt', $options); - $this->assertContains('--optVal=value', $options); $extensions = array_filter($options, static fn (string $a): bool => str_starts_with($a, '--extensions')); $this->assertCount(0, $extensions, '--extensions should no longer be used'); @@ -57,15 +44,9 @@ public function testGetOptionsFor(): void public function testGetOptionsChunkedNotContainsConfiguration(): void { $config = $this->prophesize(PHPUnitConfig::class); - $config->getPhpunitOption('stderr') - ->willReturn(null); - $config->getFileFullPath() ->willReturn('/path/to/phpunit.xml'); - $config->getPhpunitOptions() - ->willReturn([]); - $phpunit = $this->prophesize(PHPUnitBinFile::class); $cli = new CommandLine($phpunit->reveal(), $this->mockChunkSize(true)); diff --git a/tests/Unit/Process/CommandLineWithCoverageTest.php b/tests/Unit/Process/CommandLineWithCoverageTest.php index 216e4352..19a6540b 100644 --- a/tests/Unit/Process/CommandLineWithCoverageTest.php +++ b/tests/Unit/Process/CommandLineWithCoverageTest.php @@ -8,7 +8,6 @@ use Paraunit\Configuration\PHPDbgBinFile; use Paraunit\Configuration\PHPUnitBinFile; use Paraunit\Configuration\PHPUnitConfig; -use Paraunit\Configuration\PHPUnitOption; use Paraunit\Configuration\TempFilenameFactory; use Paraunit\Coverage\CoverageDriver; use Paraunit\Process\CommandLineWithCoverage; @@ -98,15 +97,7 @@ public function testGetExecutableWithNoDriverAvailable(): void public function testGetOptions(bool $enablePcov, bool $enableXdebug, ?int $xdebugVersion): void { $config = $this->prophesize(PHPUnitConfig::class); - $config->getPhpunitOption('stderr')->willReturn(null); $config->getFileFullPath()->willReturn('/path/to/phpunit.xml'); - $optionWithValue = new PHPUnitOption('optVal'); - $optionWithValue->setValue('value'); - $config->getPhpunitOptions() - ->willReturn([ - new PHPUnitOption('opt', false), - $optionWithValue, - ]); $phpunit = $this->prophesize(PHPUnitBinFile::class); @@ -122,8 +113,6 @@ public function testGetOptions(bool $enablePcov, bool $enableXdebug, ?int $xdebu $options = $cli->getOptions($config->reveal()); $this->assertContains('--configuration=/path/to/phpunit.xml', $options); - $this->assertContains('--opt', $options); - $this->assertContains('--optVal=value', $options); $extensions = array_filter($options, static fn (string $a): bool => str_starts_with($a, '--extensions')); $this->assertCount(0, $extensions, '--extensions should no longer be used'); @@ -132,15 +121,7 @@ public function testGetOptions(bool $enablePcov, bool $enableXdebug, ?int $xdebu public function testGetOptionsChunkedNotContainsConfiguration(): void { $config = $this->prophesize(PHPUnitConfig::class); - $config->getPhpunitOption('stderr')->willReturn(null); $config->getFileFullPath()->willReturn('/path/to/phpunit.xml'); - $optionWithValue = new PHPUnitOption('optVal'); - $optionWithValue->setValue('value'); - $config->getPhpunitOptions() - ->willReturn([ - new PHPUnitOption('opt', false), - $optionWithValue, - ]); $phpunit = $this->prophesize(PHPUnitBinFile::class);