From c776bcf86d73e413a2c60875cb752685fc01c668 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 26 Jul 2021 20:47:14 -0400 Subject: [PATCH 01/64] add toThrow expectation --- src/Expectation.php | 57 +++++++++++++++++++++++++++++ tests/Features/Expect/toThrow.php | 60 +++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 tests/Features/Expect/toThrow.php diff --git a/src/Expectation.php b/src/Expectation.php index e41f21d0..4a6ae253 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -5,13 +5,19 @@ declare(strict_types=1); namespace Pest; use BadMethodCallException; +use Closure; +use LogicException; use Pest\Concerns\Extendable; use Pest\Concerns\RetrievesValues; use Pest\Support\Arr; +use Pest\Support\NullClosure; use PHPUnit\Framework\Assert; use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\ExpectationFailedException; +use ReflectionFunction; +use ReflectionNamedType; use SebastianBergmann\Exporter\Exporter; +use Throwable; /** * @internal @@ -715,6 +721,57 @@ final class Expectation return $this; } + /** + * Asserts that executing value throws an exception. + * + * @param string|Closure $exception string: the exception class + * Closure: first parameter = exception class + */ + public function toThrow($exception, string $exceptionMessage = null): Expectation + { + $callback = NullClosure::create(); + + if ($exception instanceof Closure) { + $callback = $exception; + $parameters = (new ReflectionFunction($exception))->getParameters(); + + if (1 !== count($parameters)) { + throw new LogicException('The "toThrow" closure must have a single parameter type-hinted as the class string'); + } + + if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { + throw new LogicException('The "toThrow" closure\'s parameter must be type-hinted as the class string'); + } + + $exception = $type->getName(); + } + + try { + ($this->value)(); + } catch (Throwable $e) { + if (!class_exists($exception)) { + Assert::assertStringContainsString($exception, $e->getMessage()); + + return $this; + } + + if ($exceptionMessage) { + Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); + } + + Assert::assertInstanceOf($exception, $e); + $callback($e); + + return $this; + } + + if (!class_exists($exception)) { + throw new ExpectationFailedException("Exception with message \"{$exception}\" not thrown."); + } + + throw new ExpectationFailedException("Exception \"{$exception}\" not thrown."); + } + /** * Exports the given value. * diff --git a/tests/Features/Expect/toThrow.php b/tests/Features/Expect/toThrow.php new file mode 100644 index 00000000..290003eb --- /dev/null +++ b/tests/Features/Expect/toThrow.php @@ -0,0 +1,60 @@ +toThrow(RuntimeException::class); + expect(function () { throw new RuntimeException(); })->toThrow(Exception::class); + expect(function () { throw new RuntimeException(); })->toThrow(function (RuntimeException $e) {}); + expect(function () { throw new RuntimeException('actual message'); })->toThrow(function (Exception $e) { + expect($e->getMessage())->toBe('actual message'); + }); + expect(function () {})->not->toThrow(Exception::class); + expect(function () { throw new RuntimeException('actual message'); })->toThrow('actual message'); + expect(function () { throw new Exception(); })->not->toThrow(RuntimeException::class); + expect(function () { throw new RuntimeException('actual message'); })->toThrow(RuntimeException::class, 'actual message'); + expect(function () { throw new RuntimeException('actual message'); })->toThrow(function (RuntimeException $e) {}, 'actual message'); +}); + +test('failures 1', function () { + expect(function () {})->toThrow(RuntimeException::class); +})->throws(ExpectationFailedException::class, 'Exception "' . RuntimeException::class . '" not thrown.'); + +test('failures 2', function () { + expect(function () {})->toThrow(function (RuntimeException $e) {}); +})->throws(ExpectationFailedException::class, 'Exception "' . RuntimeException::class . '" not thrown.'); + +test('failures 3', function () { + expect(function () { throw new Exception(); })->toThrow(function (RuntimeException $e) {}); +})->throws(ExpectationFailedException::class, 'Failed asserting that Exception Object'); + +test('failures 4', function () { + expect(function () { throw new Exception('actual message'); }) + ->toThrow(function (Exception $e) { + expect($e->getMessage())->toBe('expected message'); + }); +})->throws(ExpectationFailedException::class, 'Failed asserting that two strings are identical'); + +test('failures 5', function () { + expect(function () { throw new Exception('actual message'); })->toThrow('expected message'); +})->throws(ExpectationFailedException::class, 'Failed asserting that \'actual message\' contains "expected message".'); + +test('failures 6', function () { + expect(function () {})->toThrow('actual message'); +})->throws(ExpectationFailedException::class, 'Exception with message "actual message" not thrown'); + +test('failures 7', function () { + expect(function () { throw new RuntimeException('actual message'); })->toThrow(RuntimeException::class, 'expected message'); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(function () { throw new RuntimeException(); })->not->toThrow(RuntimeException::class); +})->throws(ExpectationFailedException::class); + +test('closure missing parameter', function () { + expect(function () {})->toThrow(function () {}); +})->throws(LogicException::class, 'The "toThrow" closure must have a single parameter type-hinted as the class string'); + +test('closure missing type-hint', function () { + expect(function () {})->toThrow(function ($e) {}); +})->throws(LogicException::class, 'The "toThrow" closure\'s parameter must be type-hinted as the class string'); From 0a3991c314d22837c0220f6ebbde436b0a1c0962 Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 5 Aug 2021 13:13:53 +0100 Subject: [PATCH 02/64] Initial working draft --- bin/pest | 14 + composer.json | 9 +- src/Console/Paratest/ExecutablePestTest.php | 31 ++ src/Console/Paratest/PestRunnerWorker.php | 122 +++++++ src/Console/Paratest/Runner.php | 361 ++++++++++++++++++++ src/Repositories/TestRepository.php | 2 +- 6 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 src/Console/Paratest/ExecutablePestTest.php create mode 100644 src/Console/Paratest/PestRunnerWorker.php create mode 100644 src/Console/Paratest/Runner.php diff --git a/bin/pest b/bin/pest index 649c6540..39302e87 100755 --- a/bin/pest +++ b/bin/pest @@ -2,8 +2,10 @@ getParameterOption('--test-directory', 'tests')); + $shouldExecuteInParallel = $argv->hasParameterOption('--parallel'); + // Let's remove the parallel option now we've retrieved its value + if (($parallelKey = array_search('--parallel', $_SERVER['argv'])) !== false) { + unset($_SERVER['argv'][$parallelKey]); + } $isDecorated = $argv->getParameterOption('--colors', 'always') !== 'never'; $output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated); @@ -51,5 +58,12 @@ use Symfony\Component\Console\Output\OutputInterface; } } + if ($shouldExecuteInParallel) { + $_SERVER['argv'][] = '--runner'; + $_SERVER['argv'][] = Runner::class; + + exit(ParaTestCommand::applicationFactory(getcwd())->run(new ArgvInput())); + } + exit($container->get(Command::class)->run($_SERVER['argv'])); })(); diff --git a/composer.json b/composer.json index 9a3960f9..95e9848b 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,7 @@ ] }, "require-dev": { + "brianium/paratest": "dev-pest-support", "illuminate/console": "^8.47.0", "illuminate/support": "^8.47.0", "laravel/dusk": "^6.15.0", @@ -84,5 +85,11 @@ "Pest\\Laravel\\PestServiceProvider" ] } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "../paratest" + } + ] } diff --git a/src/Console/Paratest/ExecutablePestTest.php b/src/Console/Paratest/ExecutablePestTest.php new file mode 100644 index 00000000..8a9d16e2 --- /dev/null +++ b/src/Console/Paratest/ExecutablePestTest.php @@ -0,0 +1,31 @@ +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 new file mode 100644 index 00000000..5e5ed0a5 --- /dev/null +++ b/src/Console/Paratest/PestRunnerWorker.php @@ -0,0 +1,122 @@ +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( + '/Users/luke/Packages/pest/bin/pest', + $options->filtered(), + $options->passthru() + ) + ); + + $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 + { + return $this->process->stop(); + } + + /** + * 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'); + } + } + + 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 new file mode 100644 index 00000000..aa9266c7 --- /dev/null +++ b/src/Console/Paratest/Runner.php @@ -0,0 +1,361 @@ +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 = $this->options->functional() + ? $loader->getTestMethods() + : $loader->getSuites(); + + $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 + { + $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. + */ + final protected 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. + */ + final protected 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->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()) + ); + } + + final protected function hasCoverage(): bool + { + return $this->options->hasCoverage(); + } + + final protected function getCoverage(): ?CoverageMerger + { + return $this->coverage; + } + + protected function doRun(): void + { + $this->loadPestSuite(); + + $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($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; + } + + protected 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 = $tests; + + // We need to reset the printer because we don't want to output + + foreach ($this->pending as $pending) { + $this->printer->addTest($pending); + } + } +} diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index 47684548..3a39b0a7 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -29,7 +29,7 @@ final class TestRepository /** * @var array */ - private $state = []; + public $state = []; /** * @var array>> From c4a659c3b55d3c5a0d8b5d94df97b19953e7e5a9 Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 5 Aug 2021 13:16:29 +0100 Subject: [PATCH 03/64] Composer changes --- composer.json | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 95e9848b..471bd211 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ ] }, "require-dev": { - "brianium/paratest": "dev-pest-support", + "brianium/paratest": "^6.3.0", "illuminate/console": "^8.47.0", "illuminate/support": "^8.47.0", "laravel/dusk": "^6.15.0", @@ -85,11 +85,5 @@ "Pest\\Laravel\\PestServiceProvider" ] } - }, - "repositories": [ - { - "type": "vcs", - "url": "../paratest" - } - ] + } } From 7ea138c64020885a9983352c0e53ff873ce6544f Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 5 Aug 2021 13:41:04 +0100 Subject: [PATCH 04/64] CS fixes --- src/Console/Paratest/ExecutablePestTest.php | 2 +- src/Console/Paratest/PestRunnerWorker.php | 10 +++--- src/Console/Paratest/Runner.php | 35 ++++++++++----------- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/Console/Paratest/ExecutablePestTest.php b/src/Console/Paratest/ExecutablePestTest.php index 8a9d16e2..6debcde6 100644 --- a/src/Console/Paratest/ExecutablePestTest.php +++ b/src/Console/Paratest/ExecutablePestTest.php @@ -9,7 +9,7 @@ class ExecutablePestTest extends ExecutableTest /** * The number of tests in this file. * - * @var int $testCount + * @var int */ private $testCount; diff --git a/src/Console/Paratest/PestRunnerWorker.php b/src/Console/Paratest/PestRunnerWorker.php index 5e5ed0a5..dfb777f7 100644 --- a/src/Console/Paratest/PestRunnerWorker.php +++ b/src/Console/Paratest/PestRunnerWorker.php @@ -4,19 +4,17 @@ declare(strict_types=1); namespace Pest\Console\Paratest; +use function array_merge; +use const DIRECTORY_SEPARATOR; use ParaTest\Runners\PHPUnit\ExecutableTest; use ParaTest\Runners\PHPUnit\Options; use ParaTest\Runners\PHPUnit\WorkerCrashedException; use RuntimeException; +use function strlen; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; use Throwable; -use function array_merge; -use function strlen; - -use const DIRECTORY_SEPARATOR; - /** * @internal */ @@ -94,7 +92,7 @@ final class PestRunnerWorker * * @param string $cmd Command line * - * @throws RuntimeException on too long command line. + * @throws RuntimeException on too long command line * * @codeCoverageIgnore */ diff --git a/src/Console/Paratest/Runner.php b/src/Console/Paratest/Runner.php index aa9266c7..d766b722 100644 --- a/src/Console/Paratest/Runner.php +++ b/src/Console/Paratest/Runner.php @@ -7,16 +7,13 @@ use ParaTest\Coverage\CoverageMerger; use ParaTest\Coverage\CoverageReporter; use ParaTest\Logging\JUnit\Writer; use ParaTest\Logging\LogInterpreter; -use ParaTest\Runners\PHPUnit\BaseRunner; use ParaTest\Runners\PHPUnit\EmptyLogFileException; use ParaTest\Runners\PHPUnit\ExecutableTest; -use ParaTest\Runners\PHPUnit\FullSuite; use ParaTest\Runners\PHPUnit\Options; use ParaTest\Runners\PHPUnit\ResultPrinter; use ParaTest\Runners\PHPUnit\RunnerInterface; use ParaTest\Runners\PHPUnit\SuiteLoader; use Pest\Factories\TestCaseFactory; -use Pest\Support\Container; use Pest\TestSuite; use PHPUnit\TextUI\TestRunner; use SebastianBergmann\Timer\Timer; @@ -24,13 +21,13 @@ use Symfony\Component\Console\Output\OutputInterface; final class Runner implements RunnerInterface { - protected const CYCLE_SLEEP = 10000; + private const CYCLE_SLEEP = 10000; /** @var Options */ - protected $options; + private $options; /** @var ResultPrinter */ - protected $printer; + private $printer; /** * A collection of ExecutableTest objects that have processes @@ -46,7 +43,7 @@ final class Runner implements RunnerInterface * * @var ExecutableTest[] */ - protected $pending = []; + private $pending = []; /** * A tallied exit code that returns the highest exit @@ -54,10 +51,10 @@ final class Runner implements RunnerInterface * * @var int */ - protected $exitcode = -1; + private $exitcode = -1; /** @var OutputInterface */ - protected $output; + private $output; /** @var LogInterpreter */ private $interpreter; @@ -76,7 +73,7 @@ final class Runner implements RunnerInterface $this->interpreter = new LogInterpreter(); $this->printer = new ResultPrinter($this->interpreter, $output, $options); - if (! $this->options->hasCoverage()) { + if (!$this->options->hasCoverage()) { return; } @@ -157,7 +154,7 @@ final class Runner implements RunnerInterface /** * Write output to JUnit format if requested. */ - final protected function log(): void + final private function log(): void { if (($logJunit = $this->options->logJunit()) === null) { return; @@ -172,9 +169,9 @@ final class Runner implements RunnerInterface /** * Write coverage to file if requested. */ - final protected function logCoverage(): void + final private function logCoverage(): void { - if (! $this->hasCoverage()) { + if (!$this->hasCoverage()) { return; } @@ -231,17 +228,17 @@ final class Runner implements RunnerInterface ); } - final protected function hasCoverage(): bool + final private function hasCoverage(): bool { return $this->options->hasCoverage(); } - final protected function getCoverage(): ?CoverageMerger + final private function getCoverage(): ?CoverageMerger { return $this->coverage; } - protected function doRun(): void + private function doRun(): void { $this->loadPestSuite(); @@ -326,7 +323,7 @@ final class Runner implements RunnerInterface return false; } - protected function beforeLoadChecks(): void + private function beforeLoadChecks(): void { } @@ -334,13 +331,13 @@ final class Runner implements RunnerInterface { $pestTestSuite = TestSuite::getInstance(); - $files = array_values(array_map(function(TestCaseFactory $factory): string { + $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) { + $tests = array_values(array_map(function (int $occurrences, string $file) { return new ExecutablePestTest( $file, $occurrences, From 5526d4c24d20f01aed1a42d47014a4c1bce1a6a7 Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 5 Aug 2021 13:51:55 +0100 Subject: [PATCH 05/64] Cleanup --- src/Console/Paratest/Runner.php | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Console/Paratest/Runner.php b/src/Console/Paratest/Runner.php index d766b722..56d31e42 100644 --- a/src/Console/Paratest/Runner.php +++ b/src/Console/Paratest/Runner.php @@ -99,10 +99,9 @@ final class Runner implements RunnerInterface private function load(SuiteLoader $loader): void { $this->beforeLoadChecks(); + $loader->load(); - $this->pending = $this->options->functional() - ? $loader->getTestMethods() - : $loader->getSuites(); + $this->loadPestSuite(); $this->sortPending(); @@ -154,7 +153,7 @@ final class Runner implements RunnerInterface /** * Write output to JUnit format if requested. */ - final private function log(): void + private function log(): void { if (($logJunit = $this->options->logJunit()) === null) { return; @@ -169,7 +168,7 @@ final class Runner implements RunnerInterface /** * Write coverage to file if requested. */ - final private function logCoverage(): void + private function logCoverage(): void { if (!$this->hasCoverage()) { return; @@ -228,20 +227,18 @@ final class Runner implements RunnerInterface ); } - final private function hasCoverage(): bool + private function hasCoverage(): bool { return $this->options->hasCoverage(); } - final private function getCoverage(): ?CoverageMerger + private function getCoverage(): ?CoverageMerger { return $this->coverage; } private function doRun(): void { - $this->loadPestSuite(); - $availableTokens = range(1, $this->options->processes()); while (count($this->running) > 0 || count($this->pending) > 0) { $this->fillRunQueue($availableTokens); @@ -348,11 +345,5 @@ final class Runner implements RunnerInterface }, $occurrences, array_keys($occurrences))); $this->pending = $tests; - - // We need to reset the printer because we don't want to output - - foreach ($this->pending as $pending) { - $this->printer->addTest($pending); - } } } From 256b167eafa04979f15c73c8dec7cd8ff6a7c2c1 Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 5 Aug 2021 15:31:09 +0100 Subject: [PATCH 06/64] Improvements --- bin/pest | 2 +- src/Console/Paratest/PestRunnerWorker.php | 13 ++++++++++++- src/Console/Paratest/Runner.php | 4 ++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/bin/pest b/bin/pest index 39302e87..4e7f94c2 100755 --- a/bin/pest +++ b/bin/pest @@ -62,7 +62,7 @@ use Symfony\Component\Console\Output\OutputInterface; $_SERVER['argv'][] = '--runner'; $_SERVER['argv'][] = Runner::class; - exit(ParaTestCommand::applicationFactory(getcwd())->run(new ArgvInput())); + exit(ParaTestCommand::applicationFactory($testSuite->rootPath)->run(new ArgvInput())); } exit($container->get(Command::class)->run($_SERVER['argv'])); diff --git a/src/Console/Paratest/PestRunnerWorker.php b/src/Console/Paratest/PestRunnerWorker.php index dfb777f7..decd2130 100644 --- a/src/Console/Paratest/PestRunnerWorker.php +++ b/src/Console/Paratest/PestRunnerWorker.php @@ -40,7 +40,7 @@ final class PestRunnerWorker $args = array_merge( $args, $this->executableTest->commandArguments( - '/Users/luke/Packages/pest/bin/pest', + $this->getPestBinary(), $options->filtered(), $options->passthru() ) @@ -109,6 +109,17 @@ final class PestRunnerWorker } } + private function getPestBinary(): string + { + // Used when Pest is required using composer. + $vendorPath = dirname(__DIR__, 7) . '/bin/pest'; + + // Used when Pest maintainers are running Pest tests. + $localPath = dirname(__DIR__, 3) . '/bin/pest'; + + return file_exists($vendorPath) ? $vendorPath : $localPath; + } + public function getWorkerCrashedException(?Throwable $previousException = null): WorkerCrashedException { return WorkerCrashedException::fromProcess( diff --git a/src/Console/Paratest/Runner.php b/src/Console/Paratest/Runner.php index 56d31e42..d30d24b4 100644 --- a/src/Console/Paratest/Runner.php +++ b/src/Console/Paratest/Runner.php @@ -13,6 +13,7 @@ use ParaTest\Runners\PHPUnit\Options; use ParaTest\Runners\PHPUnit\ResultPrinter; use ParaTest\Runners\PHPUnit\RunnerInterface; use ParaTest\Runners\PHPUnit\SuiteLoader; +use Pest\Actions\LoadStructure; use Pest\Factories\TestCaseFactory; use Pest\TestSuite; use PHPUnit\TextUI\TestRunner; @@ -100,6 +101,8 @@ final class Runner implements RunnerInterface { $this->beforeLoadChecks(); + LoadStructure::in(TestSuite::getInstance()->rootPath); + $loader->load(); $this->loadPestSuite(); @@ -327,6 +330,7 @@ final class Runner implements RunnerInterface private function loadPestSuite(): void { $pestTestSuite = TestSuite::getInstance(); + LoadStructure::in($pestTestSuite->rootPath); $files = array_values(array_map(function (TestCaseFactory $factory): string { return $factory->filename; From beca27599cc542edfb3657c142ec7aed1ff399bc Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 5 Aug 2021 16:18:47 +0100 Subject: [PATCH 07/64] Further cleanup --- src/Console/Paratest/Runner.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Console/Paratest/Runner.php b/src/Console/Paratest/Runner.php index d30d24b4..73b0def8 100644 --- a/src/Console/Paratest/Runner.php +++ b/src/Console/Paratest/Runner.php @@ -101,9 +101,9 @@ final class Runner implements RunnerInterface { $this->beforeLoadChecks(); - LoadStructure::in(TestSuite::getInstance()->rootPath); - $loader->load(); + $this->pending = $loader->getSuites(); + $this->loadPestSuite(); $this->sortPending(); @@ -330,7 +330,6 @@ final class Runner implements RunnerInterface private function loadPestSuite(): void { $pestTestSuite = TestSuite::getInstance(); - LoadStructure::in($pestTestSuite->rootPath); $files = array_values(array_map(function (TestCaseFactory $factory): string { return $factory->filename; @@ -348,6 +347,6 @@ final class Runner implements RunnerInterface ); }, $occurrences, array_keys($occurrences))); - $this->pending = $tests; + $this->pending = array_merge($this->pending, $tests); } } From 7a76f8dce27c62fdd03ca960fffa251e0fe55fc5 Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 5 Aug 2021 16:21:06 +0100 Subject: [PATCH 08/64] Further cleanup --- bin/pest | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/pest b/bin/pest index 4e7f94c2..75faf289 100755 --- a/bin/pest +++ b/bin/pest @@ -3,6 +3,7 @@ use NunoMaduro\Collision\Provider; use ParaTest\Console\Commands\ParaTestCommand; +use Pest\Actions\LoadStructure; use Pest\Actions\ValidatesEnvironment; use Pest\Console\Command; use Pest\Console\Paratest\Runner; @@ -62,6 +63,7 @@ use Symfony\Component\Console\Output\OutputInterface; $_SERVER['argv'][] = '--runner'; $_SERVER['argv'][] = Runner::class; + LoadStructure::in($testSuite->rootPath); exit(ParaTestCommand::applicationFactory($testSuite->rootPath)->run(new ArgvInput())); } From 1ca9aa5ca630eef350d515dbd07f03ee4206c45a Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 5 Aug 2021 16:43:09 +0100 Subject: [PATCH 09/64] Further cleanup --- src/Console/Paratest/Runner.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Console/Paratest/Runner.php b/src/Console/Paratest/Runner.php index 73b0def8..2a19e050 100644 --- a/src/Console/Paratest/Runner.php +++ b/src/Console/Paratest/Runner.php @@ -13,7 +13,6 @@ use ParaTest\Runners\PHPUnit\Options; use ParaTest\Runners\PHPUnit\ResultPrinter; use ParaTest\Runners\PHPUnit\RunnerInterface; use ParaTest\Runners\PHPUnit\SuiteLoader; -use Pest\Actions\LoadStructure; use Pest\Factories\TestCaseFactory; use Pest\TestSuite; use PHPUnit\TextUI\TestRunner; From 62aabc6ae1fec53da75eaac032eedb26838eba08 Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 5 Aug 2021 17:09:45 +0100 Subject: [PATCH 10/64] =?UTF-8?q?Bugfix.=20The=20TestCase=20is=20now=20awa?= =?UTF-8?q?re=20of=20if=20it=20is=20running=20in=20parallel=20or=20not=20?= =?UTF-8?q?=F0=9F=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/pest | 9 +++++++++ src/Console/Paratest/PestRunnerWorker.php | 18 +++++++++--------- src/TestSuite.php | 7 +++++++ tests/Hooks/BeforeAllTest.php | 3 ++- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/bin/pest b/bin/pest index 75faf289..2223cffd 100755 --- a/bin/pest +++ b/bin/pest @@ -35,12 +35,21 @@ use Symfony\Component\Console\Output\OutputInterface; $argv = new ArgvInput(); $testSuite = TestSuite::getInstance($rootPath, $argv->getParameterOption('--test-directory', 'tests')); + $shouldExecuteInParallel = $argv->hasParameterOption('--parallel'); // Let's remove the parallel option now we've retrieved its value if (($parallelKey = array_search('--parallel', $_SERVER['argv'])) !== false) { unset($_SERVER['argv'][$parallelKey]); } + if ($argv->hasParameterOption('--isInParallel')) { + $testSuite->isInParallel = true; + } + // Let's remove the parallel flag now we've retrieved its value + if (($parallelKey = array_search('--isInParallel', $_SERVER['argv'])) !== false) { + unset($_SERVER['argv'][$parallelKey]); + } + $isDecorated = $argv->getParameterOption('--colors', 'always') !== 'never'; $output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated); diff --git a/src/Console/Paratest/PestRunnerWorker.php b/src/Console/Paratest/PestRunnerWorker.php index decd2130..b1a4adc9 100644 --- a/src/Console/Paratest/PestRunnerWorker.php +++ b/src/Console/Paratest/PestRunnerWorker.php @@ -40,10 +40,11 @@ final class PestRunnerWorker $args = array_merge( $args, $this->executableTest->commandArguments( - $this->getPestBinary(), + $this->getPestBinary($options), $options->filtered(), $options->passthru() - ) + ), + ['--isInParallel'], ); $this->process = new Process($args, $options->cwd(), $options->fillEnvWithTokens($token)); @@ -109,15 +110,14 @@ final class PestRunnerWorker } } - private function getPestBinary(): string + private function getPestBinary(Options $options): string { - // Used when Pest is required using composer. - $vendorPath = dirname(__DIR__, 7) . '/bin/pest'; + $paths = [ + implode(DIRECTORY_SEPARATOR, [$options->cwd(), 'bin', 'pest']), + implode(DIRECTORY_SEPARATOR, [$options->cwd(), 'vendor', 'bin', 'pest']), + ]; - // Used when Pest maintainers are running Pest tests. - $localPath = dirname(__DIR__, 3) . '/bin/pest'; - - return file_exists($vendorPath) ? $vendorPath : $localPath; + return file_exists($paths[0]) ? $paths[0] : $paths[1]; } public function getWorkerCrashedException(?Throwable $previousException = null): WorkerCrashedException diff --git a/src/TestSuite.php b/src/TestSuite.php index f2b5ccde..2fb0c7de 100644 --- a/src/TestSuite.php +++ b/src/TestSuite.php @@ -73,6 +73,13 @@ final class TestSuite */ public $testPath; + /** + * Determines if this test is running in parallel. + * + * @var bool + */ + public $isInParallel = false; + /** * Holds an instance of the test suite. * diff --git a/tests/Hooks/BeforeAllTest.php b/tests/Hooks/BeforeAllTest.php index 05e57252..c6925103 100644 --- a/tests/Hooks/BeforeAllTest.php +++ b/tests/Hooks/BeforeAllTest.php @@ -1,13 +1,14 @@ 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]); +$single = isset($args[1]) && Str::endsWith(__FILE__, $args[1]) || TestSuite::getInstance()->isInParallel; $offset = $single ? 0 : 2; uses()->beforeAll(function () use ($globalHook, $offset) { From 463a50ebd44d6a5e9a8925bf82c3788ab5dc7792 Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 5 Aug 2021 17:39:23 +0100 Subject: [PATCH 11/64] Investigating bug fix --- .../SubFolder/SubFolder/CustomTestCaseInSubFolder.php | 2 +- tests/PHPUnit/Pest.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/CustomTestCaseInSubFolder.php b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/CustomTestCaseInSubFolder.php index ee6b1489..205c227a 100644 --- a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/CustomTestCaseInSubFolder.php +++ b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/CustomTestCaseInSubFolder.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\SubFolder\SubFolder\SubFolder; +namespace Tests\CustomTestCaseInSubFolders\SubFolder\SubFolder; use PHPUnit\Framework\TestCase; diff --git a/tests/PHPUnit/Pest.php b/tests/PHPUnit/Pest.php index 0a886265..0d513bde 100644 --- a/tests/PHPUnit/Pest.php +++ b/tests/PHPUnit/Pest.php @@ -1,3 +1,5 @@ in('CustomTestCaseInSubFolders/SubFolder'); +use Tests\CustomTestCaseInSubFolders\SubFolder\SubFolder\CustomTestCaseInSubFolder; + +uses(CustomTestCaseInSubFolder::class)->in('CustomTestCaseInSubFolders/SubFolder'); From 7621247bb7c31d4e79f798b673f2eca94427cf95 Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 5 Aug 2021 17:45:50 +0100 Subject: [PATCH 12/64] Adds workflows for parallel --- .github/workflows/tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 883e3fd6..6ea74077 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,8 +10,9 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] php: ['7.3', '7.4', '8.0'] dependency-version: [prefer-lowest, prefer-stable] + parallel: ['', '--parallel'] - name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} + name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} - ${{ matrix.parallel }} steps: - name: Checkout @@ -38,7 +39,7 @@ jobs: if: "matrix.php >= 8" - name: Unit Tests - run: php bin/pest --colors=always --exclude-group=integration + run: php bin/pest --colors=always --exclude-group=integration ${{ matrix.parallel }} - name: Integration Tests run: php bin/pest --colors=always --group=integration From 221248e691b91e61d9d5bf948b0738ae6a962d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Fri, 6 Aug 2021 15:25:28 +0200 Subject: [PATCH 13/64] introduced argument mapping, added pest coverage --- bin/pest | 24 ++------ src/Actions/MapArguments.php | 102 ++++++++++++++++++++++++++++++++ src/Console/Paratest/Runner.php | 7 +++ 3 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 src/Actions/MapArguments.php diff --git a/bin/pest b/bin/pest index 2223cffd..f2e14f32 100755 --- a/bin/pest +++ b/bin/pest @@ -4,9 +4,9 @@ use NunoMaduro\Collision\Provider; use ParaTest\Console\Commands\ParaTestCommand; use Pest\Actions\LoadStructure; +use Pest\Actions\MapArguments; use Pest\Actions\ValidatesEnvironment; use Pest\Console\Command; -use Pest\Console\Paratest\Runner; use Pest\Support\Container; use Pest\TestSuite; use Symfony\Component\Console\Input\ArgvInput; @@ -36,20 +36,6 @@ use Symfony\Component\Console\Output\OutputInterface; $testSuite = TestSuite::getInstance($rootPath, $argv->getParameterOption('--test-directory', 'tests')); - $shouldExecuteInParallel = $argv->hasParameterOption('--parallel'); - // Let's remove the parallel option now we've retrieved its value - if (($parallelKey = array_search('--parallel', $_SERVER['argv'])) !== false) { - unset($_SERVER['argv'][$parallelKey]); - } - - if ($argv->hasParameterOption('--isInParallel')) { - $testSuite->isInParallel = true; - } - // Let's remove the parallel flag now we've retrieved its value - if (($parallelKey = array_search('--isInParallel', $_SERVER['argv'])) !== false) { - unset($_SERVER['argv'][$parallelKey]); - } - $isDecorated = $argv->getParameterOption('--colors', 'always') !== 'never'; $output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated); @@ -68,13 +54,13 @@ use Symfony\Component\Console\Output\OutputInterface; } } - if ($shouldExecuteInParallel) { - $_SERVER['argv'][] = '--runner'; - $_SERVER['argv'][] = Runner::class; - + if ($argv->hasParameterOption('--parallel')) { LoadStructure::in($testSuite->rootPath); + MapArguments::toParatest($testSuite); exit(ParaTestCommand::applicationFactory($testSuite->rootPath)->run(new ArgvInput())); } + MapArguments::toPest($testSuite); + exit($container->get(Command::class)->run($_SERVER['argv'])); })(); diff --git a/src/Actions/MapArguments.php b/src/Actions/MapArguments.php new file mode 100644 index 00000000..11886f46 --- /dev/null +++ b/src/Actions/MapArguments.php @@ -0,0 +1,102 @@ +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 (! Coverage::isAvailable()) { + Container::getInstance()->get(OutputInterface::class)->writeln( + "\n ERROR No code coverage driver is available.", + ); + exit(1); + } + } + + 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/Paratest/Runner.php b/src/Console/Paratest/Runner.php index 2a19e050..138b5e3b 100644 --- a/src/Console/Paratest/Runner.php +++ b/src/Console/Paratest/Runner.php @@ -14,6 +14,7 @@ use ParaTest\Runners\PHPUnit\ResultPrinter; use ParaTest\Runners\PHPUnit\RunnerInterface; use ParaTest\Runners\PHPUnit\SuiteLoader; use Pest\Factories\TestCaseFactory; +use Pest\Plugins\Coverage; use Pest\TestSuite; use PHPUnit\TextUI\TestRunner; use SebastianBergmann\Timer\Timer; @@ -227,6 +228,12 @@ final class Runner implements RunnerInterface $this->output->writeln( sprintf('done [%s]', $timer->stop()->asString()) ); + + if ($this->options->coveragePhp() && file_exists(\Pest\Support\Coverage::getPath())) { + $coveragePlugin = new Coverage($this->output); + $coveragePlugin->coverage = true; + $coveragePlugin->addOutput(0); + } } private function hasCoverage(): bool From 8b295b5e9d53a71115e2ed10704851a77fb11a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Fri, 6 Aug 2021 15:38:59 +0200 Subject: [PATCH 14/64] remove debug statements --- src/Actions/MapArguments.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Actions/MapArguments.php b/src/Actions/MapArguments.php index 11886f46..16b1e0f7 100644 --- a/src/Actions/MapArguments.php +++ b/src/Actions/MapArguments.php @@ -17,12 +17,10 @@ final class MapArguments { public static function toParatest(TestSuite $testSuite): void { - var_dump($_SERVER['argv']); self::coverage(); self::registerPlugins(); self::parallel(); self::color(); - var_dump($_SERVER['argv']); } public static function toPest(TestSuite $testSuite): void From c86058fed1da4f924eda8c843f44d46b4bfb4c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Fri, 6 Aug 2021 15:39:19 +0200 Subject: [PATCH 15/64] use support class --- src/Console/Paratest/Runner.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Console/Paratest/Runner.php b/src/Console/Paratest/Runner.php index 138b5e3b..7c7b4b28 100644 --- a/src/Console/Paratest/Runner.php +++ b/src/Console/Paratest/Runner.php @@ -14,7 +14,7 @@ use ParaTest\Runners\PHPUnit\ResultPrinter; use ParaTest\Runners\PHPUnit\RunnerInterface; use ParaTest\Runners\PHPUnit\SuiteLoader; use Pest\Factories\TestCaseFactory; -use Pest\Plugins\Coverage; +use Pest\Support\Coverage; use Pest\TestSuite; use PHPUnit\TextUI\TestRunner; use SebastianBergmann\Timer\Timer; @@ -229,10 +229,8 @@ final class Runner implements RunnerInterface sprintf('done [%s]', $timer->stop()->asString()) ); - if ($this->options->coveragePhp() && file_exists(\Pest\Support\Coverage::getPath())) { - $coveragePlugin = new Coverage($this->output); - $coveragePlugin->coverage = true; - $coveragePlugin->addOutput(0); + if ($this->options->coveragePhp() && file_exists(Coverage::getPath())) { + Coverage::report($this->output); } } From 0b5321fdd7ecdafc19e44bdb1c93fbf62279c5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Fri, 6 Aug 2021 15:48:38 +0200 Subject: [PATCH 16/64] only check for coverage driver if option is present --- src/Actions/MapArguments.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Actions/MapArguments.php b/src/Actions/MapArguments.php index 16b1e0f7..57127325 100644 --- a/src/Actions/MapArguments.php +++ b/src/Actions/MapArguments.php @@ -17,8 +17,8 @@ final class MapArguments { public static function toParatest(TestSuite $testSuite): void { - self::coverage(); self::registerPlugins(); + self::coverage(); self::parallel(); self::color(); } @@ -26,6 +26,7 @@ final class MapArguments public static function toPest(TestSuite $testSuite): void { self::inParallel($testSuite); + // we could add coverage here too, so we stop before even running tests if there is no coverage driver } private static function registerPlugins(): void @@ -70,7 +71,7 @@ final class MapArguments private static function coverage(): void { - if (! Coverage::isAvailable()) { + if (self::needsCoverage() && ! Coverage::isAvailable()) { Container::getInstance()->get(OutputInterface::class)->writeln( "\n ERROR No code coverage driver is available.", ); @@ -78,6 +79,17 @@ final class MapArguments } } + private static function needsCoverage(): bool + { + foreach ($_SERVER['argv'] as $argument) { + if(str_starts_with($argument, '--coverage')) { + return true; + } + } + + return false; + } + private static function unsetArgument(string $argument): bool { if (($key = array_search($argument, $_SERVER['argv'])) !== false) { From 721d5134b7dbebdccc5f856121ed5f0a7a300de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Fri, 6 Aug 2021 16:00:03 +0200 Subject: [PATCH 17/64] replace `str_starts_with` to support pre php 8.0 --- src/Actions/MapArguments.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Actions/MapArguments.php b/src/Actions/MapArguments.php index 57127325..f38d6668 100644 --- a/src/Actions/MapArguments.php +++ b/src/Actions/MapArguments.php @@ -82,7 +82,7 @@ final class MapArguments private static function needsCoverage(): bool { foreach ($_SERVER['argv'] as $argument) { - if(str_starts_with($argument, '--coverage')) { + if(strpos($argument, '--coverage',) === 0) { return true; } } From 31d1b1b91dd4b54e54266599e32b306cc337a142 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 6 Aug 2021 15:15:09 +0100 Subject: [PATCH 18/64] Adds a little more spacing above the coverage output. --- src/Console/Paratest/Runner.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Console/Paratest/Runner.php b/src/Console/Paratest/Runner.php index 7c7b4b28..678eb37c 100644 --- a/src/Console/Paratest/Runner.php +++ b/src/Console/Paratest/Runner.php @@ -188,6 +188,7 @@ final class Runner implements RunnerInterface $reporter = new CoverageReporter($codeCoverage, $codeCoverageConfiguration); + $this->output->writeln(''); $this->output->write('Generating code coverage report ... '); $timer = new Timer(); From a760470e4868cc9ca0c40ef027382409cd2a6737 Mon Sep 17 00:00:00 2001 From: luke Date: Mon, 9 Aug 2021 18:57:20 +0100 Subject: [PATCH 19/64] Adds Pest output to parallel. --- src/Console/Paratest/PestRunnerWorker.php | 38 +++++++++++++++++++++-- src/Console/Paratest/Runner.php | 14 ++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/Console/Paratest/PestRunnerWorker.php b/src/Console/Paratest/PestRunnerWorker.php index b1a4adc9..fbddf2b1 100644 --- a/src/Console/Paratest/PestRunnerWorker.php +++ b/src/Console/Paratest/PestRunnerWorker.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest\Console\Paratest; +use Symfony\Component\Console\Output\OutputInterface; use function array_merge; use const DIRECTORY_SEPARATOR; use ParaTest\Runners\PHPUnit\ExecutableTest; @@ -22,11 +23,23 @@ final class PestRunnerWorker { /** @var ExecutableTest */ private $executableTest; + /** @var Process */ private $process; - public function __construct(ExecutableTest $executableTest, Options $options, int $token) + /** + * @var OutputInterface + */ + private $output; + + /** + * @var array + */ + public static $additionalOutput = []; + + public function __construct(OutputInterface $output, ExecutableTest $executableTest, Options $options, int $token) { + $this->output = $output; $this->executableTest = $executableTest; $phpFinder = new PhpExecutableFinder(); @@ -81,7 +94,28 @@ final class PestRunnerWorker */ public function stop(): ?int { - return $this->process->stop(); + $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], + ); + } } /** diff --git a/src/Console/Paratest/Runner.php b/src/Console/Paratest/Runner.php index 678eb37c..749fb3cc 100644 --- a/src/Console/Paratest/Runner.php +++ b/src/Console/Paratest/Runner.php @@ -84,7 +84,7 @@ final class Runner implements RunnerInterface final public function run(): void { $this->load(new SuiteLoader($this->options, $this->output)); - $this->printer->start(); +// $this->printer->start(); $this->doRun(); @@ -109,7 +109,7 @@ final class Runner implements RunnerInterface $this->sortPending(); foreach ($this->pending as $pending) { - $this->printer->addTest($pending); +// $this->printer->addTest($pending); } } @@ -135,7 +135,11 @@ final class Runner implements RunnerInterface */ private function complete(): void { - $this->printer->printResults(); + foreach(PestRunnerWorker::$additionalOutput as $output) { + $this->output->write($output); + } + PestRunnerWorker::$additionalOutput = []; +// $this->printer->printResults(); $this->log(); $this->logCoverage(); $readers = $this->interpreter->getReaders(); @@ -273,7 +277,7 @@ final class Runner implements RunnerInterface ) { $executableTest = array_shift($this->pending); - $this->running[$token] = new PestRunnerWorker($executableTest, $this->options, $token); + $this->running[$token] = new PestRunnerWorker($this->output, $executableTest, $this->options, $token); $this->running[$token]->run(); if ($this->options->verbosity() < Options::VERBOSITY_VERY_VERBOSE) { @@ -314,7 +318,7 @@ final class Runner implements RunnerInterface $executableTest = $worker->getExecutableTest(); try { - $this->printer->printFeedback($executableTest); +// $this->printer->printFeedback($executableTest); } catch (EmptyLogFileException $emptyLogFileException) { throw $worker->getWorkerCrashedException($emptyLogFileException); } From ef503646ee92224960cf773d77db6a88e4e8702e Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 10 Aug 2021 11:30:48 +0100 Subject: [PATCH 20/64] Removes parallel classes. --- bin/pest | 10 +- composer.json | 12 +- src/Actions/InteractsWithPlugins.php | 33 ++ src/Actions/MapArguments.php | 112 ------ src/Console/Command.php | 16 +- src/Console/Paratest/ExecutablePestTest.php | 31 -- src/Console/Paratest/PestRunnerWorker.php | 165 --------- src/Console/Paratest/Runner.php | 361 -------------------- src/TestSuite.php | 2 +- tests/Hooks/BeforeAllTest.php | 2 +- tests/Visual/Success.php | 3 + 11 files changed, 56 insertions(+), 691 deletions(-) create mode 100644 src/Actions/InteractsWithPlugins.php delete mode 100644 src/Actions/MapArguments.php delete mode 100644 src/Console/Paratest/ExecutablePestTest.php delete mode 100644 src/Console/Paratest/PestRunnerWorker.php delete mode 100644 src/Console/Paratest/Runner.php 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'); From 995088b522b59a036d97c109e6cebb798c91cca9 Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 10 Aug 2021 11:32:43 +0100 Subject: [PATCH 21/64] Refactor --- bin/pest | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/pest b/bin/pest index c097363c..5de793c6 100755 --- a/bin/pest +++ b/bin/pest @@ -58,7 +58,6 @@ use Symfony\Component\Console\Output\OutputInterface; $_SERVER['argv'] = InteractsWithPlugins::handleArguments($_SERVER['argv']); if ($argv->hasParameterOption('--parallel')) { - LoadStructure::in($testSuite->rootPath); exit(ParaTestCommand::applicationFactory($testSuite->rootPath)->run(new ArgvInput())); } From 5c7de5ad7526bb53aae5c18aa2064f0ecf5b1e0a Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 10 Aug 2021 12:04:15 +0100 Subject: [PATCH 22/64] Refactor --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index e5d1023a..335ca6b8 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,6 @@ "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": { From 892f70b5b553f386f1f874ee2335d643b5ab5c92 Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 10 Aug 2021 13:17:11 +0100 Subject: [PATCH 23/64] Refactor --- bin/pest | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bin/pest b/bin/pest index 5de793c6..cc31653f 100755 --- a/bin/pest +++ b/bin/pest @@ -61,9 +61,5 @@ use Symfony\Component\Console\Output\OutputInterface; exit(ParaTestCommand::applicationFactory($testSuite->rootPath)->run(new ArgvInput())); } - if (Arr::get($_SERVER, 'PARATEST', false) !== false) { - TestSuite::getInstance()->isInParallel = true; - } - exit($container->get(Command::class)->run($_SERVER['argv'])); })(); From bcab4224fb2ac6658e0945debf8a91778c00817e Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 10 Aug 2021 14:24:34 +0100 Subject: [PATCH 24/64] Skips a test that doesn't support parallel. --- bin/pest | 2 -- .../SubFolder/SubFolder/UsesPerSubDirectory.php | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/pest b/bin/pest index cc31653f..c18c3df9 100755 --- a/bin/pest +++ b/bin/pest @@ -4,10 +4,8 @@ use NunoMaduro\Collision\Provider; use ParaTest\Console\Commands\ParaTestCommand; use Pest\Actions\InteractsWithPlugins; -use Pest\Actions\LoadStructure; 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; diff --git a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/UsesPerSubDirectory.php b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/UsesPerSubDirectory.php index 01bb8456..e98a09e7 100644 --- a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/UsesPerSubDirectory.php +++ b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/UsesPerSubDirectory.php @@ -1,5 +1,7 @@ assertCustomInSubFolderTrue(); -}); +})->skip(TestSuite::getInstance()->isInParallel, 'Nested Pest.php files are not loaded in parallel.'); From 5c592928d48e7b149f1c13ad4f4bf6ff577ec0d2 Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 10 Aug 2021 14:34:10 +0100 Subject: [PATCH 25/64] Adds a new method, `isInParallel`, to the `Testable` trait to allow a test to determine its parallel status. --- src/Concerns/Testable.php | 8 ++++++++ src/Functions.php | 3 ++- tests/.snapshots/success.txt | 7 ++++++- tests/Features/Parallel.php | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/Features/Parallel.php diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 72bf02bb..8d5b5eed 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -304,4 +304,12 @@ trait Testable { return ltrim(self::class, 'P\\'); } + + /** + * Determine whether this test case is being executed in a parallel environment. + */ + public function isInParallel(): bool + { + return TestSuite::getInstance()->isInParallel; + } } diff --git a/src/Functions.php b/src/Functions.php index e7a230dc..55d4ba88 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Pest\Concerns\Testable; use Pest\Datasets; use Pest\Expectation; use Pest\PendingObjects\AfterEachCall; @@ -85,7 +86,7 @@ if (!function_exists('test')) { * is the test description; the second argument is * a closure that contains the test expectations. * - * @return TestCall|TestCase|mixed + * @return TestCall|TestCase|Testable|mixed */ function test(string $description = null, Closure $closure = null) { diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index a6f69129..bc9e5842 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -485,6 +485,11 @@ ✓ it can call chained macro method ✓ it will throw exception from call if no macro exists + WARN Tests\Features\Parallel + - it can determine in the test case if it is running in parallel + ✓ it can determine in the test case if it is not running in parallel + ✓ it can skip using the test case based on parallel status + PASS Tests\Features\PendingHigherOrderTests ✓ get 'foo' ✓ get 'foo' → get 'bar' → expect true → toBeTrue @@ -647,5 +652,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 419 passed + Tests: 4 incompleted, 10 skipped, 421 passed \ No newline at end of file diff --git a/tests/Features/Parallel.php b/tests/Features/Parallel.php new file mode 100644 index 00000000..9bfe6ea4 --- /dev/null +++ b/tests/Features/Parallel.php @@ -0,0 +1,15 @@ +isInParallel())->toBeTrue(); +})->skip(!TestSuite::getInstance()->isInParallel); + +it('can determine in the test case if it is not running in parallel', function () { + expect(test()->isInParallel())->toBeFalse(); +})->skip(TestSuite::getInstance()->isInParallel); + +it('can skip using the test case based on parallel status', function () { + expect(TestSuite::getInstance()->isInParallel)->toBeFalse(); +})->skip(function () { return $this->isInParallel(); }); From c7a2e68941ac7a73086052a0befa0746f417662b Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 10 Aug 2021 14:57:36 +0100 Subject: [PATCH 26/64] Fixes a problem with the PhpUnit logger --- src/Logging/TeamCity.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logging/TeamCity.php b/src/Logging/TeamCity.php index a9363b43..bfd732fb 100644 --- a/src/Logging/TeamCity.php +++ b/src/Logging/TeamCity.php @@ -286,7 +286,7 @@ final class TeamCity extends DefaultResultPrinter { $this->markAsFailure($t); $this->writeWarning($test->getName()); - $this->phpunitTeamCity->addSkippedTest($test, $t, $time); + $this->phpunitTeamCity->printIgnoredTest($test->getName(), $t, $time); } private function markAsFailure(Throwable $t): void From 79ddb1f58efa5e92e297b3f64fe897984c166c14 Mon Sep 17 00:00:00 2001 From: luke Date: Wed, 11 Aug 2021 15:57:49 +0100 Subject: [PATCH 27/64] Composer change --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 335ca6b8..7a433fe7 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "illuminate/support": "^8.47.0", "laravel/dusk": "^6.15.0", "pestphp/pest-dev-tools": "dev-master", - "pestphp/pest-plugin-parallel": "1.x-dev" + "pestphp/pest-plugin-parallel": "dev-master" }, "minimum-stability": "dev", "prefer-stable": true, From ab04aef56173a2146118fceff50360a1b0c66d64 Mon Sep 17 00:00:00 2001 From: luke Date: Wed, 11 Aug 2021 20:59:19 +0100 Subject: [PATCH 28/64] Refactors `addOutput` --- src/Actions/InteractsWithPlugins.php | 17 +++++++++++++++++ src/Console/Command.php | 12 ++---------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Actions/InteractsWithPlugins.php b/src/Actions/InteractsWithPlugins.php index e2a512be..d8c5d376 100644 --- a/src/Actions/InteractsWithPlugins.php +++ b/src/Actions/InteractsWithPlugins.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest\Actions; +use Pest\Contracts\Plugins\AddsOutput; use Pest\Contracts\Plugins\HandlesArguments; use Pest\Plugin\Loader; @@ -30,4 +31,20 @@ final class InteractsWithPlugins return $argv; } + + /** + * Provides an opportunity for any plugins that want + * to provide additional output after test execution. + */ + public static function addOutput(int $result): int + { + $plugins = Loader::getPlugins(AddsOutput::class); + + /** @var AddsOutput $plugin */ + foreach ($plugins as $plugin) { + $result = $plugin->addOutput($result); + } + + return $result; + } } diff --git a/src/Console/Command.php b/src/Console/Command.php index 1ddc510f..06750381 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -6,6 +6,7 @@ namespace Pest\Console; use Pest\Actions\AddsDefaults; use Pest\Actions\AddsTests; +use Pest\Actions\InteractsWithPlugins; use Pest\Actions\LoadStructure; use Pest\Actions\ValidatesConfiguration; use Pest\Contracts\Plugins\AddsOutput; @@ -114,16 +115,7 @@ final class Command extends BaseCommand LoadStructure::in($this->testSuite->rootPath); $result = parent::run($argv, false); - - /* - * Let's call all plugins that want to add output after test execution - */ - $plugins = Loader::getPlugins(AddsOutput::class); - - /** @var AddsOutput $plugin */ - foreach ($plugins as $plugin) { - $result = $plugin->addOutput($result); - } + $result = InteractsWithPlugins::addOutput($result); exit($result); } From e1f1fcccbeaa67bde6f62d2a697577c997605739 Mon Sep 17 00:00:00 2001 From: luke Date: Wed, 11 Aug 2021 21:25:41 +0100 Subject: [PATCH 29/64] Removes unneeded dependency --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 7a433fe7..4559649e 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,6 @@ ] }, "require-dev": { - "brianium/paratest": "^6.3.0", "illuminate/console": "^8.47.0", "illuminate/support": "^8.47.0", "laravel/dusk": "^6.15.0", From 5f0bd8180efb7ca62f766cbfe44f0f8a203ce462 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 12 Aug 2021 21:41:05 +0100 Subject: [PATCH 30/64] Update FUNDING.yml --- .github/FUNDING.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8e345cd7..ef6ff772 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,3 @@ # These are supported funding model platforms github: [nunomaduro,owenvoke,olivernybroe,octoper,lukeraymonddowning] -patreon: nunomaduro From a55b31e7c39faaf551b8b85ee291f35821df388b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 12 Aug 2021 21:41:42 +0100 Subject: [PATCH 31/64] Update FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ef6ff772..8e345cd7 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,4 @@ # These are supported funding model platforms github: [nunomaduro,owenvoke,olivernybroe,octoper,lukeraymonddowning] +patreon: nunomaduro From 5de981d923ca1642c54f090af031976089caf9a5 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 09:12:56 +0100 Subject: [PATCH 32/64] Updates composer.json --- composer.json | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 4559649e..4b815b45 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "illuminate/support": "^8.47.0", "laravel/dusk": "^6.15.0", "pestphp/pest-dev-tools": "dev-master", - "pestphp/pest-plugin-parallel": "dev-master" + "pestphp/pest-plugin-parallel": "^1.0" }, "minimum-stability": "dev", "prefer-stable": true, @@ -85,11 +85,5 @@ "Pest\\Laravel\\PestServiceProvider" ] } - }, - "repositories": [ - { - "type": "path", - "url": "../pest-plugin-parallel" - } - ] + } } From 28dd3c2a036242215831ae04769ef82465ca04fd Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 09:39:56 +0100 Subject: [PATCH 33/64] Adds `-P` as a parallel shortcut --- bin/pest | 19 ++++++++----------- src/Console/Command.php | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/bin/pest b/bin/pest index c18c3df9..254265f8 100755 --- a/bin/pest +++ b/bin/pest @@ -2,10 +2,7 @@ hasParameterOption('--test-directory')) { - foreach ($_SERVER['argv'] as $key => $value) { + foreach ($args as $key => $value) { if (strpos($value, '--test-directory') !== false) { - unset($_SERVER['argv'][$key]); + unset($args[$key]); } } } - $_SERVER['argv'] = InteractsWithPlugins::handleArguments($_SERVER['argv']); + $command = $argv->hasParameterOption(['--parallel', '-P']) + ? \Pest\Parallel\Command::class + : \Pest\Console\Command::class; - if ($argv->hasParameterOption('--parallel')) { - exit(ParaTestCommand::applicationFactory($testSuite->rootPath)->run(new ArgvInput())); - } - - exit($container->get(Command::class)->run($_SERVER['argv'])); + exit($container->get($command)->run($args)); })(); diff --git a/src/Console/Command.php b/src/Console/Command.php index 06750381..a6ce762e 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -9,8 +9,6 @@ use Pest\Actions\AddsTests; use Pest\Actions\InteractsWithPlugins; use Pest\Actions\LoadStructure; use Pest\Actions\ValidatesConfiguration; -use Pest\Contracts\Plugins\AddsOutput; -use Pest\Plugin\Loader; use Pest\Plugins\Version; use Pest\Support\Container; use Pest\TestSuite; @@ -57,6 +55,8 @@ final class Command extends BaseCommand */ protected function handleArguments(array $argv): void { + $argv = InteractsWithPlugins::handleArguments($argv); + parent::handleArguments($argv); /* From 45e76a6df66ae43de85c00c1af53f5a7146d221e Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 09:41:45 +0100 Subject: [PATCH 34/64] Typehint updates --- src/Actions/InteractsWithPlugins.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Actions/InteractsWithPlugins.php b/src/Actions/InteractsWithPlugins.php index d8c5d376..a115ab40 100644 --- a/src/Actions/InteractsWithPlugins.php +++ b/src/Actions/InteractsWithPlugins.php @@ -16,9 +16,9 @@ final class InteractsWithPlugins /** * Transform the input arguments by passing it to the relevant plugins. * - * @param array $argv + * @param array $argv * - * @return array + * @return array */ public static function handleArguments(array $argv): array { From 03d34e9a10a17726bdf586209c60588b55ce1b09 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 10:14:01 +0100 Subject: [PATCH 35/64] Removes `isInParallel` --- src/Concerns/Testable.php | 8 -------- src/TestSuite.php | 7 ------- tests/.snapshots/success.txt | 7 +------ tests/Features/Parallel.php | 15 --------------- tests/Hooks/BeforeAllTest.php | 3 +-- .../SubFolder/SubFolder/UsesPerSubDirectory.php | 4 +--- tests/PHPUnit/Pest.php | 5 ----- tests/Pest.php | 4 ++++ tests/Visual/Success.php | 5 +---- 9 files changed, 8 insertions(+), 50 deletions(-) delete mode 100644 tests/Features/Parallel.php delete mode 100644 tests/PHPUnit/Pest.php diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 8d5b5eed..72bf02bb 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -304,12 +304,4 @@ trait Testable { return ltrim(self::class, 'P\\'); } - - /** - * Determine whether this test case is being executed in a parallel environment. - */ - public function isInParallel(): bool - { - return TestSuite::getInstance()->isInParallel; - } } diff --git a/src/TestSuite.php b/src/TestSuite.php index 320b6804..f2b5ccde 100644 --- a/src/TestSuite.php +++ b/src/TestSuite.php @@ -73,13 +73,6 @@ final class TestSuite */ public $testPath; - /** - * Whether this test is running as part of a parallel suite. - * - * @var bool - */ - public $isInParallel = false; - /** * Holds an instance of the test suite. * diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index bc9e5842..a6f69129 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -485,11 +485,6 @@ ✓ it can call chained macro method ✓ it will throw exception from call if no macro exists - WARN Tests\Features\Parallel - - it can determine in the test case if it is running in parallel - ✓ it can determine in the test case if it is not running in parallel - ✓ it can skip using the test case based on parallel status - PASS Tests\Features\PendingHigherOrderTests ✓ get 'foo' ✓ get 'foo' → get 'bar' → expect true → toBeTrue @@ -652,5 +647,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 10 skipped, 421 passed + Tests: 4 incompleted, 9 skipped, 419 passed \ No newline at end of file diff --git a/tests/Features/Parallel.php b/tests/Features/Parallel.php deleted file mode 100644 index 9bfe6ea4..00000000 --- a/tests/Features/Parallel.php +++ /dev/null @@ -1,15 +0,0 @@ -isInParallel())->toBeTrue(); -})->skip(!TestSuite::getInstance()->isInParallel); - -it('can determine in the test case if it is not running in parallel', function () { - expect(test()->isInParallel())->toBeFalse(); -})->skip(TestSuite::getInstance()->isInParallel); - -it('can skip using the test case based on parallel status', function () { - expect(TestSuite::getInstance()->isInParallel)->toBeFalse(); -})->skip(function () { return $this->isInParallel(); }); diff --git a/tests/Hooks/BeforeAllTest.php b/tests/Hooks/BeforeAllTest.php index 42efe051..66f5801b 100644 --- a/tests/Hooks/BeforeAllTest.php +++ b/tests/Hooks/BeforeAllTest.php @@ -1,14 +1,13 @@ 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])) || ($_SERVER['PEST_PARALLEL'] ?? false); $offset = $single ? 0 : 2; uses()->beforeAll(function () use ($globalHook, $offset) { diff --git a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/UsesPerSubDirectory.php b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/UsesPerSubDirectory.php index e98a09e7..01bb8456 100644 --- a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/UsesPerSubDirectory.php +++ b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/UsesPerSubDirectory.php @@ -1,7 +1,5 @@ assertCustomInSubFolderTrue(); -})->skip(TestSuite::getInstance()->isInParallel, 'Nested Pest.php files are not loaded in parallel.'); +}); diff --git a/tests/PHPUnit/Pest.php b/tests/PHPUnit/Pest.php deleted file mode 100644 index 0d513bde..00000000 --- a/tests/PHPUnit/Pest.php +++ /dev/null @@ -1,5 +0,0 @@ -in('CustomTestCaseInSubFolders/SubFolder'); diff --git a/tests/Pest.php b/tests/Pest.php index d3fe584f..429bf74c 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,9 @@ in('PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder'); + uses()->group('integration')->in('Visual'); // NOTE: global test value container to be mutated and checked across files, as needed diff --git a/tests/Visual/Success.php b/tests/Visual/Success.php index ade53e51..4885255e 100644 --- a/tests/Visual/Success.php +++ b/tests/Visual/Success.php @@ -1,7 +1,5 @@ 'integration', 'REBUILD_SNAPSHOTS' => false])); + $process = (new Symfony\Component\Process\Process(['php', 'bin/pest'], dirname($testsPath), ['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0])); $process->run(); @@ -37,5 +35,4 @@ test('visual snapshot of test suite on success', function () { expect(implode("\n", $output))->toContain(file_get_contents($snapshot)); } })->skip(!getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE')) - ->skip(TestSuite::getInstance()->isInParallel) ->skip(PHP_OS_FAMILY === 'Windows'); From c2070cd99d44026d9bc8d8535782355bd9416b4a Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 10:19:10 +0100 Subject: [PATCH 36/64] Refactor --- src/Functions.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Functions.php b/src/Functions.php index 55d4ba88..e7a230dc 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -2,7 +2,6 @@ declare(strict_types=1); -use Pest\Concerns\Testable; use Pest\Datasets; use Pest\Expectation; use Pest\PendingObjects\AfterEachCall; @@ -86,7 +85,7 @@ if (!function_exists('test')) { * is the test description; the second argument is * a closure that contains the test expectations. * - * @return TestCall|TestCase|Testable|mixed + * @return TestCall|TestCase|mixed */ function test(string $description = null, Closure $closure = null) { From d9749ca65bb52818851391d62f90422ecbd89f1c Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 10:26:38 +0100 Subject: [PATCH 37/64] Adds a method for getting all filenames --- src/Repositories/TestRepository.php | 12 +++++++++++- tests/Unit/TestSuite.php | 12 ++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index 3a39b0a7..d43ed458 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -29,7 +29,7 @@ final class TestRepository /** * @var array */ - public $state = []; + private $state = []; /** * @var array>> @@ -44,6 +44,16 @@ final class TestRepository return count($this->state); } + /** + * @return array + */ + public function getFilenames(): array + { + return array_values(array_map(function (TestCaseFactory $factory): string { + return $factory->filename; + }, $this->state)); + } + /** * Calls the given callable foreach test case. */ diff --git a/tests/Unit/TestSuite.php b/tests/Unit/TestSuite.php index 62cc724d..96263a3a 100644 --- a/tests/Unit/TestSuite.php +++ b/tests/Unit/TestSuite.php @@ -22,3 +22,15 @@ it('alerts users about tests with arguments but no input', function () { DatasetMissing::class, sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'foo', 1, 'int $arg', __FILE__), ); + +it('can return an array of all test suite filenames', function() { + $testSuite = new TestSuite(getcwd(), 'tests'); + $test = function () {}; + $testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test)); + $testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'bar', $test)); + + expect($testSuite->tests->getFilenames())->toEqual([ + __FILE__, + __FILE__, + ]); +}); From cadae52d5d508df964f5425136a9f9715b471c2d Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 10:29:21 +0100 Subject: [PATCH 38/64] Updates snapshots --- tests/.snapshots/success.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index a6f69129..54cbeb84 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -614,6 +614,7 @@ PASS Tests\Unit\TestSuite ✓ it does not allow to add the same test description twice ✓ it alerts users about tests with arguments but no input + ✓ it can return an array of all test suite filenames PASS Tests\Visual\Help ✓ visual snapshot of help command output @@ -647,5 +648,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 419 passed + Tests: 4 incompleted, 9 skipped, 420 passed \ No newline at end of file From 2887d212e38b124372a91b07fe9680f40be60955 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 10:30:26 +0100 Subject: [PATCH 39/64] CS fix --- tests/Unit/TestSuite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/TestSuite.php b/tests/Unit/TestSuite.php index 96263a3a..8d4cbf05 100644 --- a/tests/Unit/TestSuite.php +++ b/tests/Unit/TestSuite.php @@ -23,7 +23,7 @@ it('alerts users about tests with arguments but no input', function () { sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'foo', 1, 'int $arg', __FILE__), ); -it('can return an array of all test suite filenames', function() { +it('can return an array of all test suite filenames', function () { $testSuite = new TestSuite(getcwd(), 'tests'); $test = function () {}; $testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test)); From c6435d560613e98a1dddb82fecc1da5b9f1d1314 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 10:44:11 +0100 Subject: [PATCH 40/64] Adds a helpful message for users trying to run parallel without the plugin --- bin/pest | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bin/pest b/bin/pest index 254265f8..7e2959ec 100755 --- a/bin/pest +++ b/bin/pest @@ -5,6 +5,7 @@ use NunoMaduro\Collision\Provider; use Pest\Actions\ValidatesEnvironment; use Pest\Support\Container; use Pest\TestSuite; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; @@ -52,9 +53,11 @@ use Symfony\Component\Console\Output\OutputInterface; } } - $command = $argv->hasParameterOption(['--parallel', '-P']) - ? \Pest\Parallel\Command::class - : \Pest\Console\Command::class; + if ($runInParallel = $argv->hasParameterOption(['--parallel', '-P']) && !class_exists(\Pest\Parallel\Command::class)) { + $output->writeln("Parallel support requires the Pest Parallel plugin. Run `composer require --dev pestphp/pest-plugin-parallel` first."); + exit(Command::FAILURE); + } + $command = $runInParallel ? \Pest\Parallel\Command::class : \Pest\Console\Command::class; exit($container->get($command)->run($args)); })(); From b6c06e8c30005008c505e9691c705a556f6e8265 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 10:46:07 +0100 Subject: [PATCH 41/64] Bugfix --- bin/pest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/pest b/bin/pest index 7e2959ec..5fd0d896 100755 --- a/bin/pest +++ b/bin/pest @@ -53,7 +53,7 @@ use Symfony\Component\Console\Output\OutputInterface; } } - if ($runInParallel = $argv->hasParameterOption(['--parallel', '-P']) && !class_exists(\Pest\Parallel\Command::class)) { + if (($runInParallel = $argv->hasParameterOption(['--parallel', '-P'])) && !class_exists(\Pest\Parallel\Command::class)) { $output->writeln("Parallel support requires the Pest Parallel plugin. Run `composer require --dev pestphp/pest-plugin-parallel` first."); exit(Command::FAILURE); } From 5c84b0c6d39ca243b404ae3a185abff9e703128f Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 11:07:52 +0100 Subject: [PATCH 42/64] The `getFilenames` method now respects `only` calls. --- src/Repositories/TestRepository.php | 22 ++++++++++++++++++---- tests/.snapshots/success.txt | 3 ++- tests/Unit/TestSuite.php | 15 +++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index d43ed458..305e8752 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -45,13 +45,17 @@ final class TestRepository } /** + * Returns the filename of each test that should be run in the suite. + * * @return array */ public function getFilenames(): array { + $testsWithOnly = $this->testsUsingOnly(); + return array_values(array_map(function (TestCaseFactory $factory): string { return $factory->filename; - }, $this->state)); + }, count($testsWithOnly) > 0 ? $testsWithOnly : $this->state)); } /** @@ -95,9 +99,7 @@ final class TestRepository } } - $onlyState = array_filter($this->state, function ($testFactory): bool { - return $testFactory->only; - }); + $onlyState = $this->testsUsingOnly(); $state = count($onlyState) > 0 ? $onlyState : $this->state; @@ -110,6 +112,18 @@ final class TestRepository } } + /** + * Return all tests that have called the only method. + * + * @return array + */ + private function testsUsingOnly(): array + { + return array_filter($this->state, function ($testFactory): bool { + return $testFactory->only; + }); + } + /** * Uses the given `$testCaseClass` on the given `$paths`. * diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 54cbeb84..30b28483 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -615,6 +615,7 @@ ✓ it does not allow to add the same test description twice ✓ it alerts users about tests with arguments but no input ✓ it can return an array of all test suite filenames + ✓ it can filter the test suite filenames to those with the only method PASS Tests\Visual\Help ✓ visual snapshot of help command output @@ -648,5 +649,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 420 passed + Tests: 4 incompleted, 9 skipped, 421 passed \ No newline at end of file diff --git a/tests/Unit/TestSuite.php b/tests/Unit/TestSuite.php index 8d4cbf05..7d6c88a9 100644 --- a/tests/Unit/TestSuite.php +++ b/tests/Unit/TestSuite.php @@ -34,3 +34,18 @@ it('can return an array of all test suite filenames', function () { __FILE__, ]); }); + +it('can filter the test suite filenames to those with the only method', function () { + $testSuite = new TestSuite(getcwd(), 'tests'); + $test = function () {}; + + $testWithOnly = new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test); + $testWithOnly->only = true; + $testSuite->tests->set($testWithOnly); + + $testSuite->tests->set(new \Pest\Factories\TestCaseFactory('Baz/Bar/Boo.php', 'bar', $test)); + + expect($testSuite->tests->getFilenames())->toEqual([ + __FILE__, + ]); +}); From 4dfc02c5dad428f34ee560d21060f1dde21fb7c4 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 11:19:03 +0100 Subject: [PATCH 43/64] Refactor --- src/Repositories/TestRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index 305e8752..c3fe2877 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -45,7 +45,7 @@ final class TestRepository } /** - * Returns the filename of each test that should be run in the suite. + * Returns the filename of each test that should be executed in the suite. * * @return array */ From 0368c4846f5637f46966a6f638ce8a42d6aea050 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 13:40:07 +0100 Subject: [PATCH 44/64] Adds a `composer test:parallel` script --- composer.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4b815b45..3309bcf3 100644 --- a/composer.json +++ b/composer.json @@ -60,12 +60,14 @@ "test:lint": "php-cs-fixer fix -v --dry-run", "test:types": "phpstan analyse --ansi --memory-limit=-1", "test:unit": "php bin/pest --colors=always --exclude-group=integration", + "test:parallel": "php bin/pest -P --colors=always --exclude-group=integration", "test:integration": "php bin/pest --colors=always --group=integration", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always", "test": [ "@test:lint", "@test:types", "@test:unit", + "@test:parallel", "@test:integration" ] }, @@ -85,5 +87,11 @@ "Pest\\Laravel\\PestServiceProvider" ] } - } + }, + "repositories": [ + { + "type": "path", + "url": "../pest-plugin-parallel" + } + ] } From 490b2d66e5e285a8e0ee15e0c20804314691b2f5 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 13:41:00 +0100 Subject: [PATCH 45/64] Fix --- composer.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 3309bcf3..29a20f31 100644 --- a/composer.json +++ b/composer.json @@ -87,11 +87,5 @@ "Pest\\Laravel\\PestServiceProvider" ] } - }, - "repositories": [ - { - "type": "path", - "url": "../pest-plugin-parallel" - } - ] + } } From 578e97123dc5f79cc2a0b49303758226aac865a5 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 13 Aug 2021 22:09:55 +0100 Subject: [PATCH 46/64] Changes -P to -p --- bin/pest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/pest b/bin/pest index 5fd0d896..2735abfc 100755 --- a/bin/pest +++ b/bin/pest @@ -53,7 +53,7 @@ use Symfony\Component\Console\Output\OutputInterface; } } - if (($runInParallel = $argv->hasParameterOption(['--parallel', '-P'])) && !class_exists(\Pest\Parallel\Command::class)) { + if (($runInParallel = $argv->hasParameterOption(['--parallel', '-p'])) && !class_exists(\Pest\Parallel\Command::class)) { $output->writeln("Parallel support requires the Pest Parallel plugin. Run `composer require --dev pestphp/pest-plugin-parallel` first."); exit(Command::FAILURE); } From 0b0beac122803d2dcb2713a0b9dbee0d0b488ab2 Mon Sep 17 00:00:00 2001 From: Luke Downing Date: Mon, 16 Aug 2021 22:12:13 +0100 Subject: [PATCH 47/64] Script update --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 29a20f31..99ee6a8c 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "test:lint": "php-cs-fixer fix -v --dry-run", "test:types": "phpstan analyse --ansi --memory-limit=-1", "test:unit": "php bin/pest --colors=always --exclude-group=integration", - "test:parallel": "php bin/pest -P --colors=always --exclude-group=integration", + "test:parallel": "php bin/pest -p --colors=always --exclude-group=integration", "test:integration": "php bin/pest --colors=always --group=integration", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always", "test": [ From eca5f89e595884e26860b8aec5cfc7d892e9f942 Mon Sep 17 00:00:00 2001 From: Luke Downing Date: Thu, 19 Aug 2021 17:07:45 +0100 Subject: [PATCH 48/64] release: v1.16.0 --- CHANGELOG.md | 4 ++++ src/Pest.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d4d5e82..f576bce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [v1.16.0 (2021-08-19)](https://github.com/pestphp/pest/compare/v1.15.0...v1.16.0) +### Added +- Support for new parallel options ([#369](https://github.com/pestphp/pest/pull/369)) + ## [v1.15.0 (2021-08-04)](https://github.com/pestphp/pest/compare/v1.14.0...v1.15.0) ### Added - `toBeTruthy` and `toBeFalsy` ([#367](https://github.com/pestphp/pest/pull/367)) diff --git a/src/Pest.php b/src/Pest.php index c6d45d15..9c921b4d 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '1.15.0'; + return '1.16.0'; } function testDirectory(string $file = ''): string From 2125bf9668a20c9cef89add6114f6742af9be8eb Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 26 Aug 2021 21:14:56 +0100 Subject: [PATCH 49/64] chore: adjusts tests --- src/Expectation.php | 13 ++++++------- src/Repositories/TestRepository.php | 2 +- tests/.snapshots/success.txt | 15 ++++++++++++++- tests/Features/Expect/toThrow.php | 4 ++-- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 5d32c195..9491fa6b 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -6,7 +6,7 @@ namespace Pest; use BadMethodCallException; use Closure; -use LogicException; +use InvalidArgumentException; use Pest\Concerns\Extendable; use Pest\Concerns\RetrievesValues; use Pest\Support\Arr; @@ -758,8 +758,7 @@ final class Expectation /** * Asserts that executing value throws an exception. * - * @param string|Closure $exception string: the exception class - * Closure: first parameter = exception class + * @param (Closure(Throwable): mixed)|string $exception */ public function toThrow($exception, string $exceptionMessage = null): Expectation { @@ -770,11 +769,11 @@ final class Expectation $parameters = (new ReflectionFunction($exception))->getParameters(); if (1 !== count($parameters)) { - throw new LogicException('The "toThrow" closure must have a single parameter type-hinted as the class string'); + throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); } if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { - throw new LogicException('The "toThrow" closure\'s parameter must be type-hinted as the class string'); + throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.'); } $exception = $type->getName(); @@ -782,14 +781,14 @@ final class Expectation try { ($this->value)(); - } catch (Throwable $e) { + } catch (Throwable $e) { // @phpstan-ignore-line if (!class_exists($exception)) { Assert::assertStringContainsString($exception, $e->getMessage()); return $this; } - if ($exceptionMessage) { + if ($exceptionMessage !== null) { Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); } diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index c3fe2877..e522677a 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -22,7 +22,7 @@ use PHPUnit\Framework\TestCase; final class TestRepository { /** - * @var string + * @var non-empty-string */ private const SEPARATOR = '>>>'; diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 30b28483..3ff086f1 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -449,6 +449,19 @@ ✓ failures ✓ not failures + PASS Tests\Features\Expect\toThrow + ✓ passes + ✓ failures 1 + ✓ failures 2 + ✓ failures 3 + ✓ failures 4 + ✓ failures 5 + ✓ failures 6 + ✓ failures 7 + ✓ not failures + ✓ closure missing parameter + ✓ closure missing type-hint + PASS Tests\Features\Helpers ✓ it can set/get properties on $this ✓ it throws error if property do not exist @@ -649,5 +662,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 421 passed + Tests: 4 incompleted, 9 skipped, 432 passed \ No newline at end of file diff --git a/tests/Features/Expect/toThrow.php b/tests/Features/Expect/toThrow.php index 290003eb..434ced44 100644 --- a/tests/Features/Expect/toThrow.php +++ b/tests/Features/Expect/toThrow.php @@ -53,8 +53,8 @@ test('not failures', function () { test('closure missing parameter', function () { expect(function () {})->toThrow(function () {}); -})->throws(LogicException::class, 'The "toThrow" closure must have a single parameter type-hinted as the class string'); +})->throws(InvalidArgumentException::class, 'The given closure must have a single parameter type-hinted as the class string.'); test('closure missing type-hint', function () { expect(function () {})->toThrow(function ($e) {}); -})->throws(LogicException::class, 'The "toThrow" closure\'s parameter must be type-hinted as the class string'); +})->throws(InvalidArgumentException::class, 'The given closure\'s parameter must be type-hinted as the class string.'); From 075c31bc78a6ae864de0291c07692b628fbcda56 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 26 Aug 2021 21:17:03 +0100 Subject: [PATCH 50/64] release: v1.17.0 --- CHANGELOG.md | 4 ++++ src/Pest.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f576bce5..f0804081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [v1.17.0 (2021-08-26)](https://github.com/pestphp/pest/compare/v1.16.0...v1.17.0) +### Added +- `toThrow` expectation ([#361](https://github.com/pestphp/pest/pull/361)) + ## [v1.16.0 (2021-08-19)](https://github.com/pestphp/pest/compare/v1.15.0...v1.16.0) ### Added - Support for new parallel options ([#369](https://github.com/pestphp/pest/pull/369)) diff --git a/src/Pest.php b/src/Pest.php index 9c921b4d..ff36b07e 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '1.16.0'; + return '1.17.0'; } function testDirectory(string $file = ''): string From 4ae482c7073fb77782b8a4b5738ef1fcea0f82ab Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 27 Aug 2021 22:32:18 +0100 Subject: [PATCH 51/64] feat: adds support for `nunomaduro/collision:^6.0` --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 99ee6a8c..d9ac0d22 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ ], "require": { "php": "^7.3 || ^8.0", - "nunomaduro/collision": "^5.4.0", + "nunomaduro/collision": "^5.4.0|^6.0", "pestphp/pest-plugin": "^1.0.0", "phpunit/phpunit": "^9.5.5" }, From e3d678dc0447a11cf69ae2293572e2f7e6c65960 Mon Sep 17 00:00:00 2001 From: Daniel Ang Date: Sat, 28 Aug 2021 16:30:17 +0200 Subject: [PATCH 52/64] Update snapshots Adds update:snapshots to CONTRIBUTING --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36821899..4037f219 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,10 @@ composer lint ``` ## Tests +Update the snapshots: +```bash +composer update:snapshots +``` Run all tests: ```bash composer test From 8367af22e7ff24b29b565f4342fbfd181a98c05d Mon Sep 17 00:00:00 2001 From: Daniel Ang Date: Sat, 28 Aug 2021 17:02:09 +0200 Subject: [PATCH 53/64] Adds toHaveLength() Expectation & Tests Adds toHaveLength() Expectation and its tests. toHaveLength checks if the given value has the informed length. Works with strings, array, object and collections. --- src/Expectation.php | 30 ++++++++++++++++++++++++++ tests/.snapshots/success.txt | 17 ++++++++++++++- tests/Features/Expect/toHaveLength.php | 27 +++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/Features/Expect/toHaveLength.php diff --git a/src/Expectation.php b/src/Expectation.php index 9491fa6b..5741823f 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -327,6 +327,36 @@ final class Expectation return $this; } + /** + * Asserts that $number matches value's Length. + */ + public function toHaveLength(int $number): Expectation + { + if (is_string($this->value)) { + Assert::assertEquals($number, grapheme_strlen($this->value)); + + return $this; + } + + if (is_iterable($this->value)) { + return $this->toHaveCount($number); + } + + if (is_object($this->value)) { + if (method_exists($this->value, 'toArray')) { + $array = $this->value->toArray(); + } else { + $array = (array) $this->value; + } + + Assert::assertCount($number, $array); + + return $this; + } + + throw new BadMethodCallException('Expectation value length is not countable.'); + } + /** * Asserts that $count matches the number of elements of the value. */ diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 3ff086f1..b819b306 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -419,6 +419,21 @@ ✓ failures ✓ not failures + PASS Tests\Features\Expect\toHaveLength + ✓ it passes with ('Fortaleza') + ✓ it passes with ('Sollefteå') + ✓ it passes with ('Ιεράπετρα') + ✓ it passes with ('PT-BR 🇵🇹🇧🇷😎') + ✓ it passes with (stdClass Object (...)) + ✓ it passes with (Illuminate\Support\Collection Object (...)) + ✓ it passes with array + ✓ it passes with *not* + ✓ it properly fails with *not* + ✓ it fails with (1) + ✓ it fails with (1.5) + ✓ it fails with (true) + ✓ it fails with (null) + PASS Tests\Features\Expect\toHaveProperty ✓ pass ✓ failures @@ -662,5 +677,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 432 passed + Tests: 4 incompleted, 9 skipped, 445 passed \ No newline at end of file diff --git a/tests/Features/Expect/toHaveLength.php b/tests/Features/Expect/toHaveLength.php new file mode 100644 index 00000000..95ad382b --- /dev/null +++ b/tests/Features/Expect/toHaveLength.php @@ -0,0 +1,27 @@ +toHaveLength(9); +})->with([ + 'Fortaleza', 'Sollefteå', 'Ιεράπετρα', 'PT-BR 🇵🇹🇧🇷😎', + (object) [1, 2, 3, 4, 5, 6, 7, 8, 9], + collect([1, 2, 3, 4, 5, 6, 7, 8, 9]), +]); + +it('passes with array', function () { + expect([1, 2, 3])->toHaveLength(3); +}); + +it('passes with *not*', function () { + expect('')->not->toHaveLength(1); +}); + +it('properly fails with *not*', function () { + expect('pest')->not->toHaveLength(4); +})->throws(ExpectationFailedException::class); + +it('fails', function ($value) { + expect($value)->toHaveLength(1); +})->with([1, 1.5, true, null])->throws(BadMethodCallException::class); From 2289adade227bba65934d9830e4be515b3917ac5 Mon Sep 17 00:00:00 2001 From: Daniel Ang Date: Sat, 28 Aug 2021 17:54:29 +0200 Subject: [PATCH 54/64] Use toHaveCount changes to toHaveCount --- src/Expectation.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 5741823f..8f072d57 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -344,14 +344,12 @@ final class Expectation if (is_object($this->value)) { if (method_exists($this->value, 'toArray')) { - $array = $this->value->toArray(); + $this->value = $this->value->toArray(); } else { - $array = (array) $this->value; + $this->value = (array) $this->value; } - Assert::assertCount($number, $array); - - return $this; + return $this->toHaveCount($number); } throw new BadMethodCallException('Expectation value length is not countable.'); From 4f386894bda633bb5e297f6db46ae8f4950fb18b Mon Sep 17 00:00:00 2001 From: Daniel Ang Date: Sat, 28 Aug 2021 18:14:55 +0200 Subject: [PATCH 55/64] Revert "Use toHaveCount" This reverts commit 2289adade227bba65934d9830e4be515b3917ac5. --- src/Expectation.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 8f072d57..5741823f 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -344,12 +344,14 @@ final class Expectation if (is_object($this->value)) { if (method_exists($this->value, 'toArray')) { - $this->value = $this->value->toArray(); + $array = $this->value->toArray(); } else { - $this->value = (array) $this->value; + $array = (array) $this->value; } - return $this->toHaveCount($number); + Assert::assertCount($number, $array); + + return $this; } throw new BadMethodCallException('Expectation value length is not countable.'); From 042f2ec3f3a7fe240f4f9f90050dde3d0cd55559 Mon Sep 17 00:00:00 2001 From: Daniel Ang Date: Sun, 29 Aug 2021 12:55:37 +0200 Subject: [PATCH 56/64] Use mb_strlen instead of grapheme_strlen Due to inconsistent behave, mb_strlen will be used. --- src/Expectation.php | 2 +- tests/.snapshots/success.txt | 3 +-- tests/Features/Expect/toHaveLength.php | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 5741823f..6e5f6030 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -333,7 +333,7 @@ final class Expectation public function toHaveLength(int $number): Expectation { if (is_string($this->value)) { - Assert::assertEquals($number, grapheme_strlen($this->value)); + Assert::assertEquals($number, mb_strlen($this->value)); return $this; } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index b819b306..ae209f43 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -423,7 +423,6 @@ ✓ it passes with ('Fortaleza') ✓ it passes with ('Sollefteå') ✓ it passes with ('Ιεράπετρα') - ✓ it passes with ('PT-BR 🇵🇹🇧🇷😎') ✓ it passes with (stdClass Object (...)) ✓ it passes with (Illuminate\Support\Collection Object (...)) ✓ it passes with array @@ -677,5 +676,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 445 passed + Tests: 4 incompleted, 9 skipped, 444 passed \ No newline at end of file diff --git a/tests/Features/Expect/toHaveLength.php b/tests/Features/Expect/toHaveLength.php index 95ad382b..bd41066b 100644 --- a/tests/Features/Expect/toHaveLength.php +++ b/tests/Features/Expect/toHaveLength.php @@ -5,7 +5,7 @@ use PHPUnit\Framework\ExpectationFailedException; it('passes', function ($value) { expect($value)->toHaveLength(9); })->with([ - 'Fortaleza', 'Sollefteå', 'Ιεράπετρα', 'PT-BR 🇵🇹🇧🇷😎', + 'Fortaleza', 'Sollefteå', 'Ιεράπετρα', (object) [1, 2, 3, 4, 5, 6, 7, 8, 9], collect([1, 2, 3, 4, 5, 6, 7, 8, 9]), ]); From 60c06365233f8b7f6958799529f42cea7f9fbaf8 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 30 Aug 2021 00:05:26 +0100 Subject: [PATCH 57/64] release: v1.18.0 --- CHANGELOG.md | 5 +++++ src/Pest.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0804081..944cb54f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [v1.18.0 (2021-08-30)](https://github.com/pestphp/pest/compare/v1.17.0...v1.18.0) +### Added +- `toHaveLength` expectation ([#386](https://github.com/pestphp/pest/pull/386)) +- `nunomaduro/collision:^6.0` support ([4ae482c](https://github.com/pestphp/pest/commit/4ae482c7073fb77782b8a4b5738ef1fcea0f82ab)) + ## [v1.17.0 (2021-08-26)](https://github.com/pestphp/pest/compare/v1.16.0...v1.17.0) ### Added - `toThrow` expectation ([#361](https://github.com/pestphp/pest/pull/361)) diff --git a/src/Pest.php b/src/Pest.php index ff36b07e..dec7af04 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '1.17.0'; + return '1.18.0'; } function testDirectory(string $file = ''): string From ed389d35d0766f955c030265af9312b1f6ee1b29 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 2 Sep 2021 23:35:40 +0100 Subject: [PATCH 58/64] Adds `Auth0` as sponsor --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 541d9f24..4d3d1770 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,11 @@ We would like to extend our thanks to the following sponsors for funding Pest de ### Premium Sponsors +- **[Auth0](Auth0https://auth0.com) - **[Akaunting](https://akaunting.com)** - **[Codecourse](https://codecourse.com/)** -- **[Meema](https://meema.io/)** +- **[Meema](https://meema.io)** - **[Scout APM](https://scoutapm.com)** -- **[Spatie](https://spatie.be/)** +- **[Spatie](https://spatie.be)** Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. From 3c38facc8a7e2a915774ea7f4ac19663f9ce2f5a Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 2 Sep 2021 23:35:51 +0100 Subject: [PATCH 59/64] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d3d1770..204f19ee 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ We would like to extend our thanks to the following sponsors for funding Pest de ### Premium Sponsors -- **[Auth0](Auth0https://auth0.com) +- **[Auth0](Auth0https://auth0.com)** - **[Akaunting](https://akaunting.com)** - **[Codecourse](https://codecourse.com/)** - **[Meema](https://meema.io)** From 8d99cacc9527f4d0c02fd1506e36dfdc6248b7de Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 2 Sep 2021 23:36:02 +0100 Subject: [PATCH 60/64] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 204f19ee..b5f6cec0 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ We would like to extend our thanks to the following sponsors for funding Pest de ### Premium Sponsors -- **[Auth0](Auth0https://auth0.com)** +- **[Auth0](https://auth0.com)** - **[Akaunting](https://akaunting.com)** - **[Codecourse](https://codecourse.com/)** - **[Meema](https://meema.io)** From 4331b2aaf6d9dd03e07588556a5895425a7fce82 Mon Sep 17 00:00:00 2001 From: Esteban Date: Fri, 3 Sep 2021 04:26:57 -0400 Subject: [PATCH 61/64] Add toHaveProperties --- src/Expectation.php | 14 ++++++++++++ tests/Features/Expect/toHaveProperties.php | 26 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 tests/Features/Expect/toHaveProperties.php diff --git a/src/Expectation.php b/src/Expectation.php index 6e5f6030..02d27c28 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -386,6 +386,20 @@ final class Expectation return $this; } + /** + * Asserts that the value contains the provided properties $names. + * + * @param array $names + */ + public function toHaveProperties(array $names): Expectation + { + foreach ($names as $name) { + $this->toHaveProperty($name); + } + + return $this; + } + /** * Asserts that two variables have the same value. * diff --git a/tests/Features/Expect/toHaveProperties.php b/tests/Features/Expect/toHaveProperties.php new file mode 100644 index 00000000..ad3da1b0 --- /dev/null +++ b/tests/Features/Expect/toHaveProperties.php @@ -0,0 +1,26 @@ +name = 'Jhon'; + $object->age = 21; + + expect($object)->toHaveProperties(['name', 'age']); +}); + +test('failures', function () { + $object = new stdClass(); + $object->name = 'Jhon'; + + expect($object)->toHaveProperties(['name', 'age']); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + $object = new stdClass(); + $object->name = 'Jhon'; + $object->age = 21; + + expect($object)->not->toHaveProperties(['name', 'age']); +})->throws(ExpectationFailedException::class); From 536ce1eca0b80c14e14a757cc47fe2e32915c806 Mon Sep 17 00:00:00 2001 From: Esteban Date: Fri, 3 Sep 2021 04:40:48 -0400 Subject: [PATCH 62/64] Update snapshots --- tests/.snapshots/success.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index ae209f43..4442bbb9 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -433,6 +433,11 @@ ✓ it fails with (true) ✓ it fails with (null) + PASS Tests\Features\Expect\toHaveProperties + ✓ pass + ✓ failures + ✓ not failures + PASS Tests\Features\Expect\toHaveProperty ✓ pass ✓ failures @@ -676,5 +681,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 444 passed + Tests: 4 incompleted, 9 skipped, 447 passed \ No newline at end of file From 253e9d10c8a25a0879665fa581f4819127957600 Mon Sep 17 00:00:00 2001 From: Esteban Date: Fri, 3 Sep 2021 04:44:10 -0400 Subject: [PATCH 63/64] Fix types --- src/Expectation.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 02d27c28..253b1e1e 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -389,9 +389,9 @@ final class Expectation /** * Asserts that the value contains the provided properties $names. * - * @param array $names + * @param iterable $names */ - public function toHaveProperties(array $names): Expectation + public function toHaveProperties(iterable $names): Expectation { foreach ($names as $name) { $this->toHaveProperty($name); From 2f0cd7a4e3a36ac51367ea45d65fc09403ec32c7 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 15 Sep 2021 10:03:05 +0100 Subject: [PATCH 64/64] Adds `Fathom Analytics` as sponsor --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b5f6cec0..a4e616fe 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,10 @@ We would like to extend our thanks to the following sponsors for funding Pest de ### Premium Sponsors -- **[Auth0](https://auth0.com)** - **[Akaunting](https://akaunting.com)** +- **[Auth0](https://auth0.com)** - **[Codecourse](https://codecourse.com/)** +- **[Fathom Analytics](https://usefathom.com/)** - **[Meema](https://meema.io)** - **[Scout APM](https://scoutapm.com)** - **[Spatie](https://spatie.be)**