diff --git a/bin/pest b/bin/pest index f2e14f32..c097363c 100755 --- a/bin/pest +++ b/bin/pest @@ -3,10 +3,11 @@ use NunoMaduro\Collision\Provider; use ParaTest\Console\Commands\ParaTestCommand; +use Pest\Actions\InteractsWithPlugins; use Pest\Actions\LoadStructure; -use Pest\Actions\MapArguments; use Pest\Actions\ValidatesEnvironment; use Pest\Console\Command; +use Pest\Support\Arr; use Pest\Support\Container; use Pest\TestSuite; use Symfony\Component\Console\Input\ArgvInput; @@ -54,13 +55,16 @@ use Symfony\Component\Console\Output\OutputInterface; } } + $_SERVER['argv'] = InteractsWithPlugins::handleArguments($_SERVER['argv']); + if ($argv->hasParameterOption('--parallel')) { LoadStructure::in($testSuite->rootPath); - MapArguments::toParatest($testSuite); exit(ParaTestCommand::applicationFactory($testSuite->rootPath)->run(new ArgvInput())); } - MapArguments::toPest($testSuite); + if (Arr::get($_SERVER, 'PARATEST', false) !== false) { + TestSuite::getInstance()->isInParallel = true; + } exit($container->get(Command::class)->run($_SERVER['argv'])); })(); diff --git a/composer.json b/composer.json index 471bd211..e5d1023a 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "php": "^7.3 || ^8.0", "nunomaduro/collision": "^5.4.0", "pestphp/pest-plugin": "^1.0.0", + "pestphp/pest-plugin-template": "1.x-dev", "phpunit/phpunit": "^9.5.5" }, "autoload": { @@ -44,7 +45,8 @@ "illuminate/console": "^8.47.0", "illuminate/support": "^8.47.0", "laravel/dusk": "^6.15.0", - "pestphp/pest-dev-tools": "dev-master" + "pestphp/pest-dev-tools": "dev-master", + "pestphp/pest-plugin-parallel": "1.x-dev" }, "minimum-stability": "dev", "prefer-stable": true, @@ -85,5 +87,11 @@ "Pest\\Laravel\\PestServiceProvider" ] } - } + }, + "repositories": [ + { + "type": "path", + "url": "../pest-plugin-parallel" + } + ] } diff --git a/src/Actions/InteractsWithPlugins.php b/src/Actions/InteractsWithPlugins.php new file mode 100644 index 00000000..e2a512be --- /dev/null +++ b/src/Actions/InteractsWithPlugins.php @@ -0,0 +1,33 @@ + $argv + * + * @return array + */ + public static function handleArguments(array $argv): array + { + $plugins = Loader::getPlugins(HandlesArguments::class); + + /** @var HandlesArguments $plugin */ + foreach ($plugins as $plugin) { + $argv = $plugin->handleArguments($argv); + } + + return $argv; + } +} diff --git a/src/Actions/MapArguments.php b/src/Actions/MapArguments.php deleted file mode 100644 index f38d6668..00000000 --- a/src/Actions/MapArguments.php +++ /dev/null @@ -1,112 +0,0 @@ -handleArguments($_SERVER['argv']); - } - } - - private static function parallel(): void - { - if (self::unsetArgument('--parallel')) { - self::setArgument('--runner', Runner::class); - } - } - - private static function inParallel(TestSuite $testSuite): void - { - if (self::unsetArgument('--isInParallel')) { - $testSuite->isInParallel = true; - } - } - - private static function color(): void - { - $argv = new ArgvInput(); - $isDecorated = $argv->getParameterOption('--colors', 'always') !== 'never'; - - self::unsetArgument('--colors'); - //refactor later - self::unsetArgument('--colors=always'); - self::unsetArgument('--colors=auto'); - self::unsetArgument('--colors=never'); - - if ($isDecorated) { - self::setArgument('--colors'); - } - } - - private static function coverage(): void - { - if (self::needsCoverage() && ! Coverage::isAvailable()) { - Container::getInstance()->get(OutputInterface::class)->writeln( - "\n ERROR No code coverage driver is available.", - ); - exit(1); - } - } - - private static function needsCoverage(): bool - { - foreach ($_SERVER['argv'] as $argument) { - if(strpos($argument, '--coverage',) === 0) { - return true; - } - } - - return false; - } - - private static function unsetArgument(string $argument): bool - { - if (($key = array_search($argument, $_SERVER['argv'])) !== false) { - unset($_SERVER['argv'][$key]); - - return true; - } - - return false; - } - - private static function setArgument(string $argument, string $value = null): void - { - $_SERVER['argv'][] = $argument; - - if ($value !== null) { - $_SERVER['argv'][] = $value; - } - } -} diff --git a/src/Console/Command.php b/src/Console/Command.php index 1d6ef21c..1ddc510f 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -9,7 +9,6 @@ use Pest\Actions\AddsTests; use Pest\Actions\LoadStructure; use Pest\Actions\ValidatesConfiguration; use Pest\Contracts\Plugins\AddsOutput; -use Pest\Contracts\Plugins\HandlesArguments; use Pest\Plugin\Loader; use Pest\Plugins\Version; use Pest\Support\Container; @@ -57,23 +56,10 @@ final class Command extends BaseCommand */ protected function handleArguments(array $argv): void { - /* - * First, let's call all plugins that want to handle arguments - */ - $plugins = Loader::getPlugins(HandlesArguments::class); - - /** @var HandlesArguments $plugin */ - foreach ($plugins as $plugin) { - $argv = $plugin->handleArguments($argv); - } - - /* - * Next, as usual, let's send the console arguments to PHPUnit. - */ parent::handleArguments($argv); /* - * Finally, let's validate the configuration. Making + * Let's validate the configuration. Making * sure all options are yet supported by Pest. */ ValidatesConfiguration::in($this->arguments); diff --git a/src/Console/Paratest/ExecutablePestTest.php b/src/Console/Paratest/ExecutablePestTest.php deleted file mode 100644 index 6debcde6..00000000 --- a/src/Console/Paratest/ExecutablePestTest.php +++ /dev/null @@ -1,31 +0,0 @@ -testCount = $testCount; - } - - public function getTestCount(): int - { - return $this->testCount; - } - - protected function prepareOptions(array $options): array - { - return $options; - } -} diff --git a/src/Console/Paratest/PestRunnerWorker.php b/src/Console/Paratest/PestRunnerWorker.php deleted file mode 100644 index fbddf2b1..00000000 --- a/src/Console/Paratest/PestRunnerWorker.php +++ /dev/null @@ -1,165 +0,0 @@ - - */ - public static $additionalOutput = []; - - public function __construct(OutputInterface $output, ExecutableTest $executableTest, Options $options, int $token) - { - $this->output = $output; - $this->executableTest = $executableTest; - - $phpFinder = new PhpExecutableFinder(); - $args = [$phpFinder->find(false)]; - $args = array_merge($args, $phpFinder->findArguments()); - - if (($passthruPhp = $options->passthruPhp()) !== null) { - $args = array_merge($args, $passthruPhp); - } - - $args = array_merge( - $args, - $this->executableTest->commandArguments( - $this->getPestBinary($options), - $options->filtered(), - $options->passthru() - ), - ['--isInParallel'], - ); - - $this->process = new Process($args, $options->cwd(), $options->fillEnvWithTokens($token)); - - $cmd = $this->process->getCommandLine(); - $this->assertValidCommandLineLength($cmd); - $this->executableTest->setLastCommand($cmd); - } - - public function getExecutableTest(): ExecutableTest - { - return $this->executableTest; - } - - /** - * Executes the test by creating a separate process. - */ - public function run(): void - { - $this->process->start(); - } - - /** - * Check if the process has terminated. - */ - public function isRunning(): bool - { - return $this->process->isRunning(); - } - - /** - * Stop the process and return it's - * exit code. - */ - public function stop(): ?int - { - $exitCode = $this->process->stop(); - $this->handleOutput($this->process->getOutput()); - return $exitCode; - } - - private function handleOutput(string $output) - { - $matches = []; - preg_match_all("/^\\n/m", $output, $matches, PREG_OFFSET_CAPTURE); - - $overview = substr($output, 0, $matches[0][1][1]); - $this->output->write($overview); - - if (count($matches[0]) > 3) { - $summarySectionIndex = count($matches[0]) - 2; - - static::$additionalOutput[] = substr( - $output, - $matches[0][1][1], - $matches[0][$summarySectionIndex][1] - $matches[0][1][1], - ); - } - } - - /** - * Assert that command line length is valid. - * - * In some situations process command line can became too long when combining different test - * cases in single --filter arguments so it's better to show error regarding that to user - * and propose him to decrease max batch size. - * - * @param string $cmd Command line - * - * @throws RuntimeException on too long command line - * - * @codeCoverageIgnore - */ - private function assertValidCommandLineLength(string $cmd): void - { - if (DIRECTORY_SEPARATOR !== '\\') { - return; - } - - // symfony's process wrapper - $cmd = 'cmd /V:ON /E:ON /C "(' . $cmd . ')'; - if (strlen($cmd) > 32767) { - throw new RuntimeException('Command line is too long, try to decrease max batch size'); - } - } - - private function getPestBinary(Options $options): string - { - $paths = [ - implode(DIRECTORY_SEPARATOR, [$options->cwd(), 'bin', 'pest']), - implode(DIRECTORY_SEPARATOR, [$options->cwd(), 'vendor', 'bin', 'pest']), - ]; - - return file_exists($paths[0]) ? $paths[0] : $paths[1]; - } - - public function getWorkerCrashedException(?Throwable $previousException = null): WorkerCrashedException - { - return WorkerCrashedException::fromProcess( - $this->process, - $this->process->getCommandLine(), - $previousException - ); - } -} diff --git a/src/Console/Paratest/Runner.php b/src/Console/Paratest/Runner.php deleted file mode 100644 index 749fb3cc..00000000 --- a/src/Console/Paratest/Runner.php +++ /dev/null @@ -1,361 +0,0 @@ -options = $options; - $this->output = $output; - $this->interpreter = new LogInterpreter(); - $this->printer = new ResultPrinter($this->interpreter, $output, $options); - - if (!$this->options->hasCoverage()) { - return; - } - - $this->coverage = new CoverageMerger($this->options->coverageTestLimit()); - } - - final public function run(): void - { - $this->load(new SuiteLoader($this->options, $this->output)); -// $this->printer->start(); - - $this->doRun(); - - $this->complete(); - } - - /** - * Builds the collection of pending ExecutableTest objects - * to run. If functional mode is enabled $this->pending will - * contain a collection of TestMethod objects instead of Suite - * objects. - */ - private function load(SuiteLoader $loader): void - { - $this->beforeLoadChecks(); - - $loader->load(); - $this->pending = $loader->getSuites(); - - $this->loadPestSuite(); - - $this->sortPending(); - - foreach ($this->pending as $pending) { -// $this->printer->addTest($pending); - } - } - - private function sortPending(): void - { - if ($this->options->orderBy() === Options::ORDER_RANDOM) { - mt_srand($this->options->randomOrderSeed()); - shuffle($this->pending); - } - - if ($this->options->orderBy() !== Options::ORDER_REVERSE) { - return; - } - - $this->pending = array_reverse($this->pending); - } - - /** - * Finalizes the run process. This method - * prints all results, rewinds the log interpreter, - * logs any results to JUnit, and cleans up temporary - * files. - */ - private function complete(): void - { - foreach(PestRunnerWorker::$additionalOutput as $output) { - $this->output->write($output); - } - PestRunnerWorker::$additionalOutput = []; -// $this->printer->printResults(); - $this->log(); - $this->logCoverage(); - $readers = $this->interpreter->getReaders(); - foreach ($readers as $reader) { - $reader->removeLog(); - } - } - - /** - * Returns the highest exit code encountered - * throughout the course of test execution. - */ - final public function getExitCode(): int - { - return $this->exitcode; - } - - /** - * Write output to JUnit format if requested. - */ - private function log(): void - { - if (($logJunit = $this->options->logJunit()) === null) { - return; - } - - $name = $this->options->path() ?? ''; - - $writer = new Writer($this->interpreter, $name); - $writer->write($logJunit); - } - - /** - * Write coverage to file if requested. - */ - private function logCoverage(): void - { - if (!$this->hasCoverage()) { - return; - } - - $coverageMerger = $this->getCoverage(); - assert($coverageMerger !== null); - $codeCoverage = $coverageMerger->getCodeCoverageObject(); - assert($codeCoverage !== null); - $codeCoverageConfiguration = null; - if (($configuration = $this->options->configuration()) !== null) { - $codeCoverageConfiguration = $configuration->codeCoverage(); - } - - $reporter = new CoverageReporter($codeCoverage, $codeCoverageConfiguration); - - $this->output->writeln(''); - $this->output->write('Generating code coverage report ... '); - - $timer = new Timer(); - $timer->start(); - - if (($coverageClover = $this->options->coverageClover()) !== null) { - $reporter->clover($coverageClover); - } - - if (($coverageCobertura = $this->options->coverageCobertura()) !== null) { - $reporter->cobertura($coverageCobertura); - } - - if (($coverageCrap4j = $this->options->coverageCrap4j()) !== null) { - $reporter->crap4j($coverageCrap4j); - } - - if (($coverageHtml = $this->options->coverageHtml()) !== null) { - $reporter->html($coverageHtml); - } - - if (($coverageText = $this->options->coverageText()) !== null) { - if ($coverageText === '') { - $this->output->write($reporter->text()); - } else { - file_put_contents($coverageText, $reporter->text()); - } - } - - if (($coverageXml = $this->options->coverageXml()) !== null) { - $reporter->xml($coverageXml); - } - - if (($coveragePhp = $this->options->coveragePhp()) !== null) { - $reporter->php($coveragePhp); - } - - $this->output->writeln( - sprintf('done [%s]', $timer->stop()->asString()) - ); - - if ($this->options->coveragePhp() && file_exists(Coverage::getPath())) { - Coverage::report($this->output); - } - } - - private function hasCoverage(): bool - { - return $this->options->hasCoverage(); - } - - private function getCoverage(): ?CoverageMerger - { - return $this->coverage; - } - - private function doRun(): void - { - $availableTokens = range(1, $this->options->processes()); - while (count($this->running) > 0 || count($this->pending) > 0) { - $this->fillRunQueue($availableTokens); - usleep(self::CYCLE_SLEEP); - - $availableTokens = []; - foreach ($this->running as $token => $test) { - if ($this->testIsStillRunning($test)) { - continue; - } - - unset($this->running[$token]); - $availableTokens[] = $token; - } - } - } - - private function fillRunQueue(array $availableTokens) - { - while ( - count($this->pending) > 0 - && count($this->running) < $this->options->processes() - && ($token = array_shift($availableTokens)) !== null - ) { - $executableTest = array_shift($this->pending); - - $this->running[$token] = new PestRunnerWorker($this->output, $executableTest, $this->options, $token); - $this->running[$token]->run(); - - if ($this->options->verbosity() < Options::VERBOSITY_VERY_VERBOSE) { - continue; - } - - $cmd = $this->running[$token]; - $this->output->write("\nExecuting test via: {$cmd->getExecutableTest()->getLastCommand()}\n"); - } - } - - /** - * Returns whether or not a test has finished being - * executed. If it has, this method also halts a test process - optionally - * throwing an exception if a fatal error has occurred - - * prints feedback, and updates the overall exit code. - * - * @throws Exception - */ - private function testIsStillRunning(PestRunnerWorker $worker) - { - if ($worker->isRunning()) { - return true; - } - - $this->exitcode = max($this->exitcode, (int) $worker->stop()); - if ($this->options->stopOnFailure() && $this->exitcode > 0) { - $this->pending = []; - } - - if ( - $this->exitcode > 0 - && $this->exitcode !== TestRunner::FAILURE_EXIT - && $this->exitcode !== TestRunner::EXCEPTION_EXIT - ) { - throw $worker->getWorkerCrashedException(); - } - - $executableTest = $worker->getExecutableTest(); - try { -// $this->printer->printFeedback($executableTest); - } catch (EmptyLogFileException $emptyLogFileException) { - throw $worker->getWorkerCrashedException($emptyLogFileException); - } - - if ($this->hasCoverage()) { - $coverageMerger = $this->getCoverage(); - assert($coverageMerger !== null); - $coverageMerger->addCoverageFromFile($executableTest->getCoverageFileName()); - } - - return false; - } - - private function beforeLoadChecks(): void - { - } - - private function loadPestSuite(): void - { - $pestTestSuite = TestSuite::getInstance(); - - $files = array_values(array_map(function (TestCaseFactory $factory): string { - return $factory->filename; - }, $pestTestSuite->tests->state)); - - $occurrences = array_count_values($files); - - $tests = array_values(array_map(function (int $occurrences, string $file) { - return new ExecutablePestTest( - $file, - $occurrences, - $this->options->hasCoverage(), - $this->options->hasLogTeamcity(), - $this->options->tmpDir(), - ); - }, $occurrences, array_keys($occurrences))); - - $this->pending = array_merge($this->pending, $tests); - } -} diff --git a/src/TestSuite.php b/src/TestSuite.php index 2fb0c7de..320b6804 100644 --- a/src/TestSuite.php +++ b/src/TestSuite.php @@ -74,7 +74,7 @@ final class TestSuite public $testPath; /** - * Determines if this test is running in parallel. + * Whether this test is running as part of a parallel suite. * * @var bool */ diff --git a/tests/Hooks/BeforeAllTest.php b/tests/Hooks/BeforeAllTest.php index c6925103..42efe051 100644 --- a/tests/Hooks/BeforeAllTest.php +++ b/tests/Hooks/BeforeAllTest.php @@ -8,7 +8,7 @@ global $globalHook; // HACK: we have to determine our $globalHook->calls baseline. This is because // two other tests are executed before this one due to filename ordering. $args = $_SERVER['argv'] ?? []; -$single = isset($args[1]) && Str::endsWith(__FILE__, $args[1]) || TestSuite::getInstance()->isInParallel; +$single = (isset($args[1]) && Str::endsWith(__FILE__, $args[1])) || TestSuite::getInstance()->isInParallel; $offset = $single ? 0 : 2; uses()->beforeAll(function () use ($globalHook, $offset) { diff --git a/tests/Visual/Success.php b/tests/Visual/Success.php index 8c5e1d6f..ade53e51 100644 --- a/tests/Visual/Success.php +++ b/tests/Visual/Success.php @@ -1,5 +1,7 @@ toContain(file_get_contents($snapshot)); } })->skip(!getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE')) + ->skip(TestSuite::getInstance()->isInParallel) ->skip(PHP_OS_FAMILY === 'Windows');