├── .temp └── .gitkeep ├── resources └── views │ ├── components │ ├── new-line.php │ ├── two-column-detail.php │ └── badge.php │ ├── version.php │ └── usage.php ├── stubs ├── Unit.php ├── init │ ├── Unit │ │ └── ExampleTest.php.stub │ ├── Feature │ │ └── ExampleTest.php.stub │ ├── TestCase.php.stub │ ├── phpunit.xml.stub │ └── Pest.php.stub ├── Dataset.php ├── init-laravel │ ├── Unit │ │ └── ExampleTest.php.stub │ ├── Feature │ │ └── ExampleTest.php.stub │ ├── TestCase.php.stub │ ├── phpunit.xml.stub │ └── Pest.php.stub ├── Feature.php └── Browser.php ├── extension.neon ├── src ├── Matchers │ └── Any.php ├── Factories │ ├── Covers │ │ ├── CoversNothing.php │ │ ├── CoversClass.php │ │ └── CoversFunction.php │ ├── Annotations │ │ ├── Groups.php │ │ ├── CoversNothing.php │ │ ├── Depends.php │ │ └── TestDox.php │ ├── Attributes │ │ ├── Attribute.php │ │ └── Covers.php │ └── Concerns │ │ └── HigherOrderable.php ├── PendingCalls │ ├── Concerns │ │ └── Describable.php │ ├── DescribeCall.php │ ├── AfterEachCall.php │ ├── BeforeEachCall.php │ └── UsesCall.php ├── TestCases │ └── IgnorableTestCase.php ├── Contracts │ ├── Bootstrapper.php │ ├── Plugins │ │ ├── Bootable.php │ │ ├── Terminable.php │ │ ├── AddsOutput.php │ │ ├── HandlesOriginalArguments.php │ │ └── HandlesArguments.php │ ├── TestCaseFilter.php │ ├── TestCaseMethodFilter.php │ ├── HasPrintableTestCaseName.php │ ├── Panicable.php │ └── AddsAnnotations.php ├── Pest.php ├── Exceptions │ ├── FatalException.php │ ├── DatasetArgsCountMismatch.php │ ├── ExpectationNotFound.php │ ├── InvalidExpectationValue.php │ ├── InvalidOption.php │ ├── InvalidPestCommand.php │ ├── InvalidArgumentException.php │ ├── AfterAllAlreadyExist.php │ ├── BeforeAllAlreadyExist.php │ ├── TestDescriptionMissing.php │ ├── FileOrFolderNotFound.php │ ├── MissingDependency.php │ ├── TestCaseClassOrTraitNotFound.php │ ├── InvalidExpectation.php │ ├── DatasetAlreadyExists.php │ ├── AfterAllWithinDescribe.php │ ├── BeforeAllWithinDescribe.php │ ├── DatasetDoesNotExist.php │ ├── TestAlreadyExist.php │ ├── TestCaseAlreadyInUse.php │ ├── TestClosureMustNotBeStatic.php │ ├── ShouldNotHappen.php │ ├── NoDirtyTestsFound.php │ └── DatasetMissing.php ├── Plugins │ ├── Parallel │ │ ├── Contracts │ │ │ └── HandlersWorkerArguments.php │ │ ├── Handlers │ │ │ ├── Pest.php │ │ │ ├── Parallel.php │ │ │ └── Laravel.php │ │ └── Paratest │ │ │ └── CleanConsoleOutput.php │ ├── Actions │ │ ├── CallsBoot.php │ │ ├── CallsTerminable.php │ │ ├── CallsAddsOutput.php │ │ ├── CallsHandleOriginalArguments.php │ │ └── CallsHandleArguments.php │ ├── ProcessIsolation.php │ ├── Printer.php │ ├── Version.php │ ├── Profile.php │ ├── Bail.php │ ├── Snapshot.php │ ├── Verbose.php │ ├── Retry.php │ ├── Environment.php │ ├── Memory.php │ ├── Concerns │ │ └── HandleArguments.php │ ├── Only.php │ ├── Cache.php │ ├── Init.php │ └── Coverage.php ├── Support │ ├── NullClosure.php │ ├── Closure.php │ ├── DatasetInfo.php │ ├── ExceptionTrace.php │ ├── View.php │ ├── HigherOrderTapProxy.php │ ├── HigherOrderCallables.php │ ├── ExpectationPipeline.php │ ├── HigherOrderMessageCollection.php │ ├── Arr.php │ ├── Exporter.php │ ├── ChainableClosure.php │ ├── Container.php │ ├── HigherOrderMessage.php │ ├── Str.php │ └── Backtrace.php ├── TestCaseMethodFilters │ └── TodoTestCaseFilter.php ├── Logging │ └── TeamCity │ │ ├── Subscriber │ │ ├── TestFailedSubscriber.php │ │ ├── TestErroredSubscriber.php │ │ ├── TestSkippedSubscriber.php │ │ ├── TestFinishedSubscriber.php │ │ ├── TestPreparedSubscriber.php │ │ ├── TestSuiteStartedSubscriber.php │ │ ├── TestSuiteFinishedSubscriber.php │ │ ├── TestConsideredRiskySubscriber.php │ │ ├── TestMarkedIncompleteSubscriber.php │ │ ├── TestExecutionFinishedSubscriber.php │ │ └── Subscriber.php │ │ └── ServiceMessage.php ├── Concerns │ ├── Expectable.php │ ├── Extendable.php │ ├── Retrievable.php │ ├── Logging │ │ └── WritesToConsole.php │ └── Pipeable.php ├── Subscribers │ ├── EnsureConfigurationIsAvailable.php │ ├── EnsureKernelDumpIsFlushed.php │ ├── EnsureTeamCityEnabled.php │ └── EnsureIgnorableTestCasesAreIgnored.php ├── Bootstrappers │ ├── BootView.php │ ├── BootKernelDump.php │ ├── BootExcludeList.php │ ├── BootSubscribers.php │ ├── BootOverrides.php │ └── BootFiles.php ├── Plugin.php ├── Console │ ├── Help.php │ └── Thanks.php ├── Repositories │ ├── AfterAllRepository.php │ ├── BeforeAllRepository.php │ ├── BeforeEachRepository.php │ ├── AfterEachRepository.php │ └── SnapshotRepository.php ├── Panic.php ├── Expectations │ └── EachExpectation.php ├── Result.php ├── TestCaseFilters │ └── GitDirtyTestCaseFilter.php ├── KernelDump.php └── TestSuite.php ├── LICENSE.md ├── overrides ├── Event │ └── Value │ │ └── ThrowableBuilder.php └── TextUI │ ├── Output │ └── Default │ │ └── ProgressPrinter │ │ └── Subscriber │ │ └── TestSkippedSubscriber.php │ ├── Command │ └── Commands │ │ └── WarmCodeCoverageCacheCommand.php │ └── TestSuiteFilterProcessor.php ├── bin ├── pest └── worker.php └── composer.json /.temp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/components/new-line.php: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /stubs/Unit.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /stubs/init/Unit/ExampleTest.php.stub: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /stubs/init/Feature/ExampleTest.php.stub: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | universalObjectCratesClasses: 3 | - Pest\Support\HigherOrderTapProxy 4 | - Pest\Expectation 5 | -------------------------------------------------------------------------------- /stubs/Dataset.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /stubs/Feature.php: -------------------------------------------------------------------------------- 1 | get('/{name}'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /src/Matchers/Any.php: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /resources/views/version.php: -------------------------------------------------------------------------------- 1 |
2 | Pest Testing Framework. 3 |
4 | -------------------------------------------------------------------------------- /stubs/init/TestCase.php.stub: -------------------------------------------------------------------------------- 1 | 2 | USAGE:pest') ?> [options] 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/PendingCalls/Concerns/Describable.php: -------------------------------------------------------------------------------- 1 | browse(function (Browser $browser) { 7 | $browser->visit('/{name}') 8 | ->assertSee('{name}'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/Factories/Covers/CoversClass.php: -------------------------------------------------------------------------------- 1 | testPath.DIRECTORY_SEPARATOR.$file; 15 | } 16 | -------------------------------------------------------------------------------- /src/Contracts/Plugins/AddsOutput.php: -------------------------------------------------------------------------------- 1 | $arguments 11 | * @return array 12 | */ 13 | public function handleWorkerArguments(array $arguments): array; 14 | } 15 | -------------------------------------------------------------------------------- /src/Support/NullClosure.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/TestCaseMethodFilters/TodoTestCaseFilter.php: -------------------------------------------------------------------------------- 1 | todo; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/DatasetArgsCountMismatch.php: -------------------------------------------------------------------------------- 1 | $arguments 16 | */ 17 | public function handleOriginalArguments(array $arguments): void; 18 | } 19 | -------------------------------------------------------------------------------- /src/Contracts/Plugins/HandlesArguments.php: -------------------------------------------------------------------------------- 1 | $arguments 16 | * @return array 17 | */ 18 | public function handleArguments(array $arguments): array; 19 | } 20 | -------------------------------------------------------------------------------- /src/Contracts/Panicable.php: -------------------------------------------------------------------------------- 1 | logger()->testFailed($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestErroredSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testErrored($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestSkippedSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testSkipped($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestFinishedSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testFinished($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestPreparedSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testPrepared($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Exceptions/ExpectationNotFound.php: -------------------------------------------------------------------------------- 1 | ['blue', 'INFO'], 7 | 'ERROR' => ['red', 'ERROR'], 8 | }; 9 | 10 | ?> 11 | 12 |
13 | 14 | 15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/Concerns/Expectable.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function expect(mixed $value): Expectation 23 | { 24 | return new Expectation($value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Contracts/AddsAnnotations.php: -------------------------------------------------------------------------------- 1 | $annotations 18 | * @return array 19 | */ 20 | public function __invoke(TestCaseMethodFactory $method, array $annotations): array; 21 | } 22 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestSuiteStartedSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testSuiteStarted($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestSuiteFinishedSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testSuiteFinished($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestConsideredRiskySubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testConsideredRisky($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestMarkedIncompleteSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testMarkedIncomplete($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestExecutionFinishedSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testExecutionFinished($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/Subscriber.php: -------------------------------------------------------------------------------- 1 | logger; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Factories/Annotations/Groups.php: -------------------------------------------------------------------------------- 1 | groups as $group) { 21 | $annotations[] = "@group $group"; 22 | } 23 | 24 | return $annotations; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Plugins/Parallel/Handlers/Pest.php: -------------------------------------------------------------------------------- 1 | add(Configuration::class, $event->configuration()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Plugins/Actions/CallsBoot.php: -------------------------------------------------------------------------------- 1 | boot(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Factories/Attributes/Attribute.php: -------------------------------------------------------------------------------- 1 | $attributes 21 | * @return array 22 | */ 23 | public function __invoke(TestCaseMethodFactory $method, array $attributes): array 24 | { 25 | return $attributes; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /stubs/init/phpunit.xml.stub: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./app 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidOption.php: -------------------------------------------------------------------------------- 1 | terminate(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Plugins/ProcessIsolation.php: -------------------------------------------------------------------------------- 1 | hasArgument('--process-isolation', $arguments)) { 23 | throw new InvalidOption('The [--process-isolation] option is not supported.'); 24 | } 25 | 26 | return $arguments; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Subscribers/EnsureKernelDumpIsFlushed.php: -------------------------------------------------------------------------------- 1 | get(KernelDump::class); 23 | 24 | assert($kernelDump instanceof KernelDump); 25 | 26 | $kernelDump->disable(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Bootstrappers/BootView.php: -------------------------------------------------------------------------------- 1 | output); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidPestCommand.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * @internal 15 | */ 16 | public static array $callables = []; 17 | 18 | /** 19 | * Lazy loads an `uses` call on the context of plugins. 20 | * 21 | * @param class-string ...$traits 22 | */ 23 | public static function uses(string ...$traits): void 24 | { 25 | self::$callables[] = function () use ($traits): void { 26 | uses(...$traits)->in(TestSuite::getInstance()->rootPath); 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Factories/Annotations/CoversNothing.php: -------------------------------------------------------------------------------- 1 | covers[0] ?? null) instanceof CoversNothingFactory) { 22 | $annotations[] = '@coversNothing'; 23 | } 24 | 25 | return $annotations; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Plugins/Printer.php: -------------------------------------------------------------------------------- 1 | pushArgument('--no-output', $arguments); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | hasArgument('--version', $arguments)) { 25 | View::render('version', [ 26 | 'version' => version(), 27 | ]); 28 | 29 | exit(0); 30 | } 31 | 32 | return $arguments; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Exceptions/AfterAllAlreadyExist.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private static array $extends = []; 20 | 21 | /** 22 | * Register a new extend. 23 | */ 24 | public function extend(string $name, Closure $extend): void 25 | { 26 | static::$extends[$name] = $extend; 27 | } 28 | 29 | /** 30 | * Checks if given extend name is registered. 31 | */ 32 | public static function hasExtend(string $name): bool 33 | { 34 | return array_key_exists($name, static::$extends); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidExpectation.php: -------------------------------------------------------------------------------- 1 | $methods 19 | * 20 | * @throws self 21 | */ 22 | public static function fromMethods(array $methods): never 23 | { 24 | throw new self(sprintf('Expectation [%s] is not valid.', implode('->', $methods))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Exceptions/DatasetAlreadyExists.php: -------------------------------------------------------------------------------- 1 | depends as $depend) { 22 | $depend = Str::evaluable($method->describing !== null ? Str::describe($method->describing, $depend) : $depend); 23 | 24 | $annotations[] = "@depends $depend"; 25 | } 26 | 27 | return $annotations; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Exceptions/BeforeAllWithinDescribe.php: -------------------------------------------------------------------------------- 1 | addOutput($exitCode); 27 | } 28 | 29 | return $exitCode; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Plugins/Profile.php: -------------------------------------------------------------------------------- 1 | hasArgument('--profile', $arguments)) { 23 | return $arguments; 24 | } 25 | 26 | if ($this->hasArgument('--parallel', $arguments)) { 27 | throw new InvalidOption('The [--profile] option is not supported when running in parallel.'); 28 | } 29 | 30 | return $arguments; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Plugins/Parallel/Paratest/CleanConsoleOutput.php: -------------------------------------------------------------------------------- 1 | isOpeningHeadline($message)) { 17 | return; 18 | } 19 | 20 | parent::doWrite($message, $newline); 21 | } 22 | 23 | /** 24 | * Removes the opening headline, witch is not needed. 25 | */ 26 | private function isOpeningHeadline(string $message): bool 27 | { 28 | return str_contains($message, 'by Sebastian Bergmann and contributors.'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Concerns/Retrievable.php: -------------------------------------------------------------------------------- 1 | |object $value 19 | * @param TRetrievableValue|null $default 20 | * @return TRetrievableValue|null 21 | */ 22 | private function retrieve(string $key, mixed $value, mixed $default = null): mixed 23 | { 24 | if (is_array($value)) { 25 | return $value[$key] ?? $default; 26 | } 27 | 28 | // @phpstan-ignore-next-line 29 | return $value->$key ?? $default; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Exceptions/TestCaseAlreadyInUse.php: -------------------------------------------------------------------------------- 1 | $argv 21 | */ 22 | public static function execute(array $argv): void 23 | { 24 | $plugins = Loader::getPlugins(Plugins\HandlesOriginalArguments::class); 25 | 26 | /** @var Plugins\HandlesOriginalArguments $plugin */ 27 | foreach ($plugins as $plugin) { 28 | $plugin->handleOriginalArguments($argv); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Bootstrappers/BootKernelDump.php: -------------------------------------------------------------------------------- 1 | add(KernelDump::class, $kernelDump = new KernelDump( 32 | $this->output, 33 | )); 34 | 35 | $kernelDump->enable(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Plugins/Actions/CallsHandleArguments.php: -------------------------------------------------------------------------------- 1 | $argv 21 | * @return array 22 | */ 23 | public static function execute(array $argv): array 24 | { 25 | $plugins = Loader::getPlugins(Plugins\HandlesArguments::class); 26 | 27 | /** @var Plugins\HandlesArguments $plugin */ 28 | foreach ($plugins as $plugin) { 29 | $argv = $plugin->handleArguments($argv); 30 | } 31 | 32 | return $argv; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Plugins/Bail.php: -------------------------------------------------------------------------------- 1 | hasArgument('--bail', $arguments)) { 23 | $arguments = $this->popArgument('--bail', $arguments); 24 | 25 | $arguments = $this->pushArgument('--stop-on-failure', $arguments); 26 | $arguments = $this->pushArgument('--stop-on-error', $arguments); 27 | } 28 | 29 | return $arguments; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Support/Closure.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private const EXCLUDE_LIST = [ 21 | 'bin', 22 | 'overrides', 23 | 'resources', 24 | 'src', 25 | 'stubs', 26 | ]; 27 | 28 | /** 29 | * Boots the "exclude list" for PHPUnit to ignore Pest files. 30 | */ 31 | public function boot(): void 32 | { 33 | $baseDirectory = dirname(__DIR__, 2); 34 | 35 | foreach (self::EXCLUDE_LIST as $directory) { 36 | ExcludeList::addDirectory($baseDirectory.DIRECTORY_SEPARATOR.$directory); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Plugins/Snapshot.php: -------------------------------------------------------------------------------- 1 | hasArgument('--update-snapshots', $arguments)) { 24 | return $arguments; 25 | } 26 | 27 | if ($this->hasArgument('--parallel', $arguments)) { 28 | throw new InvalidOption('The [--update-snapshots] option is not supported when running in parallel.'); 29 | } 30 | 31 | TestSuite::getInstance()->snapshots->flush(); 32 | 33 | return $this->popArgument('--update-snapshots', $arguments); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Plugins/Verbose.php: -------------------------------------------------------------------------------- 1 | hasArgument('-'.$level, $arguments)) { 28 | $arguments = $this->popArgument('-'.$level, $arguments); 29 | } 30 | } 31 | 32 | if ($this->hasArgument('--quiet', $arguments)) { 33 | return $this->popArgument('--quiet', $arguments); 34 | } 35 | 36 | return $arguments; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Factories/Concerns/HigherOrderable.php: -------------------------------------------------------------------------------- 1 | chains = new HigherOrderMessageCollection; 32 | $this->factoryProxies = new HigherOrderMessageCollection; 33 | $this->proxies = new HigherOrderMessageCollection; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Plugins/Retry.php: -------------------------------------------------------------------------------- 1 | hasArgument('--retry', $arguments)) { 23 | return $arguments; 24 | } 25 | 26 | if ($this->hasArgument('--parallel', $arguments)) { 27 | throw new InvalidOption('The [--retry] option is not supported when running in parallel.'); 28 | } 29 | 30 | $arguments = $this->popArgument('--retry', $arguments); 31 | 32 | $arguments = $this->pushArgument('--order-by=defects', $arguments); 33 | 34 | return $this->pushArgument('--stop-on-failure', $arguments); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Exceptions/TestClosureMustNotBeStatic.php: -------------------------------------------------------------------------------- 1 | description, 27 | $method->filename 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Factories/Annotations/TestDox.php: -------------------------------------------------------------------------------- 1 | description !== null); 24 | $methodDescription = str_replace('*/', '{@*}', $method->description); 25 | 26 | $annotations[] = "@testdox $methodDescription"; 27 | 28 | return $annotations; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exceptions/ShouldNotHappen.php: -------------------------------------------------------------------------------- 1 | getMessage(); 21 | 22 | parent::__construct(sprintf(<<<'EOF' 23 | This should not happen - please create an new issue here: https://github.com/pestphp/pest. 24 | 25 | Issue: %s 26 | PHP version: %s 27 | Operating system: %s 28 | EOF 29 | , $message, phpversion(), PHP_OS), 1, $exception); 30 | } 31 | 32 | /** 33 | * Creates a new instance of should not happen without a specific exception. 34 | */ 35 | public static function fromMessage(string $message): ShouldNotHappen 36 | { 37 | return new ShouldNotHappen(new Exception($message)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Exceptions/NoDirtyTestsFound.php: -------------------------------------------------------------------------------- 1 | writeln([ 25 | '', 26 | ' INFO No "dirty" tests found.', 27 | '', 28 | ]); 29 | } 30 | 31 | /** 32 | * The exit code to be used. 33 | */ 34 | public function exitCode(): int 35 | { 36 | return 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Plugins/Parallel/Handlers/Parallel.php: -------------------------------------------------------------------------------- 1 | $this->popArgument($arg, $args), $arguments); 34 | 35 | return $this->pushArgument('--runner='.WrapperRunner::class, $args); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Support/DatasetInfo.php: -------------------------------------------------------------------------------- 1 | $arguments 21 | */ 22 | public function __construct(string $file, string $name, array $arguments) 23 | { 24 | parent::__construct(sprintf( 25 | "A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 26 | $name, 27 | count($arguments), 28 | implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)), 29 | $file, 30 | )); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Nuno Maduro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Concerns/Logging/WritesToConsole.php: -------------------------------------------------------------------------------- 1 | writePestTestOutput($message, 'fg-green, bold', '✓'); 18 | } 19 | 20 | /** 21 | * Writes the given error message to the console. 22 | */ 23 | private function writeError(string $message): void 24 | { 25 | $this->writePestTestOutput($message, 'fg-red, bold', '⨯'); 26 | } 27 | 28 | /** 29 | * Writes the given warning message to the console. 30 | */ 31 | private function writeWarning(string $message): void 32 | { 33 | $this->writePestTestOutput($message, 'fg-yellow, bold', '-'); 34 | } 35 | 36 | /** 37 | * Writes the give message to the console. 38 | */ 39 | private function writePestTestOutput(string $message, string $color, string $symbol): void 40 | { 41 | $this->writeWithColor($color, "$symbol ", false); 42 | $this->write($message); 43 | $this->writeNewLine(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /stubs/init-laravel/phpunit.xml.stub: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ./app 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Plugins/Environment.php: -------------------------------------------------------------------------------- 1 | $argument) { 35 | if ($argument === '--ci') { 36 | unset($arguments[$index]); 37 | 38 | self::$name = self::CI; 39 | } 40 | } 41 | 42 | return array_values($arguments); 43 | } 44 | 45 | /** 46 | * Gets the environment name. 47 | */ 48 | public static function name(?string $name = null): string 49 | { 50 | if (is_string($name)) { 51 | self::$name = $name; 52 | } 53 | 54 | return self::$name ?? self::LOCAL; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Console/Help.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private const HELP_MESSAGES = [ 20 | 'Pest Options:', 21 | ' --init Initialise a standard Pest configuration', 22 | ' --coverage Enable coverage and output to standard output', 23 | ' --min= Set the minimum required coverage percentage (), and fail if not met', 24 | ' --group= Only runs tests from the specified group(s)', 25 | ]; 26 | 27 | /** 28 | * Creates a new Console Command instance. 29 | */ 30 | public function __construct(private readonly OutputInterface $output) 31 | { 32 | // .. 33 | } 34 | 35 | /** 36 | * Executes the Console Command. 37 | */ 38 | public function __invoke(): void 39 | { 40 | foreach (self::HELP_MESSAGES as $message) { 41 | $this->output->writeln($message); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Repositories/AfterAllRepository.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private array $state = []; 21 | 22 | /** 23 | * Runs the given closure for each after all. 24 | */ 25 | public function each(callable $each): void 26 | { 27 | foreach ($this->state as $filename => $closure) { 28 | $each($filename, $closure); 29 | } 30 | } 31 | 32 | /** 33 | * Sets a after all closure. 34 | */ 35 | public function set(Closure $closure): void 36 | { 37 | $filename = Reflection::getFileNameFromClosure($closure); 38 | 39 | if (array_key_exists($filename, $this->state)) { 40 | throw new AfterAllAlreadyExist($filename); 41 | } 42 | 43 | $this->state[$filename] = $closure; 44 | } 45 | 46 | /** 47 | * Gets a after all closure by the given filename. 48 | */ 49 | public function get(string $filename): Closure 50 | { 51 | return $this->state[$filename] ?? NullClosure::create(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Bootstrappers/BootSubscribers.php: -------------------------------------------------------------------------------- 1 | > 22 | */ 23 | private const SUBSCRIBERS = [ 24 | Subscribers\EnsureConfigurationIsAvailable::class, 25 | Subscribers\EnsureIgnorableTestCasesAreIgnored::class, 26 | Subscribers\EnsureKernelDumpIsFlushed::class, 27 | Subscribers\EnsureTeamCityEnabled::class, 28 | ]; 29 | 30 | /** 31 | * Creates a new instance of the Boot Subscribers. 32 | */ 33 | public function __construct( 34 | private readonly Container $container, 35 | ) {} 36 | 37 | /** 38 | * Boots the list of Subscribers. 39 | */ 40 | public function boot(): void 41 | { 42 | foreach (self::SUBSCRIBERS as $subscriber) { 43 | $instance = $this->container->get($subscriber); 44 | 45 | assert($instance instanceof Subscriber); 46 | 47 | Event\Facade::instance()->registerSubscriber($instance); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Repositories/BeforeAllRepository.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private array $state = []; 21 | 22 | /** 23 | * Runs one before all closure, and unsets it from the repository. 24 | */ 25 | public function pop(string $filename): Closure 26 | { 27 | $closure = $this->get($filename); 28 | 29 | unset($this->state[$filename]); 30 | 31 | return $closure; 32 | } 33 | 34 | /** 35 | * Sets a before all closure. 36 | */ 37 | public function set(Closure $closure): void 38 | { 39 | $filename = Reflection::getFileNameFromClosure($closure); 40 | 41 | if (array_key_exists($filename, $this->state)) { 42 | throw new BeforeAllAlreadyExist($filename); 43 | } 44 | 45 | $this->state[$filename] = $closure; 46 | } 47 | 48 | /** 49 | * Gets a before all closure by the given filename. 50 | */ 51 | public function get(string $filename): Closure 52 | { 53 | return $this->state[$filename] ?? NullClosure::create(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Plugins/Memory.php: -------------------------------------------------------------------------------- 1 | enabled = $this->hasArgument('--memory', $arguments); 38 | 39 | return $this->popArgument('--memory', $arguments); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function addOutput(int $exitCode): int 46 | { 47 | if ($this->enabled) { 48 | $this->output->writeln(sprintf( 49 | ' Memory: %s MB', 50 | round(memory_get_usage(true) / 1000 ** 2, 3) 51 | )); 52 | } 53 | 54 | return $exitCode; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Subscribers/EnsureTeamCityEnabled.php: -------------------------------------------------------------------------------- 1 | input->hasParameterOption('--teamcity')) { 35 | return; 36 | } 37 | 38 | $flowId = getenv('FLOW_ID'); 39 | $flowId = is_string($flowId) ? (int) $flowId : getmypid(); 40 | 41 | new TeamCityLogger( 42 | $this->output, 43 | new Converter($this->testSuite->rootPath), 44 | $flowId === false ? null : $flowId, 45 | getenv('COLLISION_IGNORE_DURATION') !== false 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Factories/Attributes/Covers.php: -------------------------------------------------------------------------------- 1 | $attributes 25 | * @return array 26 | */ 27 | public function __invoke(TestCaseMethodFactory $method, array $attributes): array 28 | { 29 | foreach ($method->covers as $covering) { 30 | if ($covering instanceof CoversClass) { 31 | // Prepend a backslash for FQN classes 32 | if (str_contains($covering->class, '\\')) { 33 | $covering->class = '\\'.$covering->class; 34 | } 35 | 36 | $attributes[] = "#[\PHPUnit\Framework\Attributes\CoversClass({$covering->class}::class)]"; 37 | } elseif ($covering instanceof CoversFunction) { 38 | $attributes[] = "#[\PHPUnit\Framework\Attributes\CoversFunction('{$covering->function}')]"; 39 | } 40 | } 41 | 42 | return $attributes; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Support/ExceptionTrace.php: -------------------------------------------------------------------------------- 1 | getMessage(), self::UNDEFINED_METHOD)) { 29 | $class = preg_match('/^Call to undefined method ([^:]+)::/', $message, $matches) === false ? null : $matches[1]; 30 | 31 | $message = str_replace(self::UNDEFINED_METHOD, 'Call to undefined method ', $message); 32 | 33 | if (class_exists((string) $class) && (is_countable(class_parents($class)) ? count(class_parents($class)) : 0) > 0 && array_values(class_parents($class))[0] === TestCase::class) { // @phpstan-ignore-line 34 | $message .= '. Did you forget to use the [uses()] function? Read more at: https://pestphp.com/docs/configuring-tests'; 35 | } 36 | 37 | Reflection::setPropertyValue($throwable, 'message', $message); 38 | } 39 | 40 | throw $throwable; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Plugins/Concerns/HandleArguments.php: -------------------------------------------------------------------------------- 1 | $arguments 16 | */ 17 | public function hasArgument(string $argument, array $arguments): bool 18 | { 19 | foreach ($arguments as $arg) { 20 | if ($arg === $argument) { 21 | return true; 22 | } 23 | 24 | if (str_starts_with($arg, "$argument=")) { 25 | return true; 26 | } 27 | } 28 | 29 | return false; 30 | } 31 | 32 | /** 33 | * Adds the given argument and value to the list of arguments. 34 | * 35 | * @param array $arguments 36 | * @return array 37 | */ 38 | public function pushArgument(string $argument, array $arguments): array 39 | { 40 | $arguments[] = $argument; 41 | 42 | return $arguments; 43 | } 44 | 45 | /** 46 | * Pops the given argument from the arguments. 47 | * 48 | * @param array $arguments 49 | * @return array 50 | */ 51 | public function popArgument(string $argument, array $arguments): array 52 | { 53 | $arguments = array_flip($arguments); 54 | 55 | unset($arguments[$argument]); 56 | 57 | return array_values(array_flip($arguments)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Plugins/Only.php: -------------------------------------------------------------------------------- 1 | group('__pest_only'); 48 | 49 | $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; 50 | 51 | if (! file_exists($lockFile)) { 52 | touch($lockFile); 53 | } 54 | } 55 | 56 | /** 57 | * Checks if "only" mode is enabled. 58 | */ 59 | public static function isEnabled(): bool 60 | { 61 | $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; 62 | 63 | return file_exists($lockFile); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php: -------------------------------------------------------------------------------- 1 | getProperty('collector'); 26 | $property->setAccessible(true); 27 | $collector = $property->getValue(); 28 | 29 | assert($collector instanceof Collector); 30 | 31 | $reflection = new ReflectionClass($collector); 32 | $property = $reflection->getProperty('testRunnerTriggeredWarningEvents'); 33 | $property->setAccessible(true); 34 | 35 | /** @var array $testRunnerTriggeredWarningEvents */ 36 | $testRunnerTriggeredWarningEvents = $property->getValue($collector); 37 | 38 | $testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => $event->message() !== 'No tests found in class "Pest\TestCases\IgnorableTestCase".')); 39 | 40 | $property->setValue($collector, $testRunnerTriggeredWarningEvents); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /overrides/Event/Value/ThrowableBuilder.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace PHPUnit\Event\Code; 15 | 16 | use NunoMaduro\Collision\Contracts\RenderableOnCollisionEditor; 17 | use PHPUnit\Event\NoPreviousThrowableException; 18 | use PHPUnit\Framework\Exception; 19 | use PHPUnit\Util\Filter; 20 | use PHPUnit\Util\ThrowableToStringMapper; 21 | 22 | /** 23 | * @internal This class is not covered by the backward compatibility promise for PHPUnit 24 | */ 25 | final class ThrowableBuilder 26 | { 27 | /** 28 | * @throws Exception 29 | * @throws NoPreviousThrowableException 30 | */ 31 | public static function from(\Throwable $t): Throwable 32 | { 33 | $previous = $t->getPrevious(); 34 | 35 | if ($previous !== null) { 36 | $previous = self::from($previous); 37 | } 38 | 39 | $trace = Filter::getFilteredStacktrace($t); 40 | 41 | if ($t instanceof RenderableOnCollisionEditor && $frame = $t->toCollisionEditor()) { 42 | $file = $frame->getFile(); 43 | $line = $frame->getLine(); 44 | 45 | $trace = "$file:$line\n$trace"; 46 | } 47 | 48 | return new Throwable( 49 | $t::class, 50 | $t->getMessage(), 51 | ThrowableToStringMapper::map($t), 52 | $trace, 53 | $previous 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Panic.php: -------------------------------------------------------------------------------- 1 | handle(); 33 | 34 | exit(1); 35 | } 36 | 37 | /** 38 | * Handles the panic. 39 | */ 40 | private function handle(): void 41 | { 42 | try { 43 | $output = Container::getInstance()->get(OutputInterface::class); 44 | } catch (Throwable) { // @phpstan-ignore-line 45 | $output = new ConsoleOutput; 46 | } 47 | 48 | assert($output instanceof OutputInterface); 49 | 50 | if ($this->throwable instanceof Contracts\Panicable) { 51 | $this->throwable->render($output); 52 | 53 | exit($this->throwable->exitCode()); 54 | } 55 | 56 | $writer = new Writer(null, $output); 57 | 58 | $inspector = new Inspector($this->throwable); 59 | 60 | $writer->write($inspector); 61 | $output->writeln(''); 62 | 63 | exit(1); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /stubs/init/Pest.php.stub: -------------------------------------------------------------------------------- 1 | in('Feature'); 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Expectations 19 | |-------------------------------------------------------------------------- 20 | | 21 | | When you're writing tests, you often need to check that values meet certain conditions. The 22 | | "expect()" function gives you access to a set of "expectations" methods that you can use 23 | | to assert different things. Of course, you may extend the Expectation API at any time. 24 | | 25 | */ 26 | 27 | expect()->extend('toBeOne', function () { 28 | return $this->toBe(1); 29 | }); 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Functions 34 | |-------------------------------------------------------------------------- 35 | | 36 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 37 | | project that you don't want to repeat in every file. Here you can also expose helpers as 38 | | global functions to help you to reduce the number of lines of code in your test files. 39 | | 40 | */ 41 | 42 | function something() 43 | { 44 | // .. 45 | } 46 | -------------------------------------------------------------------------------- /src/Repositories/BeforeEachRepository.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private array $state = []; 21 | 22 | /** 23 | * Sets a before each closure. 24 | */ 25 | public function set(string $filename, BeforeEachCall $beforeEachCall, Closure $beforeEachTestCall, Closure $beforeEachTestCase): void 26 | { 27 | if (array_key_exists($filename, $this->state)) { 28 | [$fromBeforeEachTestCall, $fromBeforeEachTestCase] = $this->state[$filename]; 29 | 30 | $beforeEachTestCall = ChainableClosure::unbound($fromBeforeEachTestCall, $beforeEachTestCall); 31 | $beforeEachTestCase = ChainableClosure::bound($fromBeforeEachTestCase, $beforeEachTestCase)->bindTo($beforeEachCall, $beforeEachCall::class); 32 | assert($beforeEachTestCase instanceof Closure); 33 | } 34 | 35 | $this->state[$filename] = [$beforeEachTestCall, $beforeEachTestCase]; 36 | } 37 | 38 | /** 39 | * Gets a before each closure by the given filename. 40 | * 41 | * @return array{0: Closure, 1: Closure} 42 | */ 43 | public function get(string $filename): array 44 | { 45 | $closures = $this->state[$filename] ?? []; 46 | 47 | return [ 48 | $closures[0] ?? NullClosure::create(), 49 | $closures[1] ?? NullClosure::create(), 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Support/View.php: -------------------------------------------------------------------------------- 1 | $data 35 | */ 36 | public static function render(string $path, array $data = []): void 37 | { 38 | $contents = self::compile($path, $data); 39 | 40 | $existing = Termwind::getRenderer(); 41 | 42 | renderUsing(self::$output); 43 | 44 | try { 45 | render($contents); 46 | } finally { 47 | renderUsing($existing); 48 | } 49 | } 50 | 51 | /** 52 | * Compiles the given view. 53 | * 54 | * @param array $data 55 | */ 56 | private static function compile(string $path, array $data): string 57 | { 58 | extract($data); 59 | 60 | ob_start(); 61 | 62 | $path = str_replace('.', '/', $path); 63 | 64 | include sprintf('%s/../../resources/views/%s.php', __DIR__, $path); 65 | 66 | $contents = ob_get_contents(); 67 | 68 | ob_clean(); 69 | 70 | return (string) $contents; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/PendingCalls/DescribeCall.php: -------------------------------------------------------------------------------- 1 | description; 47 | 48 | try { 49 | ($this->tests)(); 50 | } finally { 51 | self::$describing = null; 52 | } 53 | } 54 | 55 | /** 56 | * Dynamically calls methods on each test call. 57 | * 58 | * @param array $arguments 59 | */ 60 | public function __call(string $name, array $arguments): BeforeEachCall 61 | { 62 | $filename = Backtrace::file(); 63 | 64 | $beforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename); 65 | 66 | $beforeEachCall->describing = $this->description; 67 | 68 | return $beforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Repositories/AfterEachRepository.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private array $state = []; 22 | 23 | /** 24 | * Sets a after each closure. 25 | */ 26 | public function set(string $filename, AfterEachCall $afterEachCall, Closure $afterEachTestCase): void 27 | { 28 | if (array_key_exists($filename, $this->state)) { 29 | $fromAfterEachTestCase = $this->state[$filename]; 30 | 31 | $afterEachTestCase = ChainableClosure::bound($fromAfterEachTestCase, $afterEachTestCase) 32 | ->bindTo($afterEachCall, $afterEachCall::class); 33 | } 34 | 35 | assert($afterEachTestCase instanceof Closure); 36 | 37 | $this->state[$filename] = $afterEachTestCase; 38 | } 39 | 40 | /** 41 | * Gets an after each closure by the given filename. 42 | */ 43 | public function get(string $filename): Closure 44 | { 45 | $afterEach = $this->state[$filename] ?? NullClosure::create(); 46 | 47 | return ChainableClosure::bound(function (): void { 48 | if (class_exists(Mockery::class)) { 49 | if ($container = Mockery::getContainer()) { 50 | /* @phpstan-ignore-next-line */ 51 | $this->addToAssertionCount($container->mockery_getExpectationCount()); 52 | } 53 | 54 | Mockery::close(); 55 | } 56 | }, $afterEach); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /stubs/init-laravel/Pest.php.stub: -------------------------------------------------------------------------------- 1 | in('Feature'); 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Expectations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | When you're writing tests, you often need to check that values meet certain conditions. The 25 | | "expect()" function gives you access to a set of "expectations" methods that you can use 26 | | to assert different things. Of course, you may extend the Expectation API at any time. 27 | | 28 | */ 29 | 30 | expect()->extend('toBeOne', function () { 31 | return $this->toBe(1); 32 | }); 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Functions 37 | |-------------------------------------------------------------------------- 38 | | 39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 40 | | project that you don't want to repeat in every file. Here you can also expose helpers as 41 | | global functions to help you to reduce the number of lines of code in your test files. 42 | | 43 | */ 44 | 45 | function something() 46 | { 47 | // .. 48 | } 49 | -------------------------------------------------------------------------------- /src/Support/HigherOrderTapProxy.php: -------------------------------------------------------------------------------- 1 | target->{$property} = $value; // @phpstan-ignore-line 30 | } 31 | 32 | /** 33 | * Dynamically pass properties gets to the target. 34 | * 35 | * @return mixed 36 | */ 37 | public function __get(string $property) 38 | { 39 | if (property_exists($this->target, $property)) { 40 | return $this->target->{$property}; // @phpstan-ignore-line 41 | } 42 | 43 | $className = (new ReflectionClass($this->target))->getName(); 44 | 45 | if (str_starts_with($className, 'P\\')) { 46 | $className = substr($className, 2); 47 | } 48 | 49 | trigger_error(sprintf('Undefined property %s::$%s', $className, $property), E_USER_WARNING); 50 | 51 | return null; 52 | } 53 | 54 | /** 55 | * Dynamically pass method calls to the target. 56 | * 57 | * @param array $arguments 58 | * @return mixed 59 | */ 60 | public function __call(string $methodName, array $arguments) 61 | { 62 | $filename = Backtrace::file(); 63 | $line = Backtrace::line(); 64 | 65 | return (new HigherOrderMessage($filename, $line, $methodName, $arguments)) 66 | ->call($this->target); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Bootstrappers/BootOverrides.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public const FILES = [ 21 | 'c7b9c8a96006dea314204a8f09a8764e51ce0b9b79aadd58da52e8c328db4870' => 'Runner/Filter/NameFilterIterator.php', 22 | 'c7c09ab7c9378710b27f761a4b2948196cbbdf2a73e4389bcdca1e7c94fa9c21' => 'Runner/ResultCache/DefaultResultCache.php', 23 | 'bc8718c89264f65800beabc23e51c6d3bcff87dfc764a12179ef5dbfde272c8b' => 'Runner/TestSuiteLoader.php', 24 | 'f41e48d6cb546772a7de4f8e66b6b7ce894a5318d063eb52e354d206e96c701c' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', 25 | 'cb7519f2d82893640b694492cf7ec9528da80773cc1d259634181b5d393528b5' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', 26 | '2f06e4b1a9f3a24145bfc7ea25df4f124117f940a2cde30a04d04d5678006bff' => 'TextUI/TestSuiteFilterProcessor.php', 27 | 'ef64a657ed9c0067791483784944107827bf227c7e3200f212b6751876b99e25' => 'Event/Value/ThrowableBuilder.php', 28 | 'c78f96e34b98ed01dd8106539d59b8aa8d67f733274118b827c01c5c4111c033' => 'Logging/JUnit/JunitXmlLogger.php', 29 | ]; 30 | 31 | /** 32 | * Boots the list of files to be overridden. 33 | */ 34 | public function boot(): void 35 | { 36 | foreach (self::FILES as $file) { 37 | $file = __DIR__."/../../overrides/$file"; 38 | 39 | if (! file_exists($file)) { 40 | throw ShouldNotHappen::fromMessage(sprintf('File [%s] does not exist.', $file)); 41 | } 42 | 43 | require_once $file; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Support/HigherOrderCallables.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function expect(mixed $value): Expectation 32 | { 33 | /** @var TValue $value */ 34 | $value = $value instanceof Closure ? Reflection::bindCallableWithData($value) : $value; 35 | 36 | return new Expectation($value); 37 | } 38 | 39 | /** 40 | * @template TValue 41 | * 42 | * Create a new expectation. Callable values will be executed prior to returning the new expectation. 43 | * 44 | * @param callable|TValue $value 45 | * @return Expectation<(callable(): mixed)|TValue> 46 | */ 47 | public function and(mixed $value): Expectation 48 | { 49 | return $this->expect($value); 50 | } 51 | 52 | /** 53 | * Execute the given callable after the test has executed the setup method. 54 | * 55 | * @deprecated This method is deprecated. Please use `defer` instead. 56 | */ 57 | public function tap(callable $callable): object 58 | { 59 | return $this->defer($callable); 60 | } 61 | 62 | /** 63 | * Execute the given callable after the test has executed the setup method. 64 | */ 65 | public function defer(callable $callable): object 66 | { 67 | Reflection::bindCallableWithData($callable); 68 | 69 | return $this->target; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Plugins/Cache.php: -------------------------------------------------------------------------------- 1 | hasArgument('--cache-directory', $arguments)) { 38 | 39 | $cliConfiguration = (new CliConfigurationBuilder)->fromParameters([]); 40 | $configurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration); 41 | $xmlConfiguration = DefaultConfiguration::create(); 42 | 43 | if (is_string($configurationFile)) { 44 | $xmlConfiguration = (new Loader)->load($configurationFile); 45 | } 46 | 47 | if (! $xmlConfiguration->phpunit()->hasCacheDirectory()) { 48 | $arguments = $this->pushArgument('--cache-directory', $arguments); 49 | $arguments = $this->pushArgument((string) realpath(self::TEMPORARY_FOLDER), $arguments); 50 | } 51 | } 52 | 53 | if (! $this->hasArgument('--parallel', $arguments)) { 54 | return $this->pushArgument('--cache-result', $arguments); 55 | } 56 | 57 | return $arguments; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Support/ExpectationPipeline.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private array $pipes = []; 20 | 21 | /** 22 | * The list of passables. 23 | * 24 | * @var array 25 | */ 26 | private array $passables; 27 | 28 | /** 29 | * Creates a new instance of Expectation Pipeline. 30 | */ 31 | public function __construct( 32 | private readonly Closure $closure 33 | ) {} 34 | 35 | /** 36 | * Creates a new instance of Expectation Pipeline with given closure. 37 | */ 38 | public static function for(Closure $closure): self 39 | { 40 | return new self($closure); 41 | } 42 | 43 | /** 44 | * Sets the list of passables. 45 | */ 46 | public function send(mixed ...$passables): self 47 | { 48 | $this->passables = $passables; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Sets the list of pipes. 55 | * 56 | * @param array $pipes 57 | */ 58 | public function through(array $pipes): self 59 | { 60 | $this->pipes = $pipes; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Runs the pipeline. 67 | */ 68 | public function run(): void 69 | { 70 | $pipeline = array_reduce( 71 | array_reverse($this->pipes), 72 | $this->carry(), 73 | function (): void { 74 | call_user_func_array($this->closure, $this->passables); 75 | } 76 | ); 77 | 78 | $pipeline(); 79 | } 80 | 81 | /** 82 | * Get a Closure that will carry of the expectation. 83 | */ 84 | public function carry(): Closure 85 | { 86 | return fn (mixed $stack, callable $pipe): Closure => fn () => $pipe($stack, ...$this->passables); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Concerns/Pipeable.php: -------------------------------------------------------------------------------- 1 | > 18 | */ 19 | private static array $pipes = []; 20 | 21 | /** 22 | * The list of interceptors. 23 | * 24 | * @var array> 25 | */ 26 | private static array $interceptors = []; 27 | 28 | /** 29 | * Register a pipe to be applied before an expectation is checked. 30 | */ 31 | public function pipe(string $name, Closure $pipe): void 32 | { 33 | self::$pipes[$name][] = $pipe; 34 | } 35 | 36 | /** 37 | * Register an interceptor that should replace an existing expectation. 38 | * 39 | * @param string|Closure(mixed $value, mixed ...$arguments):bool $filter 40 | */ 41 | public function intercept(string $name, string|Closure $filter, Closure $handler): void 42 | { 43 | if (is_string($filter)) { 44 | $filter = fn ($value): bool => $value instanceof $filter; 45 | } 46 | 47 | self::$interceptors[$name][] = $handler; 48 | 49 | $this->pipe($name, function ($next, ...$arguments) use ($handler, $filter): void { 50 | /* @phpstan-ignore-next-line */ 51 | if ($filter($this->value, ...$arguments)) { 52 | // @phpstan-ignore-next-line 53 | $handler->bindTo($this, $this::class)(...$arguments); 54 | 55 | return; 56 | } 57 | 58 | $next(); 59 | }); 60 | } 61 | 62 | /** 63 | * Get the list of pipes by the given name. 64 | * 65 | * @return array 66 | */ 67 | private function pipes(string $name, object $context, string $scope): array 68 | { 69 | return array_map(fn (Closure $pipe): \Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Expectations/EachExpectation.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class EachExpectation 19 | { 20 | private bool $opposite = false; 21 | 22 | /** 23 | * Creates an expectation on each item of the iterable "value". 24 | * 25 | * @param Expectation $original 26 | */ 27 | public function __construct(private readonly Expectation $original) {} 28 | 29 | /** 30 | * Creates a new expectation. 31 | * 32 | * @template TAndValue 33 | * 34 | * @param TAndValue $value 35 | * @return Expectation 36 | */ 37 | public function and(mixed $value): Expectation 38 | { 39 | return $this->original->and($value); 40 | } 41 | 42 | /** 43 | * Creates the opposite expectation for the value. 44 | * 45 | * @return self 46 | */ 47 | public function not(): self 48 | { 49 | $this->opposite = true; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Dynamically calls methods on the class with the given arguments on each item. 56 | * 57 | * @param array $arguments 58 | * @return self 59 | */ 60 | public function __call(string $name, array $arguments): self 61 | { 62 | foreach ($this->original->value as $item) { 63 | /* @phpstan-ignore-next-line */ 64 | $this->opposite ? expect($item)->not()->$name(...$arguments) : expect($item)->$name(...$arguments); 65 | } 66 | 67 | $this->opposite = false; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Dynamically calls methods on the class without any arguments on each item. 74 | * 75 | * @return self 76 | */ 77 | public function __get(string $name): self 78 | { 79 | /* @phpstan-ignore-next-line */ 80 | return $this->$name(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Support/HigherOrderMessageCollection.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private array $messages = []; 16 | 17 | /** 18 | * Adds a new higher order message to the collection. 19 | * 20 | * @param array|null $arguments 21 | */ 22 | public function add(string $filename, int $line, string $name, ?array $arguments): void 23 | { 24 | $this->messages[] = new HigherOrderMessage($filename, $line, $name, $arguments); 25 | } 26 | 27 | /** 28 | * Adds a new higher order message to the collection if the callable condition is does not return false. 29 | * 30 | * @param array|null $arguments 31 | */ 32 | public function addWhen(callable $condition, string $filename, int $line, string $name, ?array $arguments): void 33 | { 34 | $this->messages[] = (new HigherOrderMessage($filename, $line, $name, $arguments))->when($condition); 35 | } 36 | 37 | /** 38 | * Proxy all the messages starting from the target. 39 | */ 40 | public function chain(object $target): void 41 | { 42 | foreach ($this->messages as $message) { 43 | // @phpstan-ignore-next-line 44 | $target = $message->call($target) ?? $target; 45 | } 46 | } 47 | 48 | /** 49 | * Proxy all the messages to the target. 50 | */ 51 | public function proxy(object $target): void 52 | { 53 | foreach ($this->messages as $message) { 54 | $message->call($target); 55 | } 56 | } 57 | 58 | /** 59 | * Count the number of messages with the given name. 60 | * 61 | * @param string $name A higher order message name (usually a method name) 62 | */ 63 | public function count(string $name): int 64 | { 65 | return array_reduce( 66 | $this->messages, 67 | static fn (int $total, HigherOrderMessage $message): int => $total + (int) ($name === $message->name), 68 | 0, 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Support/Arr.php: -------------------------------------------------------------------------------- 1 | $array 16 | */ 17 | public static function has(array $array, string|int $key): bool 18 | { 19 | $key = (string) $key; 20 | 21 | if (array_key_exists($key, $array)) { 22 | return true; 23 | } 24 | 25 | foreach (explode('.', $key) as $segment) { 26 | if (is_array($array) && array_key_exists($segment, $array)) { 27 | $array = $array[$segment]; 28 | } else { 29 | return false; 30 | } 31 | } 32 | 33 | return true; 34 | } 35 | 36 | /** 37 | * Gets the given key value. 38 | * 39 | * @param array $array 40 | */ 41 | public static function get(array $array, string|int $key, mixed $default = null): mixed 42 | { 43 | $key = (string) $key; 44 | 45 | if (array_key_exists($key, $array)) { 46 | return $array[$key]; 47 | } 48 | 49 | if (! str_contains($key, '.')) { 50 | return $array[$key] ?? $default; 51 | } 52 | 53 | foreach (explode('.', $key) as $segment) { 54 | if (is_array($array) && array_key_exists($segment, $array)) { 55 | $array = $array[$segment]; 56 | } else { 57 | return $default; 58 | } 59 | } 60 | 61 | return $array; 62 | } 63 | 64 | /** 65 | * Flatten a multi-dimensional associative array with dots. 66 | * 67 | * @param array $array 68 | * @return array 69 | */ 70 | public static function dot(array $array, string $prepend = ''): array 71 | { 72 | $results = []; 73 | 74 | foreach ($array as $key => $value) { 75 | if (is_array($value) && $value !== []) { 76 | $results = array_merge($results, self::dot($value, $prepend.$key.'.')); 77 | } else { 78 | $results[$prepend.$value] = $value; 79 | } 80 | } 81 | 82 | return $results; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/PendingCalls/AfterEachCall.php: -------------------------------------------------------------------------------- 1 | closure = $closure instanceof Closure ? $closure : NullClosure::create(); 41 | 42 | $this->proxies = new HigherOrderMessageCollection; 43 | 44 | $this->describing = DescribeCall::describing(); 45 | } 46 | 47 | /** 48 | * Creates the Call. 49 | */ 50 | public function __destruct() 51 | { 52 | $describing = $this->describing; 53 | 54 | $proxies = $this->proxies; 55 | 56 | $afterEachTestCase = ChainableClosure::boundWhen( 57 | fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line 58 | ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line 59 | )->bindTo($this, self::class); 60 | 61 | assert($afterEachTestCase instanceof Closure); 62 | 63 | $this->testSuite->afterEach->set( 64 | $this->filename, 65 | $this, 66 | $afterEachTestCase, 67 | ); 68 | 69 | } 70 | 71 | /** 72 | * Saves the calls to be used on the target. 73 | * 74 | * @param array $arguments 75 | */ 76 | public function __call(string $name, array $arguments): self 77 | { 78 | $this->proxies 79 | ->add(Backtrace::file(), Backtrace::line(), $name, $arguments); 80 | 81 | return $this; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Support/Exporter.php: -------------------------------------------------------------------------------- 1 | $data 43 | */ 44 | public function shortenedRecursiveExport(array &$data, ?Context $context = null): string 45 | { 46 | $result = []; 47 | $array = $data; 48 | $itemsCount = 0; 49 | $exporter = self::default(); 50 | $context ??= new Context; 51 | 52 | $context->add($data); 53 | 54 | foreach ($array as $key => $value) { 55 | if (++$itemsCount > self::MAX_ARRAY_ITEMS) { 56 | $result[] = '…'; 57 | 58 | break; 59 | } 60 | 61 | if (! is_array($value)) { 62 | $result[] = $exporter->shortenedExport($value); 63 | 64 | continue; 65 | } 66 | 67 | assert(is_array($data)); 68 | 69 | $result[] = $context->contains($data[$key]) !== false 70 | ? '*RECURSION*' 71 | : sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context)); 72 | } 73 | 74 | return implode(', ', $result); 75 | } 76 | 77 | /** 78 | * Exports a value into a single-line string. 79 | */ 80 | public function shortenedExport(mixed $value): string 81 | { 82 | $map = [ 83 | '#\.{3}#' => '…', 84 | '#\\\n\s*#' => '', 85 | '# Object \(…\)#' => '', 86 | ]; 87 | 88 | return (string) preg_replace(array_keys($map), array_values($map), $this->exporter->shortenedExport($value)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Support/ChainableClosure.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | private const STRUCTURE = [ 28 | 'Expectations', 29 | 'Expectations.php', 30 | 'Helpers', 31 | 'Helpers.php', 32 | 'Pest.php', 33 | ]; 34 | 35 | /** 36 | * Boots the structure of the tests directory. 37 | */ 38 | public function boot(): void 39 | { 40 | $rootPath = TestSuite::getInstance()->rootPath; 41 | $testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory(); 42 | 43 | foreach (self::STRUCTURE as $filename) { 44 | $filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename); 45 | 46 | if (! file_exists($filename)) { 47 | continue; 48 | } 49 | 50 | if (is_dir($filename)) { 51 | $directory = new RecursiveDirectoryIterator($filename); 52 | $iterator = new RecursiveIteratorIterator($directory); 53 | /** @var \DirectoryIterator $file */ 54 | foreach ($iterator as $file) { 55 | $this->load($file->__toString()); 56 | } 57 | } else { 58 | $this->load($filename); 59 | } 60 | } 61 | 62 | $this->bootDatasets($testsPath); 63 | } 64 | 65 | /** 66 | * Loads, if possible, the given file. 67 | */ 68 | private function load(string $filename): void 69 | { 70 | if (! Str::endsWith($filename, '.php')) { 71 | return; 72 | } 73 | if (! file_exists($filename)) { 74 | return; 75 | } 76 | include_once $filename; 77 | } 78 | 79 | private function bootDatasets(string $testsPath): void 80 | { 81 | assert(strlen($testsPath) > 0); 82 | 83 | $files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php'); 84 | 85 | foreach ($files as $file) { 86 | if (DatasetInfo::isADatasetsFile($file) || DatasetInfo::isInsideADatasetsDirectory($file)) { 87 | $this->load($file); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Result.php: -------------------------------------------------------------------------------- 1 | wasSuccessfulIgnoringPhpunitWarnings()) { 44 | if ($configuration->failOnWarning()) { 45 | $warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents() 46 | + count($result->warnings()) 47 | + count($result->phpWarnings()); 48 | 49 | if ($warnings > 0) { 50 | return self::FAILURE_EXIT; 51 | } 52 | } 53 | 54 | if (! $result->hasTestTriggeredPhpunitWarningEvents()) { 55 | return self::SUCCESS_EXIT; 56 | } 57 | } 58 | 59 | if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) { 60 | return self::FAILURE_EXIT; 61 | } 62 | 63 | if ($result->wasSuccessfulIgnoringPhpunitWarnings()) { 64 | if ($configuration->failOnRisky() && $result->hasTestConsideredRiskyEvents()) { 65 | $returnCode = self::FAILURE_EXIT; 66 | } 67 | 68 | if ($configuration->failOnIncomplete() && $result->hasTestMarkedIncompleteEvents()) { 69 | $returnCode = self::FAILURE_EXIT; 70 | } 71 | 72 | if ($configuration->failOnSkipped() && $result->hasTestSkippedEvents()) { 73 | $returnCode = self::FAILURE_EXIT; 74 | } 75 | } 76 | 77 | if ($result->hasTestErroredEvents()) { 78 | return self::EXCEPTION_EXIT; 79 | } 80 | 81 | return self::FAILURE_EXIT; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Console/Thanks.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | private const FUNDING_MESSAGES = [ 26 | 'Star' => 'https://github.com/pestphp/pest', 27 | 'News' => 'https://twitter.com/pestphp', 28 | 'Videos' => 'https://youtube.com/@nunomaduro', 29 | 'Sponsor' => 'https://github.com/sponsors/nunomaduro', 30 | ]; 31 | 32 | /** 33 | * Creates a new Console Command instance. 34 | */ 35 | public function __construct( 36 | private readonly InputInterface $input, 37 | private readonly OutputInterface $output 38 | ) { 39 | // .. 40 | } 41 | 42 | /** 43 | * Executes the Console Command. 44 | */ 45 | public function __invoke(): void 46 | { 47 | $bootstrapper = new BootView($this->output); 48 | $bootstrapper->boot(); 49 | 50 | $wantsToSupport = false; 51 | 52 | if (getenv('PEST_NO_SUPPORT') !== 'true' && $this->input->isInteractive()) { 53 | $wantsToSupport = (new SymfonyQuestionHelper)->ask( 54 | new ArrayInput([]), 55 | $this->output, 56 | new ConfirmationQuestion( 57 | ' Wanna show Pest some love by starring it on GitHub?', 58 | false, 59 | ) 60 | ); 61 | 62 | View::render('components.new-line'); 63 | 64 | foreach (self::FUNDING_MESSAGES as $message => $link) { 65 | View::render('components.two-column-detail', [ 66 | 'left' => $message, 67 | 'right' => $link, 68 | ]); 69 | } 70 | 71 | View::render('components.new-line'); 72 | } 73 | 74 | if ($wantsToSupport === true) { 75 | if (PHP_OS_FAMILY == 'Darwin') { 76 | exec('open https://github.com/pestphp/pest'); 77 | } 78 | if (PHP_OS_FAMILY == 'Windows') { 79 | exec('start https://github.com/pestphp/pest'); 80 | } 81 | if (PHP_OS_FAMILY == 'Linux') { 82 | exec('xdg-open https://github.com/pestphp/pest'); 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/TestCaseFilters/GitDirtyTestCaseFilter.php: -------------------------------------------------------------------------------- 1 | changedFiles === null) { 29 | $this->loadChangedFiles(); 30 | } 31 | 32 | assert(is_array($this->changedFiles)); 33 | 34 | $relativePath = str_replace($this->projectRoot, '', $testCaseFilename); 35 | $relativePath = str_replace(DIRECTORY_SEPARATOR, '/', $relativePath); 36 | 37 | if (str_starts_with($relativePath, '/')) { 38 | $relativePath = substr($relativePath, 1); 39 | } 40 | 41 | return in_array($relativePath, $this->changedFiles, true); 42 | } 43 | 44 | private function loadChangedFiles(): void 45 | { 46 | $process = new Process(['git', 'status', '--short', '--', '*.php']); 47 | $process->run(); 48 | 49 | if (! $process->isSuccessful()) { 50 | throw new MissingDependency('Filter by dirty files', 'git'); 51 | } 52 | 53 | $output = preg_split('/\R+/', $process->getOutput(), flags: PREG_SPLIT_NO_EMPTY); 54 | assert(is_array($output)); 55 | 56 | $dirtyFiles = []; 57 | 58 | foreach ($output as $dirtyFile) { 59 | $dirtyFiles[substr($dirtyFile, 3)] = trim(substr($dirtyFile, 0, 3)); 60 | } 61 | 62 | $dirtyFiles = array_filter($dirtyFiles, fn (string $status): bool => $status !== 'D'); 63 | 64 | $dirtyFiles = array_map( 65 | fn (string $file, string $status): string => in_array($status, ['R', 'RM'], true) 66 | ? explode(' -> ', $file)[1] 67 | : $file, array_keys($dirtyFiles), $dirtyFiles, 68 | ); 69 | 70 | $dirtyFiles = array_filter( 71 | $dirtyFiles, 72 | fn (string $file): bool => str_starts_with('.'.DIRECTORY_SEPARATOR.$file, TestSuite::getInstance()->testPath) 73 | || str_starts_with($file, TestSuite::getInstance()->testPath) 74 | ); 75 | 76 | $dirtyFiles = array_values($dirtyFiles); 77 | 78 | if ($dirtyFiles === []) { 79 | Panic::with(new NoDirtyTestsFound); 80 | } 81 | 82 | $this->changedFiles = $dirtyFiles; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/KernelDump.php: -------------------------------------------------------------------------------- 1 | buffer .= $message; 33 | 34 | return ''; 35 | }); 36 | } 37 | 38 | /** 39 | * Disable the output buffering. 40 | */ 41 | public function disable(): void 42 | { 43 | @ob_clean(); 44 | 45 | if ($this->buffer !== '') { 46 | $this->flush(); 47 | } 48 | } 49 | 50 | /** 51 | * Terminate the output buffering. 52 | */ 53 | public function terminate(): void 54 | { 55 | $this->disable(); 56 | } 57 | 58 | /** 59 | * Flushes the buffer. 60 | */ 61 | private function flush(): void 62 | { 63 | View::renderUsing($this->output); 64 | 65 | if ($this->isOpeningHeadline($this->buffer)) { 66 | $this->buffer = implode(PHP_EOL, array_slice(explode(PHP_EOL, $this->buffer), 2)); 67 | } 68 | 69 | $type = 'INFO'; 70 | 71 | if ($this->isInternalError($this->buffer)) { 72 | $type = 'ERROR'; 73 | $this->buffer = str_replace( 74 | sprintf('An error occurred inside PHPUnit.%s%sMessage: ', PHP_EOL, PHP_EOL), '', $this->buffer, 75 | ); 76 | } 77 | 78 | $this->buffer = trim($this->buffer); 79 | $this->buffer = rtrim($this->buffer, '.').'.'; 80 | 81 | $lines = explode(PHP_EOL, $this->buffer); 82 | 83 | $lines = array_reverse($lines); 84 | $firstLine = array_pop($lines); 85 | $lines = array_reverse($lines); 86 | 87 | View::render('components.badge', [ 88 | 'type' => $type, 89 | 'content' => $firstLine, 90 | ]); 91 | 92 | $this->output->writeln($lines); 93 | 94 | $this->buffer = ''; 95 | } 96 | 97 | /** 98 | * Checks if the given output contains an opening headline. 99 | */ 100 | private function isOpeningHeadline(string $output): bool 101 | { 102 | return str_contains($output, 'by Sebastian Bergmann and contributors.'); 103 | } 104 | 105 | /** 106 | * Checks if the given output contains an opening headline. 107 | */ 108 | private function isInternalError(string $output): bool 109 | { 110 | return str_contains($output, 'An error occurred inside PHPUnit.') 111 | || str_contains($output, 'Fatal error'); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /bin/pest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $value) { 22 | if ($value === '--compact') { 23 | $_SERVER['COLLISION_PRINTER_COMPACT'] = 'true'; 24 | unset($arguments[$key]); 25 | } 26 | 27 | if ($value === '--profile') { 28 | $_SERVER['COLLISION_PRINTER_PROFILE'] = 'true'; 29 | unset($arguments[$key]); 30 | } 31 | 32 | if (str_contains($value, '--test-directory')) { 33 | unset($arguments[$key]); 34 | } 35 | 36 | if ($value === '--dirty') { 37 | $dirty = true; 38 | unset($arguments[$key]); 39 | } 40 | 41 | if (in_array($value, ['--todo', '--todos'], true)) { 42 | $todo = true; 43 | unset($arguments[$key]); 44 | } 45 | 46 | if (str_contains($value, '--teamcity')) { 47 | unset($arguments[$key]); 48 | $arguments[] = '--no-output'; 49 | unset($_SERVER['COLLISION_PRINTER']); 50 | } 51 | } 52 | 53 | // Used when Pest is required using composer. 54 | $vendorPath = dirname(__DIR__, 4).'/vendor/autoload.php'; 55 | 56 | // Used when Pest maintainers are running Pest tests. 57 | $localPath = dirname(__DIR__).'/vendor/autoload.php'; 58 | 59 | if (file_exists($vendorPath)) { 60 | include_once $vendorPath; 61 | $autoloadPath = $vendorPath; 62 | } else { 63 | include_once $localPath; 64 | $autoloadPath = $localPath; 65 | } 66 | 67 | // Get $rootPath based on $autoloadPath 68 | $rootPath = dirname($autoloadPath, 2); 69 | $input = new ArgvInput(); 70 | 71 | $testSuite = TestSuite::getInstance( 72 | $rootPath, 73 | $input->getParameterOption('--test-directory', 'tests'), 74 | ); 75 | 76 | if ($dirty) { 77 | $testSuite->tests->addTestCaseFilter(new GitDirtyTestCaseFilter($rootPath)); 78 | } 79 | 80 | if ($todo) { 81 | $testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter()); 82 | } 83 | 84 | $isDecorated = $input->getParameterOption('--colors', 'always') !== 'never'; 85 | 86 | $output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated); 87 | 88 | try { 89 | $kernel = Kernel::boot($testSuite, $input, $output); 90 | 91 | $result = $kernel->handle($originalArguments, $arguments); 92 | 93 | $kernel->terminate(); 94 | } catch (Throwable|Error $e) { 95 | Panic::with($e); 96 | } 97 | 98 | exit($result); 99 | })(); 100 | -------------------------------------------------------------------------------- /overrides/TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php: -------------------------------------------------------------------------------- 1 | 41 | * 42 | * For the full copyright and license information, please view the LICENSE 43 | * file that was distributed with this source code. 44 | */ 45 | 46 | namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; 47 | 48 | use PHPUnit\Event\Test\Skipped; 49 | use PHPUnit\Event\Test\SkippedSubscriber; 50 | use ReflectionClass; 51 | 52 | /** 53 | * @internal This class is not covered by the backward compatibility promise for PHPUnit 54 | * 55 | * This file is overridden to allow Pest Parallel to show todo items in the progress output. 56 | */ 57 | final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber 58 | { 59 | /** 60 | * Notifies the printer that a test was skipped. 61 | */ 62 | public function notify(Skipped $event): void 63 | { 64 | if (str_contains($event->message(), '__TODO__')) { 65 | $this->printTodoItem(); 66 | } 67 | 68 | $this->printer()->testSkipped(); 69 | } 70 | 71 | /** 72 | * Prints a "T" to the standard PHPUnit output to indicate a todo item. 73 | */ 74 | private function printTodoItem(): void 75 | { 76 | $mirror = new ReflectionClass($this->printer()); 77 | $printerMirror = $mirror->getMethod('printProgress'); 78 | $printerMirror->invoke($this->printer(), 'T'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Support/Container.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private array $instances = []; 22 | 23 | /** 24 | * Gets a new or already existing container. 25 | */ 26 | public static function getInstance(): self 27 | { 28 | if (! self::$instance instanceof \Pest\Support\Container) { 29 | self::$instance = new self; 30 | } 31 | 32 | return self::$instance; 33 | } 34 | 35 | /** 36 | * Gets a dependency from the container. 37 | */ 38 | public function get(string $id): object|string 39 | { 40 | if (! array_key_exists($id, $this->instances)) { 41 | /** @var class-string $id */ 42 | $this->instances[$id] = $this->build($id); 43 | } 44 | 45 | return $this->instances[$id]; 46 | } 47 | 48 | /** 49 | * Adds the given instance to the container. 50 | * 51 | * @return $this 52 | */ 53 | public function add(string $id, object|string $instance): self 54 | { 55 | $this->instances[$id] = $instance; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Tries to build the given instance. 62 | * 63 | * @template TObject of object 64 | * 65 | * @param class-string $id 66 | * @return TObject 67 | */ 68 | private function build(string $id): object 69 | { 70 | $reflectionClass = new ReflectionClass($id); 71 | 72 | if ($reflectionClass->isInstantiable()) { 73 | $constructor = $reflectionClass->getConstructor(); 74 | 75 | if ($constructor instanceof \ReflectionMethod) { 76 | $params = array_map( 77 | function (ReflectionParameter $param) use ($id): object|string { 78 | $candidate = Reflection::getParameterClassName($param); 79 | 80 | if ($candidate === null) { 81 | $type = $param->getType(); 82 | /* @phpstan-ignore-next-line */ 83 | if ($type instanceof \ReflectionType && $type->isBuiltin()) { 84 | $candidate = $param->getName(); 85 | } else { 86 | throw ShouldNotHappen::fromMessage(sprintf('The type of `$%s` in `%s` cannot be determined.', $id, $param->getName())); 87 | } 88 | } 89 | 90 | return $this->get($candidate); 91 | }, 92 | $constructor->getParameters() 93 | ); 94 | 95 | return $reflectionClass->newInstanceArgs($params); 96 | } 97 | 98 | return $reflectionClass->newInstance(); 99 | } 100 | 101 | throw ShouldNotHappen::fromMessage(sprintf('A dependency with the name `%s` cannot be resolved.', $id)); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/PendingCalls/BeforeEachCall.php: -------------------------------------------------------------------------------- 1 | closure = $closure instanceof Closure ? $closure : NullClosure::create(); 46 | 47 | $this->testCallProxies = new HigherOrderMessageCollection; 48 | $this->testCaseProxies = new HigherOrderMessageCollection; 49 | 50 | $this->describing = DescribeCall::describing(); 51 | } 52 | 53 | /** 54 | * Creates the Call. 55 | */ 56 | public function __destruct() 57 | { 58 | $describing = $this->describing; 59 | $testCaseProxies = $this->testCaseProxies; 60 | 61 | $beforeEachTestCall = function (TestCall $testCall) use ($describing): void { 62 | if ($describing !== $this->describing) { 63 | return; 64 | } 65 | if ($describing !== $testCall->describing) { 66 | return; 67 | } 68 | $this->testCallProxies->chain($testCall); 69 | }; 70 | 71 | $beforeEachTestCase = ChainableClosure::boundWhen( 72 | fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line 73 | ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line 74 | )->bindTo($this, self::class); 75 | 76 | assert($beforeEachTestCase instanceof Closure); 77 | 78 | $this->testSuite->beforeEach->set( 79 | $this->filename, 80 | $this, 81 | $beforeEachTestCall, 82 | $beforeEachTestCase, 83 | ); 84 | } 85 | 86 | /** 87 | * Saves the calls to be used on the target. 88 | * 89 | * @param array $arguments 90 | */ 91 | public function __call(string $name, array $arguments): self 92 | { 93 | if (method_exists(TestCall::class, $name)) { 94 | $this->testCallProxies->add(Backtrace::file(), Backtrace::line(), $name, $arguments); 95 | 96 | return $this; 97 | } 98 | 99 | $this->testCaseProxies 100 | ->add(Backtrace::file(), Backtrace::line(), $name, $arguments); 101 | 102 | return $this; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Plugins/Parallel/Handlers/Laravel.php: -------------------------------------------------------------------------------- 1 | ensureRunnerIsResolvable(); 31 | 32 | $arguments = $this->ensureEnvironmentVariables($arguments); 33 | 34 | return $this->ensureRunner($arguments); 35 | }); 36 | } 37 | 38 | /** 39 | * Executes the given closure when running Laravel. 40 | * 41 | * @param array $arguments 42 | * @param CLosure(array): array $closure 43 | * @return array 44 | */ 45 | private static function whenUsingLaravel(array $arguments, Closure $closure): array 46 | { 47 | $isLaravelApplication = InstalledVersions::isInstalled('laravel/framework', false); 48 | $isLaravelPackage = class_exists(\Orchestra\Testbench\TestCase::class); 49 | 50 | if ($isLaravelApplication && ! $isLaravelPackage) { 51 | return $closure($arguments); 52 | } 53 | 54 | return $arguments; 55 | } 56 | 57 | /** 58 | * Ensures the runner is resolvable. 59 | */ 60 | private function ensureRunnerIsResolvable(): void 61 | { 62 | ParallelRunner::resolveRunnerUsing( // @phpstan-ignore-line 63 | fn (Options $options, OutputInterface $output): RunnerInterface => new WrapperRunner($options, $output) 64 | ); 65 | } 66 | 67 | /** 68 | * Ensures the environment variables are set. 69 | * 70 | * @param array $arguments 71 | * @return array 72 | */ 73 | private function ensureEnvironmentVariables(array $arguments): array 74 | { 75 | $_ENV['LARAVEL_PARALLEL_TESTING'] = 1; 76 | 77 | if ($this->hasArgument('--recreate-databases', $arguments)) { 78 | $_ENV['LARAVEL_PARALLEL_TESTING_RECREATE_DATABASES'] = 1; 79 | } 80 | 81 | if ($this->hasArgument('--drop-databases', $arguments)) { 82 | $_ENV['LARAVEL_PARALLEL_TESTING_DROP_DATABASES'] = 1; 83 | } 84 | 85 | $arguments = $this->popArgument('--recreate-databases', $arguments); 86 | 87 | return $this->popArgument('--drop-databases', $arguments); 88 | } 89 | 90 | /** 91 | * Ensure the runner is set. 92 | * 93 | * @param array $arguments 94 | * @return array 95 | */ 96 | private function ensureRunner(array $arguments): array 97 | { 98 | foreach ($arguments as $value) { 99 | if (str_starts_with($value, '--runner')) { 100 | $arguments = $this->popArgument($value, $arguments); 101 | } 102 | } 103 | 104 | return $this->pushArgument('--runner=\Illuminate\Testing\ParallelRunner', $arguments); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Support/HigherOrderMessage.php: -------------------------------------------------------------------------------- 1 | |null $arguments 29 | */ 30 | public function __construct( 31 | public string $filename, 32 | public int $line, 33 | public string $name, 34 | public ?array $arguments 35 | ) { 36 | // .. 37 | } 38 | 39 | /** 40 | * Re-throws the given `$throwable` with the good line and filename. 41 | * 42 | * @template TValue of object 43 | * 44 | * @param TValue $target 45 | */ 46 | public function call(object $target): mixed 47 | { 48 | if (is_callable($this->condition) && call_user_func(Closure::bind($this->condition, $target)) === false) { 49 | return $target; 50 | } 51 | 52 | if ($this->hasHigherOrderCallable()) { 53 | /* @phpstan-ignore-next-line */ 54 | return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments); 55 | } 56 | 57 | try { 58 | return is_array($this->arguments) 59 | ? Reflection::call($target, $this->name, $this->arguments) 60 | : $target->{$this->name}; /* @phpstan-ignore-line */ 61 | } catch (Throwable $throwable) { 62 | Reflection::setPropertyValue($throwable, 'file', $this->filename); 63 | Reflection::setPropertyValue($throwable, 'line', $this->line); 64 | 65 | if ($throwable->getMessage() === $this->getUndefinedMethodMessage($target, $this->name)) { 66 | /** @var ReflectionClass $reflection */ 67 | $reflection = new ReflectionClass($target); 68 | /* @phpstan-ignore-next-line */ 69 | $reflection = $reflection->getParentClass() ?: $reflection; 70 | Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->name)); 71 | } 72 | 73 | throw $throwable; 74 | } 75 | } 76 | 77 | /** 78 | * Indicates that this message should only be called when the given condition is true. 79 | * 80 | * @param callable(): bool $condition 81 | */ 82 | public function when(callable $condition): self 83 | { 84 | $this->condition = Closure::fromCallable($condition); 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Determines whether or not there exists a higher order callable with the message name. 91 | */ 92 | private function hasHigherOrderCallable(): bool 93 | { 94 | return in_array($this->name, get_class_methods(HigherOrderCallables::class), true); 95 | } 96 | 97 | private function getUndefinedMethodMessage(object $target, string $methodName): string 98 | { 99 | if (\PHP_MAJOR_VERSION >= 8) { 100 | return sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName)); 101 | } 102 | 103 | return sprintf(self::UNDEFINED_METHOD, $methodName); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Plugins/Init.php: -------------------------------------------------------------------------------- 1 | 'phpunit.xml', 30 | 'Pest.php.stub' => 'tests/Pest.php', 31 | 'TestCase.php.stub' => 'tests/TestCase.php', 32 | 'Unit/ExampleTest.php.stub' => 'tests/Unit/ExampleTest.php', 33 | 'Feature/ExampleTest.php.stub' => 'tests/Feature/ExampleTest.php', 34 | ]; 35 | 36 | /** 37 | * Creates a new Plugin instance. 38 | */ 39 | public function __construct( 40 | private readonly TestSuite $testSuite, 41 | private readonly InputInterface $input, 42 | private readonly OutputInterface $output 43 | ) { 44 | // .. 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function handleArguments(array $arguments): array 51 | { 52 | if (! array_key_exists(1, $arguments)) { 53 | return $arguments; 54 | } 55 | if ($arguments[1] !== self::INIT_OPTION) { 56 | return $arguments; 57 | } 58 | 59 | unset($arguments[1]); 60 | 61 | $this->init(); 62 | 63 | exit(0); 64 | } 65 | 66 | /** 67 | * Initializes the tests directory. 68 | */ 69 | public function init(): void 70 | { 71 | $testsBaseDir = "{$this->testSuite->rootPath}/tests"; 72 | 73 | if (! is_dir($testsBaseDir)) { 74 | mkdir($testsBaseDir); 75 | } 76 | 77 | View::render('components.badge', [ 78 | 'type' => 'INFO', 79 | 'content' => 'Preparing tests directory.', 80 | ]); 81 | 82 | foreach (self::STUBS as $from => $to) { 83 | if ($this->isLaravelInstalled()) { 84 | $fromPath = __DIR__."/../../stubs/init-laravel/{$from}"; 85 | } else { 86 | $fromPath = __DIR__."/../../stubs/init/{$from}"; 87 | } 88 | 89 | $toPath = "{$this->testSuite->rootPath}/{$to}"; 90 | 91 | if (file_exists($toPath)) { 92 | View::render('components.two-column-detail', [ 93 | 'left' => $to, 94 | 'right' => 'File already exists.', 95 | ]); 96 | 97 | continue; 98 | } 99 | 100 | if (! is_dir(dirname($toPath))) { 101 | mkdir(dirname($toPath)); 102 | } 103 | 104 | copy($fromPath, $toPath); 105 | 106 | View::render('components.two-column-detail', [ 107 | 'left' => $to, 108 | 'right' => 'File created.', 109 | ]); 110 | } 111 | 112 | View::render('components.new-line'); 113 | 114 | (new Thanks($this->input, $this->output))(); 115 | } 116 | 117 | /** 118 | * Checks if laravel is installed through Composer 119 | */ 120 | private function isLaravelInstalled(): bool 121 | { 122 | return InstalledVersions::isInstalled('laravel/laravel'); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Support/Str.php: -------------------------------------------------------------------------------- 1 | 0; 102 | } 103 | 104 | /** 105 | * Creates a describe block as `$describeDescription` → `$testDescription` format. 106 | */ 107 | public static function describe(string $describeDescription, string $testDescription): string 108 | { 109 | return sprintf('`%s` → %s', $describeDescription, $testDescription); 110 | } 111 | 112 | /** 113 | * Determine if a given value is a valid URL. 114 | */ 115 | public static function isUrl(string $value): bool 116 | { 117 | return (bool) filter_var($value, FILTER_VALIDATE_URL); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /bin/worker.php: -------------------------------------------------------------------------------- 1 | getParameterOption( 19 | '--test-directory', 20 | 'tests' 21 | )); 22 | 23 | $input = new ArgvInput; 24 | 25 | $output = new ConsoleOutput(OutputInterface::VERBOSITY_NORMAL, true); 26 | 27 | Kernel::boot($testSuite, $input, $output); 28 | }); 29 | 30 | (static function () use ($bootPest): void { 31 | $getopt = getopt('', [ 32 | 'status-file:', 33 | 'progress-file:', 34 | 'unexpected-output-file:', 35 | 'testresult-file:', 36 | 'teamcity-file:', 37 | 'testdox-file:', 38 | 'testdox-color', 39 | 'phpunit-argv:', 40 | ]); 41 | 42 | $composerAutoloadFiles = [ 43 | dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'autoload.php', 44 | dirname(__DIR__, 2).DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php', 45 | dirname(__DIR__).DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php', 46 | ]; 47 | 48 | foreach ($composerAutoloadFiles as $file) { 49 | 50 | if (file_exists($file)) { 51 | require_once $file; 52 | define('PHPUNIT_COMPOSER_INSTALL', $file); 53 | 54 | break; 55 | } 56 | } 57 | 58 | assert(isset($getopt['status-file']) && is_string($getopt['status-file'])); 59 | $statusFile = fopen($getopt['status-file'], 'wb'); 60 | assert(is_resource($statusFile)); 61 | 62 | assert(isset($getopt['progress-file']) && is_string($getopt['progress-file'])); 63 | assert(isset($getopt['unexpected-output-file']) && is_string($getopt['unexpected-output-file'])); 64 | assert(isset($getopt['testresult-file']) && is_string($getopt['testresult-file'])); 65 | assert(! isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file'])); 66 | assert(! isset($getopt['testdox-file']) || is_string($getopt['testdox-file'])); 67 | 68 | assert(isset($getopt['phpunit-argv']) && is_string($getopt['phpunit-argv'])); 69 | $phpunitArgv = unserialize($getopt['phpunit-argv'], ['allowed_classes' => false]); 70 | assert(is_array($phpunitArgv)); 71 | 72 | $bootPest(); 73 | 74 | $phpunitArgv = CallsHandleArguments::execute($phpunitArgv); 75 | 76 | $application = new ApplicationForWrapperWorker( 77 | $phpunitArgv, 78 | $getopt['progress-file'], 79 | $getopt['unexpected-output-file'], 80 | $getopt['testresult-file'], 81 | $getopt['teamcity-file'] ?? null, 82 | $getopt['testdox-file'] ?? null, 83 | isset($getopt['testdox-color']), 84 | $getopt['testdox-columns'] ?? null, 85 | ); 86 | 87 | while (true) { 88 | if (feof(STDIN)) { 89 | $application->end(); 90 | exit; 91 | } 92 | 93 | $testPath = fgets(STDIN); 94 | if ($testPath === false || $testPath === WrapperWorker::COMMAND_EXIT) { 95 | $application->end(); 96 | exit; 97 | } 98 | 99 | // It must be a 1 byte string to ensure filesize() is equal to the number of tests executed 100 | $exitCode = $application->runTest(realpath(trim($testPath))); 101 | 102 | fwrite($statusFile, (string) $exitCode); 103 | fflush($statusFile); 104 | } 105 | })(); 106 | -------------------------------------------------------------------------------- /src/Support/Backtrace.php: -------------------------------------------------------------------------------- 1 | beforeAll = new BeforeAllRepository; 75 | $this->beforeEach = new BeforeEachRepository; 76 | $this->tests = new TestRepository; 77 | $this->afterEach = new AfterEachRepository; 78 | $this->afterAll = new AfterAllRepository; 79 | $this->rootPath = (string) realpath($rootPath); 80 | $this->snapshots = new SnapshotRepository( 81 | implode(DIRECTORY_SEPARATOR, [$this->rootPath, $this->testPath]), 82 | implode(DIRECTORY_SEPARATOR, ['.pest', 'snapshots']), 83 | ); 84 | } 85 | 86 | /** 87 | * Returns the current instance of the test suite. 88 | */ 89 | public static function getInstance( 90 | ?string $rootPath = null, 91 | ?string $testPath = null, 92 | ): TestSuite { 93 | if (is_string($rootPath) && is_string($testPath)) { 94 | self::$instance = new TestSuite($rootPath, $testPath); 95 | 96 | foreach (Plugin::$callables as $callable) { 97 | $callable(); 98 | } 99 | 100 | return self::$instance; 101 | } 102 | 103 | if (! self::$instance instanceof self) { 104 | Panic::with(new InvalidPestCommand); 105 | } 106 | 107 | return self::$instance; 108 | } 109 | 110 | public function getFilename(): string 111 | { 112 | assert($this->test instanceof TestCase); 113 | 114 | return (fn () => self::$__filename)->call($this->test, $this->test::class); // @phpstan-ignore-line 115 | } 116 | 117 | public function getDescription(): string 118 | { 119 | assert($this->test instanceof TestCase); 120 | 121 | $description = str_replace('__pest_evaluable_', '', $this->test->name()); 122 | $datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($this->test->dataSetAsStringWithData())); 123 | 124 | return str_replace(' ', '_', $description.$datasetAsString); 125 | } 126 | 127 | public function registerSnapshotChange(string $message): void 128 | { 129 | assert($this->test instanceof TestCase); 130 | 131 | (fn (): string => $this->__snapshotChanges[] = $message)->call($this->test, $this->test::class); // @phpstan-ignore-line 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pestphp/pest", 3 | "description": "The elegant PHP Testing Framework.", 4 | "keywords": [ 5 | "php", 6 | "framework", 7 | "pest", 8 | "unit", 9 | "test", 10 | "testing" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Nuno Maduro", 16 | "email": "enunomaduro@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1.0", 21 | "brianium/paratest": "^7.3.1", 22 | "nunomaduro/collision": "^7.11.0|^8.4.0", 23 | "nunomaduro/termwind": "^1.16.0|^2.1.0", 24 | "pestphp/pest-plugin": "^2.1.1", 25 | "pestphp/pest-plugin-arch": "^2.7.0", 26 | "phpunit/phpunit": "^10.5.36" 27 | }, 28 | "conflict": { 29 | "filp/whoops": "<2.16.0", 30 | "phpunit/phpunit": ">10.5.36", 31 | "sebastian/exporter": "<5.1.0", 32 | "webmozart/assert": "<1.11.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Pest\\": "src/" 37 | }, 38 | "files": [ 39 | "src/Functions.php", 40 | "src/Pest.php" 41 | ] 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Tests\\Fixtures\\Covers\\": "tests/Fixtures/Covers", 46 | "Tests\\Fixtures\\Inheritance\\": "tests/Fixtures/Inheritance", 47 | "Tests\\Fixtures\\Arch\\": "tests/Fixtures/Arch", 48 | "Tests\\": "tests/PHPUnit/" 49 | }, 50 | "files": [ 51 | "tests/Autoload.php" 52 | ] 53 | }, 54 | "require-dev": { 55 | "pestphp/pest-dev-tools": "^2.17.0", 56 | "pestphp/pest-plugin-type-coverage": "^2.8.7", 57 | "symfony/process": "^6.4.0|^7.1.5" 58 | }, 59 | "minimum-stability": "dev", 60 | "prefer-stable": true, 61 | "config": { 62 | "sort-packages": true, 63 | "preferred-install": "dist", 64 | "allow-plugins": { 65 | "pestphp/pest-plugin": true 66 | } 67 | }, 68 | "bin": [ 69 | "bin/pest" 70 | ], 71 | "scripts": { 72 | "refacto": "rector", 73 | "lint": "pint", 74 | "test:refacto": "rector --dry-run", 75 | "test:lint": "pint --test", 76 | "test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug", 77 | "test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100", 78 | "test:unit": "php bin/pest --colors=always --exclude-group=integration --compact", 79 | "test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml", 80 | "test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3", 81 | "test:integration": "php bin/pest --colors=always --group=integration", 82 | "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots", 83 | "test": [ 84 | "@test:refacto", 85 | "@test:lint", 86 | "@test:type:check", 87 | "@test:type:coverage", 88 | "@test:unit", 89 | "@test:parallel", 90 | "@test:integration" 91 | ] 92 | }, 93 | "extra": { 94 | "pest": { 95 | "plugins": [ 96 | "Pest\\Plugins\\Bail", 97 | "Pest\\Plugins\\Cache", 98 | "Pest\\Plugins\\Coverage", 99 | "Pest\\Plugins\\Init", 100 | "Pest\\Plugins\\Environment", 101 | "Pest\\Plugins\\Help", 102 | "Pest\\Plugins\\Memory", 103 | "Pest\\Plugins\\Only", 104 | "Pest\\Plugins\\Printer", 105 | "Pest\\Plugins\\ProcessIsolation", 106 | "Pest\\Plugins\\Profile", 107 | "Pest\\Plugins\\Retry", 108 | "Pest\\Plugins\\Snapshot", 109 | "Pest\\Plugins\\Verbose", 110 | "Pest\\Plugins\\Version", 111 | "Pest\\Plugins\\Parallel" 112 | ] 113 | }, 114 | "phpstan": { 115 | "includes": [ 116 | "extension.neon" 117 | ] 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/ServiceMessage.php: -------------------------------------------------------------------------------- 1 | $parameters 16 | */ 17 | public function __construct( 18 | private readonly string $type, 19 | private readonly array $parameters, 20 | ) {} 21 | 22 | public function toString(): string 23 | { 24 | $paramsToString = ''; 25 | 26 | foreach ([...$this->parameters, 'flowId' => self::$flowId] as $key => $value) { 27 | $value = $this->escapeServiceMessage((string) $value); 28 | $paramsToString .= " $key='$value'"; 29 | } 30 | 31 | return "##teamcity[$this->type$paramsToString]"; 32 | } 33 | 34 | public static function testSuiteStarted(string $name, ?string $location): self 35 | { 36 | return new self('testSuiteStarted', [ 37 | 'name' => $name, 38 | 'locationHint' => $location === null ? null : "file://$location", 39 | ]); 40 | } 41 | 42 | public static function testSuiteCount(int $count): self 43 | { 44 | return new self('testCount', [ 45 | 'count' => $count, 46 | ]); 47 | } 48 | 49 | public static function testSuiteFinished(string $name): self 50 | { 51 | return new self('testSuiteFinished', [ 52 | 'name' => $name, 53 | ]); 54 | } 55 | 56 | public static function testStarted(string $name, string $location): self 57 | { 58 | return new self('testStarted', [ 59 | 'name' => $name, 60 | 'locationHint' => "pest_qn://$location", 61 | ]); 62 | } 63 | 64 | /** 65 | * @param int $duration in milliseconds 66 | */ 67 | public static function testFinished(string $name, int $duration): self 68 | { 69 | return new self('testFinished', [ 70 | 'name' => $name, 71 | 'duration' => $duration, 72 | ]); 73 | } 74 | 75 | public static function testStdOut(string $name, string $data): self 76 | { 77 | if (! str_ends_with($data, "\n")) { 78 | $data .= "\n"; 79 | } 80 | 81 | return new self('testStdOut', [ 82 | 'name' => $name, 83 | 'out' => $data, 84 | ]); 85 | } 86 | 87 | public static function testFailed(string $name, string $message, string $details): self 88 | { 89 | return new self('testFailed', [ 90 | 'name' => $name, 91 | 'message' => $message, 92 | 'details' => $details, 93 | ]); 94 | } 95 | 96 | public static function testStdErr(string $name, string $data): self 97 | { 98 | if (! str_ends_with($data, "\n")) { 99 | $data .= "\n"; 100 | } 101 | 102 | return new self('testStdErr', [ 103 | 'name' => $name, 104 | 'out' => $data, 105 | ]); 106 | } 107 | 108 | public static function testIgnored(string $name, string $message, ?string $details = null): self 109 | { 110 | return new self('testIgnored', [ 111 | 'name' => $name, 112 | 'message' => $message, 113 | 'details' => $details, 114 | ]); 115 | } 116 | 117 | public static function comparisonFailure(string $name, string $message, string $details, string $actual, string $expected): self 118 | { 119 | return new self('testFailed', [ 120 | 'name' => $name, 121 | 'message' => $message, 122 | 'details' => $details, 123 | 'type' => 'comparisonFailure', 124 | 'actual' => $actual, 125 | 'expected' => $expected, 126 | ]); 127 | } 128 | 129 | private function escapeServiceMessage(string $text): string 130 | { 131 | return str_replace( 132 | ['|', "'", "\n", "\r", ']', '['], 133 | ['||', "|'", '|n', '|r', '|]', '|['], 134 | $text 135 | ); 136 | } 137 | 138 | public static function setFlowId(int $flowId): void 139 | { 140 | self::$flowId = $flowId; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /overrides/TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php: -------------------------------------------------------------------------------- 1 | 41 | * 42 | * For the full copyright and license information, please view the LICENSE 43 | * file that was distributed with this source code. 44 | */ 45 | 46 | namespace PHPUnit\TextUI\Command; 47 | 48 | use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry; 49 | use PHPUnit\TextUI\Configuration\Configuration; 50 | use PHPUnit\TextUI\Configuration\NoCoverageCacheDirectoryException; 51 | use SebastianBergmann\CodeCoverage\StaticAnalysis\CacheWarmer; 52 | use SebastianBergmann\Timer\NoActiveTimerException; 53 | use SebastianBergmann\Timer\Timer; 54 | 55 | /** 56 | * @internal This class is not covered by the backward compatibility promise for PHPUnit 57 | */ 58 | final class WarmCodeCoverageCacheCommand implements Command 59 | { 60 | private readonly Configuration $configuration; 61 | 62 | private readonly CodeCoverageFilterRegistry $codeCoverageFilterRegistry; 63 | 64 | public function __construct(Configuration $configuration, CodeCoverageFilterRegistry $codeCoverageFilterRegistry) 65 | { 66 | $this->configuration = $configuration; 67 | $this->codeCoverageFilterRegistry = $codeCoverageFilterRegistry; 68 | } 69 | 70 | /** 71 | * @throws NoActiveTimerException 72 | * @throws NoCoverageCacheDirectoryException 73 | */ 74 | public function execute(): Result 75 | { 76 | if (! $this->configuration->hasCoverageCacheDirectory()) { 77 | return Result::from( 78 | 'Cache for static analysis has not been configured'.PHP_EOL, 79 | Result::FAILURE 80 | ); 81 | } 82 | 83 | $this->codeCoverageFilterRegistry->init($this->configuration); 84 | 85 | if (! $this->codeCoverageFilterRegistry->configured()) { 86 | return Result::from( 87 | 'Filter for code coverage has not been configured'.PHP_EOL, 88 | Result::FAILURE 89 | ); 90 | } 91 | 92 | $timer = new Timer; 93 | $timer->start(); 94 | 95 | (new CacheWarmer)->warmCache( 96 | $this->configuration->coverageCacheDirectory(), 97 | ! $this->configuration->disableCodeCoverageIgnore(), 98 | $this->configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage(), 99 | $this->codeCoverageFilterRegistry->get() 100 | ); 101 | 102 | return Result::from(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/PendingCalls/UsesCall.php: -------------------------------------------------------------------------------- 1 | `beforeAll` 22 | * - `1` => `beforeEach` 23 | * - `2` => `afterEach` 24 | * - `3` => `afterAll` 25 | * 26 | * @var array 27 | */ 28 | private array $hooks = []; 29 | 30 | /** 31 | * Holds the targets of the uses. 32 | * 33 | * @var array 34 | */ 35 | private array $targets; 36 | 37 | /** 38 | * Holds the groups of the uses. 39 | * 40 | * @var array 41 | */ 42 | private array $groups = []; 43 | 44 | /** 45 | * Creates a new Pending Call. 46 | * 47 | * @param array $classAndTraits 48 | */ 49 | public function __construct( 50 | private readonly string $filename, 51 | private readonly array $classAndTraits 52 | ) { 53 | $this->targets = [$filename]; 54 | } 55 | 56 | public function compact(): self 57 | { 58 | DefaultPrinter::compact(true); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * The directories or file where the 65 | * class or traits should be used. 66 | */ 67 | public function in(string ...$targets): void 68 | { 69 | $targets = array_map(function (string $path): string { 70 | $startChar = DIRECTORY_SEPARATOR; 71 | 72 | if ('\\' === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0) { 73 | $path = (string) preg_replace_callback('~^(?P[a-z]+:\\\)~i', fn (array $match): string => strtolower($match['drive']), $path); 74 | 75 | $startChar = strtolower((string) preg_replace('~^([a-z]+:\\\).*$~i', '$1', __DIR__)); 76 | } 77 | 78 | return str_starts_with($path, $startChar) 79 | ? $path 80 | : implode(DIRECTORY_SEPARATOR, [ 81 | dirname($this->filename), 82 | $path, 83 | ]); 84 | }, $targets); 85 | 86 | $this->targets = array_reduce($targets, function (array $accumulator, string $target): array { 87 | if (($matches = glob($target)) !== false) { 88 | foreach ($matches as $file) { 89 | $accumulator[] = (string) realpath($file); 90 | } 91 | } 92 | 93 | return $accumulator; 94 | }, []); 95 | } 96 | 97 | /** 98 | * Sets the test group(s). 99 | */ 100 | public function group(string ...$groups): self 101 | { 102 | $this->groups = array_values($groups); 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Sets the global beforeAll test hook. 109 | */ 110 | public function beforeAll(Closure $hook): self 111 | { 112 | $this->hooks[0] = $hook; 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * Sets the global beforeEach test hook. 119 | */ 120 | public function beforeEach(Closure $hook): self 121 | { 122 | $this->hooks[1] = $hook; 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * Sets the global afterEach test hook. 129 | */ 130 | public function afterEach(Closure $hook): self 131 | { 132 | $this->hooks[2] = $hook; 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Sets the global afterAll test hook. 139 | */ 140 | public function afterAll(Closure $hook): self 141 | { 142 | $this->hooks[3] = $hook; 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * Creates the Call. 149 | */ 150 | public function __destruct() 151 | { 152 | TestSuite::getInstance()->tests->use( 153 | $this->classAndTraits, 154 | $this->groups, 155 | $this->targets, 156 | $this->hooks, 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Repositories/SnapshotRepository.php: -------------------------------------------------------------------------------- 1 | */ 16 | private static array $expectationsCounter = []; 17 | 18 | /** 19 | * Creates a snapshot repository instance. 20 | */ 21 | public function __construct( 22 | readonly private string $testsPath, 23 | readonly private string $snapshotsPath, 24 | ) {} 25 | 26 | /** 27 | * Checks if the snapshot exists. 28 | */ 29 | public function has(): bool 30 | { 31 | return file_exists($this->getSnapshotFilename()); 32 | } 33 | 34 | /** 35 | * Gets the snapshot. 36 | * 37 | * @return array{0: string, 1: string} 38 | * 39 | * @throws ShouldNotHappen 40 | */ 41 | public function get(): array 42 | { 43 | $contents = file_get_contents($snapshotFilename = $this->getSnapshotFilename()); 44 | 45 | if ($contents === false) { 46 | throw ShouldNotHappen::fromMessage('Snapshot file could not be read.'); 47 | } 48 | 49 | $snapshot = str_replace(dirname($this->testsPath).'/', '', $snapshotFilename); 50 | 51 | return [$snapshot, $contents]; 52 | } 53 | 54 | /** 55 | * Saves the given snapshot for the given test case. 56 | */ 57 | public function save(string $snapshot): string 58 | { 59 | $snapshotFilename = $this->getSnapshotFilename(); 60 | 61 | if (! file_exists(dirname($snapshotFilename))) { 62 | mkdir(dirname($snapshotFilename), 0755, true); 63 | } 64 | 65 | file_put_contents($snapshotFilename, $snapshot); 66 | 67 | return str_replace(dirname($this->testsPath).'/', '', $snapshotFilename); 68 | } 69 | 70 | /** 71 | * Flushes the snapshots. 72 | */ 73 | public function flush(): void 74 | { 75 | $absoluteSnapshotsPath = $this->testsPath.'/'.$this->snapshotsPath; 76 | 77 | $deleteDirectory = function (string $path) use (&$deleteDirectory): void { 78 | if (file_exists($path)) { 79 | $scannedDir = scandir($path); 80 | assert(is_array($scannedDir)); 81 | 82 | $files = array_diff($scannedDir, ['.', '..']); 83 | 84 | foreach ($files as $file) { 85 | if (is_dir($path.'/'.$file)) { 86 | $deleteDirectory($path.'/'.$file); 87 | } else { 88 | unlink($path.'/'.$file); 89 | } 90 | } 91 | 92 | rmdir($path); 93 | } 94 | }; 95 | 96 | if (file_exists($absoluteSnapshotsPath)) { 97 | $deleteDirectory($absoluteSnapshotsPath); 98 | } 99 | } 100 | 101 | /** 102 | * Gets the snapshot's "filename". 103 | */ 104 | private function getSnapshotFilename(): string 105 | { 106 | $relativePath = str_replace($this->testsPath, '', TestSuite::getInstance()->getFilename()); 107 | 108 | // remove extension from filename 109 | $relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.')); 110 | 111 | $description = TestSuite::getInstance()->getDescription(); 112 | 113 | if ($this->getCurrentSnapshotCounter() > 1) { 114 | $description .= '__'.$this->getCurrentSnapshotCounter(); 115 | } 116 | 117 | return sprintf('%s/%s.snap', $this->testsPath.'/'.$this->snapshotsPath.$relativePath, $description); 118 | } 119 | 120 | private function getCurrentSnapshotKey(): string 121 | { 122 | return TestSuite::getInstance()->getFilename().'###'.TestSuite::getInstance()->getDescription(); 123 | } 124 | 125 | private function getCurrentSnapshotCounter(): int 126 | { 127 | return self::$expectationsCounter[$this->getCurrentSnapshotKey()] ?? 0; 128 | } 129 | 130 | public function startNewExpectation(): void 131 | { 132 | $key = $this->getCurrentSnapshotKey(); 133 | 134 | if (! isset(self::$expectationsCounter[$key])) { 135 | self::$expectationsCounter[$key] = 0; 136 | } 137 | 138 | self::$expectationsCounter[$key]++; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /overrides/TextUI/TestSuiteFilterProcessor.php: -------------------------------------------------------------------------------- 1 | 41 | * 42 | * For the full copyright and license information, please view the LICENSE 43 | * file that was distributed with this source code. 44 | */ 45 | 46 | namespace PHPUnit\TextUI; 47 | 48 | use Pest\Plugins\Only; 49 | use PHPUnit\Event; 50 | use PHPUnit\Framework\TestSuite; 51 | use PHPUnit\Runner\Filter\Factory; 52 | use PHPUnit\TextUI\Configuration\Configuration; 53 | use PHPUnit\TextUI\Configuration\FilterNotConfiguredException; 54 | 55 | use function array_map; 56 | 57 | /** 58 | * @internal This class is not covered by the backward compatibility promise for PHPUnit 59 | */ 60 | final class TestSuiteFilterProcessor 61 | { 62 | /** 63 | * @throws Event\RuntimeException 64 | * @throws FilterNotConfiguredException 65 | */ 66 | public function process(Configuration $configuration, TestSuite $suite): void 67 | { 68 | $factory = new Factory; 69 | 70 | if (! $configuration->hasFilter() && 71 | ! $configuration->hasGroups() && 72 | ! $configuration->hasExcludeGroups() && 73 | ! $configuration->hasTestsCovering() && 74 | ! $configuration->hasTestsUsing() && 75 | ! Only::isEnabled() 76 | ) { 77 | return; 78 | } 79 | 80 | if ($configuration->hasExcludeGroups()) { 81 | $factory->addExcludeGroupFilter( 82 | $configuration->excludeGroups() 83 | ); 84 | } 85 | 86 | if (Only::isEnabled()) { 87 | $factory->addIncludeGroupFilter(['__pest_only']); 88 | } elseif ($configuration->hasGroups()) { 89 | $factory->addIncludeGroupFilter( 90 | $configuration->groups() 91 | ); 92 | } 93 | 94 | if ($configuration->hasTestsCovering()) { 95 | $factory->addIncludeGroupFilter( 96 | array_map( 97 | static fn (string $name): string => '__phpunit_covers_'.$name, 98 | $configuration->testsCovering() 99 | ) 100 | ); 101 | } 102 | 103 | if ($configuration->hasTestsUsing()) { 104 | $factory->addIncludeGroupFilter( 105 | array_map( 106 | static fn (string $name): string => '__phpunit_uses_'.$name, 107 | $configuration->testsUsing() 108 | ) 109 | ); 110 | } 111 | 112 | if ($configuration->hasFilter()) { 113 | $factory->addNameFilter( 114 | $configuration->filter() 115 | ); 116 | } 117 | 118 | $suite->injectFilter($factory); 119 | 120 | Event\Facade::emitter()->testSuiteFiltered( 121 | Event\TestSuite\TestSuiteBuilder::from($suite) 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Plugins/Coverage.php: -------------------------------------------------------------------------------- 1 | getOption(self::COVERAGE_OPTION)) { 79 | $this->coverage = true; 80 | $originals[] = '--coverage-php'; 81 | $originals[] = \Pest\Support\Coverage::getPath(); 82 | 83 | if (! \Pest\Support\Coverage::isAvailable()) { 84 | if (\Pest\Support\Coverage::usingXdebug()) { 85 | $this->output->writeln([ 86 | '', 87 | " ERROR Unable to get coverage using Xdebug. Did you set Xdebug's coverage mode?", 88 | '', 89 | ]); 90 | } else { 91 | $this->output->writeln([ 92 | '', 93 | ' ERROR No code coverage driver is available.', 94 | '', 95 | ]); 96 | } 97 | 98 | exit(1); 99 | } 100 | } 101 | 102 | if ($input->getOption(self::MIN_OPTION) !== null) { 103 | /** @var int|float $minOption */ 104 | $minOption = $input->getOption(self::MIN_OPTION); 105 | 106 | $this->coverageMin = (float) $minOption; 107 | } 108 | 109 | return $originals; 110 | } 111 | 112 | /** 113 | * {@inheritdoc} 114 | */ 115 | public function addOutput(int $exitCode): int 116 | { 117 | if ($exitCode === 0 && $this->coverage) { 118 | if (! \Pest\Support\Coverage::isAvailable()) { 119 | $this->output->writeln( 120 | "\n ERROR No code coverage driver is available.", 121 | ); 122 | exit(1); 123 | } 124 | 125 | $coverage = \Pest\Support\Coverage::report($this->output); 126 | 127 | $exitCode = (int) ($coverage < $this->coverageMin); 128 | 129 | if ($exitCode === 1) { 130 | $this->output->writeln(sprintf( 131 | "\n FAIL Code coverage below expected %s %%, currently %s %%.", 132 | number_format($this->coverageMin, 1), 133 | number_format($coverage, 1) 134 | )); 135 | } 136 | 137 | $this->output->writeln(['']); 138 | } 139 | 140 | return $exitCode; 141 | } 142 | } 143 | --------------------------------------------------------------------------------