├── .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 |
--------------------------------------------------------------------------------