├── .temp └── .gitkeep ├── LICENSE.md ├── bin ├── pest └── worker.php ├── composer.json ├── extension.neon ├── overrides ├── Event │ └── Value │ │ └── ThrowableBuilder.php ├── Logging │ └── JUnit │ │ └── JunitXmlLogger.php ├── Runner │ ├── Filter │ │ └── NameFilterIterator.php │ ├── ResultCache │ │ └── DefaultResultCache.php │ └── TestSuiteLoader.php └── TextUI │ ├── Command │ └── Commands │ │ └── WarmCodeCoverageCacheCommand.php │ ├── Output │ └── Default │ │ └── ProgressPrinter │ │ └── Subscriber │ │ └── TestSkippedSubscriber.php │ └── TestSuiteFilterProcessor.php ├── phpstan-baseline.neon ├── resources ├── base-phpunit.xml └── views │ ├── components │ ├── badge.php │ ├── new-line.php │ └── two-column-detail.php │ ├── usage.php │ └── version.php ├── src ├── ArchPresets │ ├── AbstractPreset.php │ ├── Custom.php │ ├── Laravel.php │ ├── Php.php │ ├── Relaxed.php │ ├── Security.php │ └── Strict.php ├── Bootstrappers │ ├── BootExcludeList.php │ ├── BootFiles.php │ ├── BootKernelDump.php │ ├── BootOverrides.php │ ├── BootSubscribers.php │ └── BootView.php ├── Collision │ └── Events.php ├── Concerns │ ├── Expectable.php │ ├── Extendable.php │ ├── Logging │ │ └── WritesToConsole.php │ ├── Pipeable.php │ ├── Retrievable.php │ └── Testable.php ├── Configuration.php ├── Configuration │ ├── Presets.php │ ├── Printer.php │ └── Project.php ├── Console │ ├── Help.php │ └── Thanks.php ├── Contracts │ ├── ArchPreset.php │ ├── Bootstrapper.php │ ├── HasPrintableTestCaseName.php │ ├── Panicable.php │ ├── Plugins │ │ ├── AddsOutput.php │ │ ├── Bootable.php │ │ ├── HandlesArguments.php │ │ ├── HandlesOriginalArguments.php │ │ └── Terminable.php │ ├── TestCaseFilter.php │ └── TestCaseMethodFilter.php ├── Evaluators │ └── Attributes.php ├── Exceptions │ ├── AfterAllAlreadyExist.php │ ├── AfterAllWithinDescribe.php │ ├── AfterBeforeTestFunction.php │ ├── BeforeAllAlreadyExist.php │ ├── BeforeAllWithinDescribe.php │ ├── DatasetAlreadyExists.php │ ├── DatasetArgumentsMismatch.php │ ├── DatasetDoesNotExist.php │ ├── DatasetMissing.php │ ├── ExpectationNotFound.php │ ├── FatalException.php │ ├── FileOrFolderNotFound.php │ ├── InvalidArgumentException.php │ ├── InvalidExpectation.php │ ├── InvalidExpectationValue.php │ ├── InvalidOption.php │ ├── InvalidPestCommand.php │ ├── MissingDependency.php │ ├── NoDirtyTestsFound.php │ ├── ShouldNotHappen.php │ ├── TestAlreadyExist.php │ ├── TestCaseAlreadyInUse.php │ ├── TestCaseClassOrTraitNotFound.php │ ├── TestClosureMustNotBeStatic.php │ └── TestDescriptionMissing.php ├── Expectation.php ├── Expectations │ ├── EachExpectation.php │ ├── HigherOrderExpectation.php │ └── OppositeExpectation.php ├── Factories │ ├── Attribute.php │ ├── Concerns │ │ └── HigherOrderable.php │ ├── Covers │ │ ├── CoversClass.php │ │ ├── CoversFunction.php │ │ └── CoversNothing.php │ ├── TestCaseFactory.php │ └── TestCaseMethodFactory.php ├── Functions.php ├── Kernel.php ├── KernelDump.php ├── Logging │ ├── Converter.php │ └── TeamCity │ │ ├── ServiceMessage.php │ │ ├── Subscriber │ │ ├── Subscriber.php │ │ ├── TestConsideredRiskySubscriber.php │ │ ├── TestErroredSubscriber.php │ │ ├── TestExecutionFinishedSubscriber.php │ │ ├── TestFailedSubscriber.php │ │ ├── TestFinishedSubscriber.php │ │ ├── TestPreparedSubscriber.php │ │ ├── TestSkippedSubscriber.php │ │ ├── TestSuiteFinishedSubscriber.php │ │ └── TestSuiteStartedSubscriber.php │ │ └── TeamCityLogger.php ├── Matchers │ └── Any.php ├── Mixins │ └── Expectation.php ├── Panic.php ├── PendingCalls │ ├── AfterEachCall.php │ ├── BeforeEachCall.php │ ├── Concerns │ │ └── Describable.php │ ├── DescribeCall.php │ ├── TestCall.php │ └── UsesCall.php ├── Pest.php ├── Plugin.php ├── Plugins │ ├── Actions │ │ ├── CallsAddsOutput.php │ │ ├── CallsBoot.php │ │ ├── CallsHandleArguments.php │ │ ├── CallsHandleOriginalArguments.php │ │ └── CallsTerminable.php │ ├── Bail.php │ ├── Cache.php │ ├── Concerns │ │ └── HandleArguments.php │ ├── Configuration.php │ ├── Coverage.php │ ├── Environment.php │ ├── Help.php │ ├── Init.php │ ├── Memory.php │ ├── Only.php │ ├── Parallel.php │ ├── Parallel │ │ ├── Contracts │ │ │ └── HandlersWorkerArguments.php │ │ ├── Handlers │ │ │ ├── Laravel.php │ │ │ ├── Parallel.php │ │ │ └── Pest.php │ │ ├── Paratest │ │ │ ├── CleanConsoleOutput.php │ │ │ ├── ResultPrinter.php │ │ │ └── WrapperRunner.php │ │ └── Support │ │ │ └── CompactPrinter.php │ ├── Printer.php │ ├── ProcessIsolation.php │ ├── Profile.php │ ├── Retry.php │ ├── Snapshot.php │ ├── Verbose.php │ └── Version.php ├── Preset.php ├── Repositories │ ├── AfterAllRepository.php │ ├── AfterEachRepository.php │ ├── BeforeAllRepository.php │ ├── BeforeEachRepository.php │ ├── DatasetsRepository.php │ ├── SnapshotRepository.php │ └── TestRepository.php ├── Result.php ├── Subscribers │ ├── EnsureConfigurationIsAvailable.php │ ├── EnsureIgnorableTestCasesAreIgnored.php │ ├── EnsureKernelDumpIsFlushed.php │ └── EnsureTeamCityEnabled.php ├── Support │ ├── Arr.php │ ├── Backtrace.php │ ├── ChainableClosure.php │ ├── Closure.php │ ├── Container.php │ ├── Coverage.php │ ├── DatasetInfo.php │ ├── ExceptionTrace.php │ ├── ExpectationPipeline.php │ ├── Exporter.php │ ├── HigherOrderCallables.php │ ├── HigherOrderMessage.php │ ├── HigherOrderMessageCollection.php │ ├── HigherOrderTapProxy.php │ ├── NullClosure.php │ ├── Reflection.php │ ├── StateGenerator.php │ ├── Str.php │ └── View.php ├── TestCaseFilters │ └── GitDirtyTestCaseFilter.php ├── TestCaseMethodFilters │ ├── AssigneeTestCaseFilter.php │ ├── IssueTestCaseFilter.php │ ├── NotesTestCaseFilter.php │ ├── PrTestCaseFilter.php │ └── TodoTestCaseFilter.php ├── TestCases │ └── IgnorableTestCase.php └── TestSuite.php └── stubs ├── Browser.php ├── Dataset.php ├── Feature.php ├── Unit.php ├── init-laravel ├── Feature │ └── ExampleTest.php.stub ├── Pest.php.stub ├── TestCase.php.stub ├── Unit │ └── ExampleTest.php.stub └── phpunit.xml.stub └── init ├── Feature └── ExampleTest.php.stub ├── Pest.php.stub ├── TestCase.php.stub ├── Unit └── ExampleTest.php.stub └── phpunit.xml.stub /.temp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pestphp/pest/c6244a8712968dbac88eb998e7ff3b5caa556b0d/.temp/.gitkeep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 'test-result-file:', 36 | 'result-cache-file:', 37 | 'teamcity-file:', 38 | 'testdox-file:', 39 | 'testdox-color', 40 | 'testdox-columns:', 41 | 'testdox-summary', 42 | 'phpunit-argv:', 43 | ]); 44 | 45 | $composerAutoloadFiles = [ 46 | dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'autoload.php', 47 | dirname(__DIR__, 2).DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php', 48 | dirname(__DIR__).DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php', 49 | ]; 50 | 51 | foreach ($composerAutoloadFiles as $file) { 52 | 53 | if (file_exists($file)) { 54 | require_once $file; 55 | define('PHPUNIT_COMPOSER_INSTALL', $file); 56 | 57 | break; 58 | } 59 | } 60 | 61 | assert(isset($getopt['status-file']) && is_string($getopt['status-file'])); 62 | $statusFile = fopen($getopt['status-file'], 'wb'); 63 | assert(is_resource($statusFile)); 64 | 65 | assert(isset($getopt['progress-file']) && is_string($getopt['progress-file'])); 66 | assert(isset($getopt['unexpected-output-file']) && is_string($getopt['unexpected-output-file'])); 67 | assert(isset($getopt['test-result-file']) && is_string($getopt['test-result-file'])); 68 | assert(! isset($getopt['result-cache-file']) || is_string($getopt['result-cache-file'])); 69 | assert(! isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file'])); 70 | assert(! isset($getopt['testdox-file']) || is_string($getopt['testdox-file'])); 71 | 72 | assert(isset($getopt['phpunit-argv']) && is_string($getopt['phpunit-argv'])); 73 | $phpunitArgv = unserialize($getopt['phpunit-argv'], ['allowed_classes' => false]); 74 | assert(is_array($phpunitArgv)); 75 | 76 | $bootPest(); 77 | 78 | $phpunitArgv = CallsHandleArguments::execute($phpunitArgv); 79 | 80 | $application = new ApplicationForWrapperWorker( 81 | $phpunitArgv, 82 | $getopt['progress-file'], 83 | $getopt['unexpected-output-file'], 84 | $getopt['test-result-file'], 85 | $getopt['result-cache-file'] ?? null, 86 | $getopt['teamcity-file'] ?? null, 87 | $getopt['testdox-file'] ?? null, 88 | isset($getopt['testdox-color']), 89 | $getopt['testdox-columns'] ?? null, 90 | ); 91 | 92 | while (true) { 93 | if (feof(STDIN)) { 94 | $application->end(); 95 | exit; 96 | } 97 | 98 | $testPath = fgets(STDIN); 99 | if ($testPath === false || $testPath === WrapperWorker::COMMAND_EXIT) { 100 | $application->end(); 101 | exit; 102 | } 103 | 104 | // It must be a 1 byte string to ensure filesize() is equal to the number of tests executed 105 | $exitCode = $application->runTest(realpath(trim($testPath))); 106 | 107 | fwrite($statusFile, (string) $exitCode); 108 | fflush($statusFile); 109 | } 110 | })(); 111 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | universalObjectCratesClasses: 3 | - Pest\Support\HigherOrderTapProxy 4 | - Pest\Expectation 5 | -------------------------------------------------------------------------------- /overrides/Event/Value/ThrowableBuilder.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\Event\Code; 47 | 48 | use NunoMaduro\Collision\Contracts\RenderableOnCollisionEditor; 49 | use PHPUnit\Event\NoPreviousThrowableException; 50 | use PHPUnit\Framework\Exception; 51 | use PHPUnit\Util\Filter; 52 | use PHPUnit\Util\ThrowableToStringMapper; 53 | 54 | /** 55 | * @internal This class is not covered by the backward compatibility promise for PHPUnit 56 | */ 57 | final readonly class ThrowableBuilder 58 | { 59 | /** 60 | * @throws Exception 61 | * @throws NoPreviousThrowableException 62 | */ 63 | public static function from(\Throwable $t): Throwable 64 | { 65 | $previous = $t->getPrevious(); 66 | 67 | if ($previous !== null) { 68 | $previous = self::from($previous); 69 | } 70 | 71 | $trace = Filter::stackTraceFromThrowableAsString($t); 72 | 73 | if ($t instanceof RenderableOnCollisionEditor && $frame = $t->toCollisionEditor()) { 74 | $file = $frame->getFile(); 75 | $line = $frame->getLine(); 76 | 77 | $trace = "$file:$line\n$trace"; 78 | } 79 | 80 | return new Throwable( 81 | $t::class, 82 | $t->getMessage(), 83 | ThrowableToStringMapper::map($t), 84 | $trace, 85 | $previous 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /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 const PHP_EOL; 49 | 50 | use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry; 51 | use PHPUnit\TextUI\Configuration\Configuration; 52 | use PHPUnit\TextUI\Configuration\NoCoverageCacheDirectoryException; 53 | use SebastianBergmann\CodeCoverage\StaticAnalysis\CacheWarmer; 54 | use SebastianBergmann\Timer\NoActiveTimerException; 55 | use SebastianBergmann\Timer\Timer; 56 | 57 | /** 58 | * @internal This class is not covered by the backward compatibility promise for PHPUnit 59 | */ 60 | final readonly class WarmCodeCoverageCacheCommand implements Command 61 | { 62 | private Configuration $configuration; 63 | 64 | private CodeCoverageFilterRegistry $codeCoverageFilterRegistry; 65 | 66 | public function __construct(Configuration $configuration, CodeCoverageFilterRegistry $codeCoverageFilterRegistry) 67 | { 68 | $this->configuration = $configuration; 69 | $this->codeCoverageFilterRegistry = $codeCoverageFilterRegistry; 70 | } 71 | 72 | /** 73 | * @throws NoActiveTimerException 74 | * @throws NoCoverageCacheDirectoryException 75 | */ 76 | public function execute(): Result 77 | { 78 | if (! $this->configuration->hasCoverageCacheDirectory()) { 79 | return Result::from( 80 | 'Cache for static analysis has not been configured'.PHP_EOL, 81 | Result::FAILURE, 82 | ); 83 | } 84 | 85 | $this->codeCoverageFilterRegistry->init($this->configuration, true); 86 | 87 | if (! $this->codeCoverageFilterRegistry->configured()) { 88 | return Result::from( 89 | 'Filter for code coverage has not been configured'.PHP_EOL, 90 | Result::FAILURE, 91 | ); 92 | } 93 | 94 | $timer = new Timer; 95 | $timer->start(); 96 | 97 | (new CacheWarmer)->warmCache( 98 | $this->configuration->coverageCacheDirectory(), 99 | ! $this->configuration->disableCodeCoverageIgnore(), 100 | $this->configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage(), 101 | $this->codeCoverageFilterRegistry->get(), 102 | ); 103 | 104 | return Result::from(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /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 Pest\Logging\TeamCity\Subscriber; 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 | final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber 56 | { 57 | public function notify(Skipped $event): void 58 | { 59 | if (str_contains($event->message(), '__TODO__')) { 60 | $this->printTodoItem(); 61 | } 62 | 63 | $this->logger()->testSkipped($event); 64 | } 65 | 66 | /** 67 | * Prints a "T" to the standard PHPUnit output to indicate a todo item. 68 | */ 69 | private function printTodoItem(): void 70 | { 71 | $mirror = new ReflectionClass($this->printer()); 72 | $printerMirror = $mirror->getMethod('printProgress'); 73 | $printerMirror->invoke($this->printer(), 'T'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /resources/base-phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/ 10 | 11 | 12 | 13 | 14 | app 15 | src 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resources/views/components/badge.php: -------------------------------------------------------------------------------- 1 | ['blue', 'INFO'], 7 | 'ERROR' => ['red', 'ERROR'], 8 | }; 9 | 10 | ?> 11 | 12 |
13 | 14 | 15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /resources/views/components/new-line.php: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /resources/views/components/two-column-detail.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /resources/views/usage.php: -------------------------------------------------------------------------------- 1 |
2 | USAGE:pest') ?> [options] 3 |
4 | 5 | -------------------------------------------------------------------------------- /resources/views/version.php: -------------------------------------------------------------------------------- 1 |
2 | Pest Testing Framework. 3 |
4 | -------------------------------------------------------------------------------- /src/ArchPresets/AbstractPreset.php: -------------------------------------------------------------------------------- 1 | |ArchExpectation> 19 | */ 20 | protected array $expectations = []; 21 | 22 | /** 23 | * Creates a new preset instance. 24 | * 25 | * @param array $userNamespaces 26 | */ 27 | public function __construct( 28 | private readonly array $userNamespaces, 29 | ) { 30 | // 31 | } 32 | 33 | /** 34 | * Executes the arch preset. 35 | * 36 | * @internal 37 | */ 38 | abstract public function execute(): void; 39 | 40 | /** 41 | * Ignores the given "targets" or "dependencies". 42 | * 43 | * @param array|string $targetsOrDependencies 44 | */ 45 | final public function ignoring(array|string $targetsOrDependencies): void 46 | { 47 | $this->expectations = array_map( 48 | fn (ArchExpectation|Expectation $expectation): Expectation|ArchExpectation => $expectation instanceof ArchExpectation ? $expectation->ignoring($targetsOrDependencies) : $expectation, 49 | $this->expectations, 50 | ); 51 | } 52 | 53 | /** 54 | * Runs the given callback for each namespace. 55 | * 56 | * @param callable(Expectation): ArchExpectation ...$callbacks 57 | */ 58 | final public function eachUserNamespace(callable ...$callbacks): void 59 | { 60 | foreach ($this->userNamespaces as $namespace) { 61 | foreach ($callbacks as $callback) { 62 | $this->expectations[] = $callback(expect($namespace)); 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * Flushes the expectations. 69 | */ 70 | final public function flush(): void 71 | { 72 | $this->expectations = []; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ArchPresets/Custom.php: -------------------------------------------------------------------------------- 1 | $userNamespaces 20 | * @param Closure(array): array|ArchExpectation> $execute 21 | */ 22 | public function __construct( 23 | private readonly array $userNamespaces, 24 | private readonly string $name, 25 | private readonly Closure $execute, 26 | ) { 27 | parent::__construct($userNamespaces); 28 | } 29 | 30 | /** 31 | * Returns the name of the preset. 32 | */ 33 | public function name(): string 34 | { 35 | return $this->name; 36 | } 37 | 38 | /** 39 | * Executes the arch preset. 40 | */ 41 | public function execute(): void 42 | { 43 | $this->expectations = ($this->execute)($this->userNamespaces); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ArchPresets/Php.php: -------------------------------------------------------------------------------- 1 | expectations[] = expect([ 18 | 'debug_zval_dump', 19 | 'debug_backtrace', 20 | 'debug_print_backtrace', 21 | 'dump', 22 | 'ray', 23 | 'ds', 24 | 'die', 25 | 'goto', 26 | 'global', 27 | 'var_dump', 28 | 'phpinfo', 29 | 'echo', 30 | 'ereg', 31 | 'eregi', 32 | 'mysql_connect', 33 | 'mysql_pconnect', 34 | 'mysql_query', 35 | 'mysql_select_db', 36 | 'mysql_fetch_array', 37 | 'mysql_fetch_assoc', 38 | 'mysql_fetch_object', 39 | 'mysql_fetch_row', 40 | 'mysql_num_rows', 41 | 'mysql_affected_rows', 42 | 'mysql_free_result', 43 | 'mysql_insert_id', 44 | 'mysql_error', 45 | 'mysql_real_escape_string', 46 | 'print', 47 | 'print_r', 48 | 'var_export', 49 | 'xdebug_break', 50 | 'xdebug_call_class', 51 | 'xdebug_call_file', 52 | 'xdebug_call_int', 53 | 'xdebug_call_line', 54 | 'xdebug_code_coverage_started', 55 | 'xdebug_connect_to_client', 56 | 'xdebug_debug_zval', 57 | 'xdebug_debug_zval_stdout', 58 | 'xdebug_dump_superglobals', 59 | 'xdebug_get_code_coverage', 60 | 'xdebug_get_collected_errors', 61 | 'xdebug_get_function_count', 62 | 'xdebug_get_function_stack', 63 | 'xdebug_get_gc_run_count', 64 | 'xdebug_get_gc_total_collected_roots', 65 | 'xdebug_get_gcstats_filename', 66 | 'xdebug_get_headers', 67 | 'xdebug_get_monitored_functions', 68 | 'xdebug_get_profiler_filename', 69 | 'xdebug_get_stack_depth', 70 | 'xdebug_get_tracefile_name', 71 | 'xdebug_info', 72 | 'xdebug_is_debugger_active', 73 | 'xdebug_memory_usage', 74 | 'xdebug_notify', 75 | 'xdebug_peak_memory_usage', 76 | 'xdebug_print_function_stack', 77 | 'xdebug_set_filter', 78 | 'xdebug_start_code_coverage', 79 | 'xdebug_start_error_collection', 80 | 'xdebug_start_function_monitor', 81 | 'xdebug_start_gcstats', 82 | 'xdebug_start_trace', 83 | 'xdebug_stop_code_coverage', 84 | 'xdebug_stop_error_collection', 85 | 'xdebug_stop_function_monitor', 86 | 'xdebug_stop_gcstats', 87 | 'xdebug_stop_trace', 88 | 'xdebug_time_index', 89 | 'xdebug_var_dump', 90 | 'trap', 91 | ])->not->toBeUsed(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ArchPresets/Relaxed.php: -------------------------------------------------------------------------------- 1 | eachUserNamespace( 21 | fn (Expectation $namespace): ArchExpectation => $namespace->not->toUseStrictTypes(), 22 | fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeFinal(), 23 | fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHavePrivateMethods(), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ArchPresets/Security.php: -------------------------------------------------------------------------------- 1 | expectations[] = expect([ 18 | 'md5', 19 | 'sha1', 20 | 'uniqid', 21 | 'rand', 22 | 'mt_rand', 23 | 'tempnam', 24 | 'str_shuffle', 25 | 'shuffle', 26 | 'array_rand', 27 | 'eval', 28 | 'exec', 29 | 'shell_exec', 30 | 'system', 31 | 'passthru', 32 | 'create_function', 33 | 'unserialize', 34 | 'extract', 35 | 'parse_str', 36 | 'mb_parse_str', 37 | 'dl', 38 | 'assert', 39 | ])->not->toBeUsed(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ArchPresets/Strict.php: -------------------------------------------------------------------------------- 1 | eachUserNamespace( 21 | fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHaveProtectedMethods(), 22 | fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeAbstract(), 23 | fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictTypes(), 24 | fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictEquality(), 25 | fn (Expectation $namespace): ArchExpectation => $namespace->classes()->toBeFinal(), 26 | ); 27 | 28 | $this->expectations[] = expect([ 29 | 'sleep', 30 | 'usleep', 31 | ])->not->toBeUsed(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Bootstrappers/BootExcludeList.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/Bootstrappers/BootFiles.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/Bootstrappers/BootKernelDump.php: -------------------------------------------------------------------------------- 1 | add(KernelDump::class, $kernelDump = new KernelDump( 32 | $this->output, 33 | )); 34 | 35 | $kernelDump->enable(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Bootstrappers/BootOverrides.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public const FILES = [ 21 | '53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php', 22 | '77ffb7647b583bd82e37962c6fbdc4b04d3344d8a2c1ed103e625ed1ff7cb5c2' => 'Runner/ResultCache/DefaultResultCache.php', 23 | 'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php', 24 | '3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', 25 | '8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', 26 | 'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php', 27 | '8cfcb4999af79463eca51a42058e502ea4ddc776cba5677bf2f8eb6093e21a5c' => 'Event/Value/ThrowableBuilder.php', 28 | '86cd9bcaa53cdd59c5b13e58f30064a015c549501e7629d93b96893d4dee1eb1' => '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/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 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/Bootstrappers/BootView.php: -------------------------------------------------------------------------------- 1 | output); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Collision/Events.php: -------------------------------------------------------------------------------- 1 | context) === []) { 38 | return $description; 39 | } 40 | 41 | renderUsing(self::$output); 42 | 43 | [ 44 | 'assignees' => $assignees, 45 | 'issues' => $issues, 46 | 'prs' => $prs, 47 | ] = $context; 48 | 49 | if (($link = Project::getInstance()->issues) !== '') { 50 | $issuesDescription = array_map(fn (int $issue): string => sprintf('#%s', sprintf($link, $issue), $issue), $issues); 51 | } 52 | 53 | if (($link = Project::getInstance()->prs) !== '') { 54 | $prsDescription = array_map(fn (int $pr): string => sprintf('#%s', sprintf($link, $pr), $pr), $prs); 55 | } 56 | 57 | if (($link = Project::getInstance()->assignees) !== '' && count($assignees) > 0) { 58 | $assigneesDescription = array_map(fn (string $assignee): string => sprintf( 59 | '@%s', 60 | sprintf($link, $assignee), 61 | $assignee, 62 | ), $assignees); 63 | } 64 | 65 | if (count($assignees) > 0 || count($issues) > 0 || count($prs) > 0) { 66 | $description .= ' '.implode(', ', array_merge( 67 | $issuesDescription ?? [], 68 | $prsDescription ?? [], 69 | isset($assigneesDescription) ? ['['.implode(', ', $assigneesDescription).']'] : [], 70 | )); 71 | } 72 | 73 | return $description; 74 | } 75 | 76 | /** 77 | * Fires after the test method description is printed. 78 | */ 79 | public static function afterTestMethodDescription(TestResult $result): void 80 | { 81 | if (($context = $result->context) === []) { 82 | return; 83 | } 84 | 85 | renderUsing(self::$output); 86 | 87 | [ 88 | 'notes' => $notes, 89 | ] = $context; 90 | 91 | foreach ($notes as $note) { 92 | render(sprintf(<<<'HTML' 93 |
94 | // %s 95 |
96 | HTML, $note, 97 | )); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Concerns/Expectable.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function expect(mixed $value): Expectation 23 | { 24 | return new Expectation($value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Concerns/Extendable.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/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 | -------------------------------------------------------------------------------- /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/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/Configuration.php: -------------------------------------------------------------------------------- 1 | filename = str_ends_with($filename, DIRECTORY_SEPARATOR.'Pest.php') ? dirname($filename) : $filename; 28 | } 29 | 30 | /** 31 | * Use the given classes and traits in the given targets. 32 | */ 33 | public function in(string ...$targets): UsesCall 34 | { 35 | return (new UsesCall($this->filename, []))->in(...$targets); 36 | } 37 | 38 | /** 39 | * Depending on where is called, it will extend the given classes and traits globally or locally. 40 | */ 41 | public function extend(string ...$classAndTraits): UsesCall 42 | { 43 | return new UsesCall( 44 | $this->filename, 45 | array_values($classAndTraits) 46 | ); 47 | } 48 | 49 | /** 50 | * Depending on where is called, it will extend the given classes and traits globally or locally. 51 | */ 52 | public function extends(string ...$classAndTraits): UsesCall 53 | { 54 | return $this->extend(...$classAndTraits); 55 | } 56 | 57 | /** 58 | * Depending on where is called, it will add the given groups globally or locally. 59 | */ 60 | public function group(string ...$groups): UsesCall 61 | { 62 | return (new UsesCall($this->filename, []))->group(...$groups); 63 | } 64 | 65 | /** 66 | * Depending on where is called, it will extend the given classes and traits globally or locally. 67 | */ 68 | public function use(string ...$classAndTraits): UsesCall 69 | { 70 | return $this->extend(...$classAndTraits); 71 | } 72 | 73 | /** 74 | * Depending on where is called, it will extend the given classes and traits globally or locally. 75 | */ 76 | public function uses(string ...$classAndTraits): UsesCall 77 | { 78 | return $this->extends(...$classAndTraits); 79 | } 80 | 81 | /** 82 | * Gets the printer configuration. 83 | */ 84 | public function printer(): Configuration\Printer 85 | { 86 | return new Configuration\Printer; 87 | } 88 | 89 | /** 90 | * Gets the presets configuration. 91 | */ 92 | public function presets(): Configuration\Presets 93 | { 94 | return new Configuration\Presets; 95 | } 96 | 97 | /** 98 | * Gets the project configuration. 99 | */ 100 | public function project(): Configuration\Project 101 | { 102 | return Configuration\Project::getInstance(); 103 | } 104 | 105 | /** 106 | * Proxies calls to the uses method. 107 | * 108 | * @param array $arguments 109 | */ 110 | public function __call(string $name, array $arguments): mixed 111 | { 112 | return $this->uses()->$name(...$arguments); // @phpstan-ignore-line 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Configuration/Presets.php: -------------------------------------------------------------------------------- 1 | issues = "https://github.com/{$project}/issues/%s"; 52 | $this->prs = "https://github.com/{$project}/pull/%s"; 53 | 54 | $this->assignees = 'https://github.com/%s'; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Sets the test project to GitLab. 61 | */ 62 | public function gitlab(string $project): self 63 | { 64 | $this->issues = "https://gitlab.com/{$project}/issues/%s"; 65 | $this->prs = "https://gitlab.com/{$project}/merge_requests/%s"; 66 | 67 | $this->assignees = 'https://gitlab.com/%s'; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Sets the test project to Bitbucket. 74 | */ 75 | public function bitbucket(string $project): self 76 | { 77 | $this->issues = "https://bitbucket.org/{$project}/issues/%s"; 78 | $this->prs = "https://bitbucket.org/{$project}/pull-requests/%s"; 79 | 80 | $this->assignees = 'https://bitbucket.org/%s'; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Sets the test project to Jira. 87 | */ 88 | public function jira(string $namespace, string $project): self 89 | { 90 | $this->issues = "https://{$namespace}.atlassian.net/browse/{$project}-%s"; 91 | 92 | $this->assignees = "https://{$namespace}.atlassian.net/secure/ViewProfile.jspa?name=%s"; 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * Sets the test project to custom. 99 | */ 100 | public function custom(string $issues, string $prs, string $assignees): self 101 | { 102 | $this->issues = $issues; 103 | $this->prs = $prs; 104 | 105 | $this->assignees = $assignees; 106 | 107 | return $this; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /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 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/Console/Thanks.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | private const FUNDING_MESSAGES = [ 26 | 'Star' => 'https://github.com/pestphp/pest', 27 | 'YouTube' => 'https://youtube.com/@nunomaduro', 28 | 'TikTok' => 'https://tiktok.com/@nunomaduro', 29 | 'Twitch' => 'https://twitch.tv/enunomaduro', 30 | 'LinkedIn' => 'https://linkedin.com/in/nunomaduro', 31 | 'Instagram' => 'https://instagram.com/enunomaduro', 32 | 'X' => 'https://x.com/enunomaduro', 33 | 'Sponsor' => 'https://github.com/sponsors/nunomaduro', 34 | ]; 35 | 36 | /** 37 | * Creates a new Console Command instance. 38 | */ 39 | public function __construct( 40 | private InputInterface $input, 41 | private OutputInterface $output 42 | ) { 43 | // .. 44 | } 45 | 46 | /** 47 | * Executes the Console Command. 48 | */ 49 | public function __invoke(): void 50 | { 51 | $bootstrapper = new BootView($this->output); 52 | $bootstrapper->boot(); 53 | 54 | $wantsToSupport = false; 55 | 56 | if (getenv('PEST_NO_SUPPORT') !== 'true' && $this->input->isInteractive()) { 57 | $wantsToSupport = (new SymfonyQuestionHelper)->ask( 58 | new ArrayInput([]), 59 | $this->output, 60 | new ConfirmationQuestion( 61 | ' Wanna show Pest some love by starring it on GitHub?', 62 | false, 63 | ) 64 | ); 65 | 66 | View::render('components.new-line'); 67 | 68 | foreach (self::FUNDING_MESSAGES as $message => $link) { 69 | View::render('components.two-column-detail', [ 70 | 'left' => $message, 71 | 'right' => $link, 72 | ]); 73 | } 74 | 75 | View::render('components.new-line'); 76 | } 77 | 78 | if ($wantsToSupport === true) { 79 | if (PHP_OS_FAMILY === 'Darwin') { 80 | exec('open https://github.com/pestphp/pest'); 81 | } 82 | if (PHP_OS_FAMILY === 'Windows') { 83 | exec('start https://github.com/pestphp/pest'); 84 | } 85 | if (PHP_OS_FAMILY === 'Linux') { 86 | exec('xdg-open https://github.com/pestphp/pest'); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Contracts/ArchPreset.php: -------------------------------------------------------------------------------- 1 | $arguments 16 | * @return array 17 | */ 18 | public function handleArguments(array $arguments): array; 19 | } 20 | -------------------------------------------------------------------------------- /src/Contracts/Plugins/HandlesOriginalArguments.php: -------------------------------------------------------------------------------- 1 | $arguments 16 | */ 17 | public function handleOriginalArguments(array $arguments): void; 18 | } 19 | -------------------------------------------------------------------------------- /src/Contracts/Plugins/Terminable.php: -------------------------------------------------------------------------------- 1 | $attributes 18 | */ 19 | public static function code(iterable $attributes): string 20 | { 21 | return implode(PHP_EOL, array_map(function (Attribute $attribute): string { 22 | $name = $attribute->name; 23 | 24 | if ($attribute->arguments === []) { 25 | return " #[\\{$name}]"; 26 | } 27 | 28 | $arguments = array_map(fn (string $argument): string => var_export($argument, true), iterator_to_array($attribute->arguments)); 29 | 30 | return sprintf(' #[\\%s(%s)]', $name, implode(', ', $arguments)); 31 | }, iterator_to_array($attributes))); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Exceptions/AfterAllAlreadyExist.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 | -------------------------------------------------------------------------------- /src/Exceptions/ExpectationNotFound.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/InvalidExpectationValue.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/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/TestAlreadyExist.php: -------------------------------------------------------------------------------- 1 | description, 27 | $method->filename 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Exceptions/TestDescriptionMissing.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class EachExpectation 19 | { 20 | /** 21 | * Indicates if the expectation is the opposite. 22 | */ 23 | private bool $opposite = false; 24 | 25 | /** 26 | * Creates an expectation on each item of the iterable "value". 27 | * 28 | * @param Expectation $original 29 | */ 30 | public function __construct(private readonly Expectation $original) {} 31 | 32 | /** 33 | * Creates a new expectation. 34 | * 35 | * @template TAndValue 36 | * 37 | * @param TAndValue $value 38 | * @return Expectation 39 | */ 40 | public function and(mixed $value): Expectation 41 | { 42 | return $this->original->and($value); 43 | } 44 | 45 | /** 46 | * Creates the opposite expectation for the value. 47 | * 48 | * @return self 49 | */ 50 | public function not(): self 51 | { 52 | $this->opposite = true; 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * Dynamically calls methods on the class with the given arguments on each item. 59 | * 60 | * @param array $arguments 61 | * @return self 62 | */ 63 | public function __call(string $name, array $arguments): self 64 | { 65 | foreach ($this->original->value as $item) { 66 | /* @phpstan-ignore-next-line */ 67 | $this->opposite ? expect($item)->not()->$name(...$arguments) : expect($item)->$name(...$arguments); 68 | } 69 | 70 | $this->opposite = false; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Dynamically calls methods on the class without any arguments on each item. 77 | * 78 | * @return self 79 | */ 80 | public function __get(string $name): self 81 | { 82 | /* @phpstan-ignore-next-line */ 83 | return $this->$name(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Factories/Attribute.php: -------------------------------------------------------------------------------- 1 | $arguments 14 | */ 15 | public function __construct(public string $name, public iterable $arguments) 16 | { 17 | // 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Factories/Concerns/HigherOrderable.php: -------------------------------------------------------------------------------- 1 | chains = new HigherOrderMessageCollection; 32 | $this->factoryProxies = new HigherOrderMessageCollection; 33 | $this->proxies = new HigherOrderMessageCollection; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Factories/Covers/CoversClass.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 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/ServiceMessage.php: -------------------------------------------------------------------------------- 1 | $parameters 19 | */ 20 | public function __construct( 21 | private readonly string $type, 22 | private readonly array $parameters, 23 | ) {} 24 | 25 | public function toString(): string 26 | { 27 | $paramsToString = ''; 28 | 29 | foreach ([...$this->parameters, 'flowId' => self::$flowId] as $key => $value) { 30 | $value = $this->escapeServiceMessage((string) $value); 31 | $paramsToString .= " $key='$value'"; 32 | } 33 | 34 | return "##teamcity[$this->type$paramsToString]"; 35 | } 36 | 37 | public static function testSuiteStarted(string $name, ?string $location): self 38 | { 39 | return new self('testSuiteStarted', [ 40 | 'name' => $name, 41 | 'locationHint' => $location === null ? null : "pest_qn://$location", 42 | ]); 43 | } 44 | 45 | public static function testSuiteCount(int $count): self 46 | { 47 | return new self('testCount', [ 48 | 'count' => $count, 49 | ]); 50 | } 51 | 52 | public static function testSuiteFinished(string $name): self 53 | { 54 | return new self('testSuiteFinished', [ 55 | 'name' => $name, 56 | ]); 57 | } 58 | 59 | public static function testStarted(string $name, string $location): self 60 | { 61 | return new self('testStarted', [ 62 | 'name' => $name, 63 | 'locationHint' => "pest_qn://$location", 64 | ]); 65 | } 66 | 67 | /** 68 | * @param int $duration in milliseconds 69 | */ 70 | public static function testFinished(string $name, int $duration): self 71 | { 72 | return new self('testFinished', [ 73 | 'name' => $name, 74 | 'duration' => $duration, 75 | ]); 76 | } 77 | 78 | public static function testStdOut(string $name, string $data): self 79 | { 80 | if (! str_ends_with($data, "\n")) { 81 | $data .= "\n"; 82 | } 83 | 84 | return new self('testStdOut', [ 85 | 'name' => $name, 86 | 'out' => $data, 87 | ]); 88 | } 89 | 90 | public static function testFailed(string $name, string $message, string $details): self 91 | { 92 | return new self('testFailed', [ 93 | 'name' => $name, 94 | 'message' => $message, 95 | 'details' => $details, 96 | ]); 97 | } 98 | 99 | public static function testStdErr(string $name, string $data): self 100 | { 101 | if (! str_ends_with($data, "\n")) { 102 | $data .= "\n"; 103 | } 104 | 105 | return new self('testStdErr', [ 106 | 'name' => $name, 107 | 'out' => $data, 108 | ]); 109 | } 110 | 111 | public static function testIgnored(string $name, string $message, ?string $details = null): self 112 | { 113 | return new self('testIgnored', [ 114 | 'name' => $name, 115 | 'message' => $message, 116 | 'details' => $details, 117 | ]); 118 | } 119 | 120 | public static function comparisonFailure(string $name, string $message, string $details, string $actual, string $expected): self 121 | { 122 | return new self('testFailed', [ 123 | 'name' => $name, 124 | 'message' => $message, 125 | 'details' => $details, 126 | 'type' => 'comparisonFailure', 127 | 'actual' => $actual, 128 | 'expected' => $expected, 129 | ]); 130 | } 131 | 132 | private function escapeServiceMessage(string $text): string 133 | { 134 | return str_replace( 135 | ['|', "'", "\n", "\r", ']', '['], 136 | ['||', "|'", '|n', '|r', '|]', '|['], 137 | $text 138 | ); 139 | } 140 | 141 | public static function setFlowId(int $flowId): void 142 | { 143 | self::$flowId = $flowId; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/Subscriber.php: -------------------------------------------------------------------------------- 1 | logger; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestConsideredRiskySubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testConsideredRisky($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestErroredSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testErrored($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestExecutionFinishedSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testExecutionFinished($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestFailedSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testFailed($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/Logging/TeamCity/Subscriber/TestSkippedSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testSkipped($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestSuiteFinishedSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testSuiteFinished($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Logging/TeamCity/Subscriber/TestSuiteStartedSubscriber.php: -------------------------------------------------------------------------------- 1 | logger()->testSuiteStarted($event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Matchers/Any.php: -------------------------------------------------------------------------------- 1 | getPrevious())) { 32 | $throwable = $previous; 33 | } 34 | 35 | $panic = new self($throwable); 36 | 37 | $panic->handle(); 38 | 39 | exit(1); 40 | } 41 | 42 | /** 43 | * Handles the panic. 44 | */ 45 | private function handle(): void 46 | { 47 | try { 48 | $output = Container::getInstance()->get(OutputInterface::class); 49 | } catch (Throwable) { 50 | $output = new ConsoleOutput; 51 | } 52 | 53 | assert($output instanceof OutputInterface); 54 | 55 | if ($this->throwable instanceof Contracts\Panicable) { 56 | $this->throwable->render($output); 57 | 58 | exit($this->throwable->exitCode()); 59 | } 60 | 61 | $writer = new Writer(null, $output); 62 | 63 | $inspector = new Inspector($this->throwable); 64 | 65 | $writer->write($inspector); 66 | $output->writeln(''); 67 | 68 | exit(1); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/PendingCalls/AfterEachCall.php: -------------------------------------------------------------------------------- 1 | closure = $closure instanceof Closure ? $closure : NullClosure::create(); 42 | 43 | $this->proxies = new HigherOrderMessageCollection; 44 | 45 | $this->describing = DescribeCall::describing(); 46 | } 47 | 48 | /** 49 | * Creates the Call. 50 | */ 51 | public function __destruct() 52 | { 53 | $describing = $this->describing; 54 | 55 | $proxies = $this->proxies; 56 | 57 | $afterEachTestCase = ChainableClosure::boundWhen( 58 | fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true), 59 | ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), 60 | )->bindTo($this, self::class); 61 | 62 | assert($afterEachTestCase instanceof Closure); 63 | 64 | $this->testSuite->afterEach->set( 65 | $this->filename, 66 | $this, 67 | $afterEachTestCase, 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/PendingCalls/BeforeEachCall.php: -------------------------------------------------------------------------------- 1 | closure = $closure instanceof Closure ? $closure : NullClosure::create(); 50 | 51 | $this->testCallProxies = new HigherOrderMessageCollection; 52 | $this->testCaseProxies = new HigherOrderMessageCollection; 53 | 54 | $this->describing = DescribeCall::describing(); 55 | } 56 | 57 | /** 58 | * Creates the Call. 59 | */ 60 | public function __destruct() 61 | { 62 | $describing = $this->describing; 63 | $testCaseProxies = $this->testCaseProxies; 64 | 65 | $beforeEachTestCall = function (TestCall $testCall) use ($describing): void { 66 | 67 | if ($this->describing !== []) { 68 | if (Arr::last($describing) !== Arr::last($this->describing)) { 69 | return; 70 | } 71 | 72 | if (! in_array(Arr::last($describing), $testCall->describing, true)) { 73 | return; 74 | } 75 | } 76 | 77 | $this->testCallProxies->chain($testCall); 78 | }; 79 | 80 | $beforeEachTestCase = ChainableClosure::boundWhen( 81 | fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true), 82 | ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), 83 | )->bindTo($this, self::class); 84 | 85 | assert($beforeEachTestCase instanceof Closure); 86 | 87 | $this->testSuite->beforeEach->set( 88 | $this->filename, 89 | $this, 90 | $beforeEachTestCall, 91 | $beforeEachTestCase, 92 | ); 93 | } 94 | 95 | /** 96 | * Runs the given closure after the test. 97 | */ 98 | public function after(Closure $closure): self 99 | { 100 | if ($this->describing === []) { 101 | throw new AfterBeforeTestFunction($this->filename); 102 | } 103 | 104 | return $this->__call('after', [$closure]); 105 | } 106 | 107 | /** 108 | * Saves the calls to be used on the target. 109 | * 110 | * @param array $arguments 111 | */ 112 | public function __call(string $name, array $arguments): self 113 | { 114 | if (method_exists(TestCall::class, $name)) { 115 | $this->testCallProxies 116 | ->add(Backtrace::file(), Backtrace::line(), $name, $arguments); 117 | 118 | return $this; 119 | } 120 | 121 | $this->testCaseProxies 122 | ->add(Backtrace::file(), Backtrace::line(), $name, $arguments); 123 | 124 | return $this; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/PendingCalls/Concerns/Describable.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public array $__describing; 18 | 19 | /** 20 | * The describing of the test case. 21 | * 22 | * @var array 23 | */ 24 | public array $describing = []; 25 | } 26 | -------------------------------------------------------------------------------- /src/PendingCalls/DescribeCall.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private static array $describing = []; 22 | 23 | /** 24 | * The describe "before each" call. 25 | */ 26 | private ?BeforeEachCall $currentBeforeEachCall = null; 27 | 28 | /** 29 | * Creates a new Pending Call. 30 | */ 31 | public function __construct( 32 | public readonly TestSuite $testSuite, 33 | public readonly string $filename, 34 | public readonly string $description, 35 | public readonly Closure $tests 36 | ) { 37 | // 38 | } 39 | 40 | /** 41 | * What is the current describing. 42 | * 43 | * @return array 44 | */ 45 | public static function describing(): array 46 | { 47 | return self::$describing; 48 | } 49 | 50 | /** 51 | * Creates the Call. 52 | */ 53 | public function __destruct() 54 | { 55 | unset($this->currentBeforeEachCall); 56 | 57 | self::$describing[] = $this->description; 58 | 59 | try { 60 | ($this->tests)(); 61 | } finally { 62 | array_pop(self::$describing); 63 | } 64 | } 65 | 66 | /** 67 | * Dynamically calls methods on each test call. 68 | * 69 | * @param array $arguments 70 | */ 71 | public function __call(string $name, array $arguments): self 72 | { 73 | $filename = Backtrace::file(); 74 | 75 | if (! $this->currentBeforeEachCall instanceof \Pest\PendingCalls\BeforeEachCall) { 76 | $this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename); 77 | 78 | $this->currentBeforeEachCall->describing[] = $this->description; 79 | } 80 | 81 | $this->currentBeforeEachCall->{$name}(...$arguments); 82 | 83 | return $this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Pest.php: -------------------------------------------------------------------------------- 1 | testPath.DIRECTORY_SEPARATOR.$file; 15 | } 16 | -------------------------------------------------------------------------------- /src/Plugin.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/Plugins/Actions/CallsAddsOutput.php: -------------------------------------------------------------------------------- 1 | addOutput($exitCode); 27 | } 28 | 29 | return $exitCode; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Plugins/Actions/CallsBoot.php: -------------------------------------------------------------------------------- 1 | boot(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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/Actions/CallsHandleOriginalArguments.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/Plugins/Actions/CallsTerminable.php: -------------------------------------------------------------------------------- 1 | terminate(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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/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/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((string) $arg, "$argument=")) { // @phpstan-ignore-line 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/Configuration.php: -------------------------------------------------------------------------------- 1 | hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) { 38 | return $arguments; 39 | } 40 | 41 | $arguments = $this->pushArgument('--configuration', $arguments); 42 | 43 | return $this->pushArgument((string) realpath($this->fromGeneratedConfigurationFile()), $arguments); 44 | } 45 | 46 | /** 47 | * Get the configuration file from the generated configuration file. 48 | */ 49 | private function fromGeneratedConfigurationFile(): string 50 | { 51 | $path = $this->getTempPhpunitXmlPath(); 52 | if (file_exists($path)) { 53 | unlink($path); 54 | } 55 | 56 | $doc = new DOMDocument; 57 | $doc->load(self::BASE_PHPUNIT_FILE); 58 | 59 | $contents = $doc->saveXML(); 60 | 61 | assert(is_int(file_put_contents($path, $contents))); 62 | 63 | return $path; 64 | } 65 | 66 | /** 67 | * Check if the configuration file is custom. 68 | */ 69 | private function hasCustomConfigurationFile(): bool 70 | { 71 | $cliConfiguration = (new CliConfigurationBuilder)->fromParameters([]); 72 | $configurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration); 73 | 74 | return is_string($configurationFile); 75 | } 76 | 77 | /** 78 | * Get the temporary phpunit.xml path. 79 | */ 80 | private function getTempPhpunitXmlPath(): string 81 | { 82 | return getcwd().'/.pest.xml'; 83 | } 84 | 85 | /** 86 | * Terminates the plugin. 87 | */ 88 | public function terminate(): void 89 | { 90 | $path = $this->getTempPhpunitXmlPath(); 91 | 92 | if (file_exists($path)) { 93 | unlink($path); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /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/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 TestSuite $testSuite, 41 | private InputInterface $input, 42 | private 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/framework'); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /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/Plugins/Only.php: -------------------------------------------------------------------------------- 1 | group($group); 48 | 49 | if (Environment::name() === Environment::CI || Parallel::isWorker()) { 50 | return; 51 | } 52 | 53 | $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; 54 | 55 | if (file_exists($lockFile) && $group === '__pest_only') { 56 | file_put_contents($lockFile, $group); 57 | 58 | return; 59 | } 60 | 61 | if (! file_exists($lockFile)) { 62 | touch($lockFile); 63 | 64 | file_put_contents($lockFile, $group); 65 | } 66 | } 67 | 68 | /** 69 | * Checks if "only" mode is enabled. 70 | */ 71 | public static function isEnabled(): bool 72 | { 73 | $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; 74 | 75 | return file_exists($lockFile); 76 | } 77 | 78 | /** 79 | * Returns the group name. 80 | */ 81 | public static function group(): string 82 | { 83 | $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; 84 | 85 | if (! file_exists($lockFile)) { 86 | return '__pest_only'; 87 | } 88 | 89 | return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Plugins/Parallel/Contracts/HandlersWorkerArguments.php: -------------------------------------------------------------------------------- 1 | $arguments 11 | * @return array 12 | */ 13 | public function handleWorkerArguments(array $arguments): array; 14 | } 15 | -------------------------------------------------------------------------------- /src/Plugins/Parallel/Handlers/Laravel.php: -------------------------------------------------------------------------------- 1 | whenUsingLaravel($arguments, function (array $arguments): array { 30 | $this->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 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/Plugins/Parallel/Handlers/Parallel.php: -------------------------------------------------------------------------------- 1 | $this->popArgument($arg, $args), $arguments); 34 | 35 | return $this->pushArgument('--runner='.WrapperRunner::class, $args); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Plugins/Parallel/Handlers/Pest.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/Plugins/Printer.php: -------------------------------------------------------------------------------- 1 | pushArgument('--no-output', $arguments); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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/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/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/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/Plugins/Version.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/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/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 | -------------------------------------------------------------------------------- /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/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/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/Subscribers/EnsureConfigurationIsAvailable.php: -------------------------------------------------------------------------------- 1 | add(Configuration::class, $event->configuration()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Subscribers/EnsureKernelDumpIsFlushed.php: -------------------------------------------------------------------------------- 1 | get(KernelDump::class); 23 | 24 | assert($kernelDump instanceof KernelDump); 25 | 26 | $kernelDump->disable(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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/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 | /** 86 | * Returns the value of the last element or false for empty array 87 | * 88 | * @param array $array 89 | */ 90 | public static function last(array $array): mixed 91 | { 92 | return end($array); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Support/Backtrace.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | private array $instances = []; 25 | 26 | /** 27 | * Gets a new or already existing container. 28 | */ 29 | public static function getInstance(): self 30 | { 31 | if (! self::$instance instanceof \Pest\Support\Container) { 32 | self::$instance = new self; 33 | } 34 | 35 | return self::$instance; 36 | } 37 | 38 | /** 39 | * Gets a dependency from the container. 40 | */ 41 | public function get(string $id): object|string 42 | { 43 | if (! array_key_exists($id, $this->instances)) { 44 | /** @var class-string $id */ 45 | $this->instances[$id] = $this->build($id); 46 | } 47 | 48 | return $this->instances[$id]; 49 | } 50 | 51 | /** 52 | * Adds the given instance to the container. 53 | * 54 | * @return $this 55 | */ 56 | public function add(string $id, object|string $instance): self 57 | { 58 | $this->instances[$id] = $instance; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Tries to build the given instance. 65 | * 66 | * @template TObject of object 67 | * 68 | * @param class-string $id 69 | * @return TObject 70 | */ 71 | private function build(string $id): object 72 | { 73 | $reflectionClass = new ReflectionClass($id); 74 | 75 | if ($reflectionClass->isInstantiable()) { 76 | $constructor = $reflectionClass->getConstructor(); 77 | 78 | if ($constructor instanceof \ReflectionMethod) { 79 | $params = array_map( 80 | function (ReflectionParameter $param) use ($id): object|string { 81 | $candidate = Reflection::getParameterClassName($param); 82 | 83 | if ($candidate === null) { 84 | $type = $param->getType(); 85 | /* @phpstan-ignore-next-line */ 86 | if ($type instanceof \ReflectionType && $type->isBuiltin()) { 87 | $candidate = $param->getName(); 88 | } else { 89 | throw ShouldNotHappen::fromMessage(sprintf('The type of `$%s` in `%s` cannot be determined.', $id, $param->getName())); 90 | } 91 | } 92 | 93 | return $this->get($candidate); 94 | }, 95 | $constructor->getParameters() 96 | ); 97 | 98 | return $reflectionClass->newInstanceArgs($params); 99 | } 100 | 101 | return $reflectionClass->newInstance(); 102 | } 103 | 104 | throw ShouldNotHappen::fromMessage(sprintf('A dependency with the name `%s` cannot be resolved.', $id)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Support/DatasetInfo.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 [pest()->extend()] 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/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/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 | $result[] = $context->contains($data[$key]) !== false 68 | ? '*RECURSION*' 69 | // @phpstan-ignore-next-line 70 | : sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context)); 71 | } 72 | 73 | return implode(', ', $result); 74 | } 75 | 76 | /** 77 | * Exports a value into a single-line string. 78 | */ 79 | public function shortenedExport(mixed $value): string 80 | { 81 | $map = [ 82 | '#\.{3}#' => '…', 83 | '#\\\n\s*#' => '', 84 | '# Object \(…\)#' => '', 85 | ]; 86 | 87 | return (string) preg_replace(array_keys($map), array_values($map), $this->exporter->shortenedExport($value)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /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 | public function defer(callable $callable): object 56 | { 57 | Reflection::bindCallableWithData($callable); 58 | 59 | return $this->target; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 | return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments); 54 | } 55 | 56 | try { 57 | return is_array($this->arguments) 58 | ? Reflection::call($target, $this->name, $this->arguments) 59 | : $target->{$this->name}; 60 | } catch (Throwable $throwable) { 61 | Reflection::setPropertyValue($throwable, 'file', $this->filename); 62 | Reflection::setPropertyValue($throwable, 'line', $this->line); 63 | 64 | if ($throwable->getMessage() === $this->getUndefinedMethodMessage($target, $this->name)) { 65 | /** @var ReflectionClass $reflection */ 66 | $reflection = new ReflectionClass($target); 67 | $reflection = $reflection->getParentClass() ?: $reflection; 68 | Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->name)); 69 | } 70 | 71 | throw $throwable; 72 | } 73 | } 74 | 75 | /** 76 | * Indicates that this message should only be called when the given condition is true. 77 | * 78 | * @param callable(): bool $condition 79 | */ 80 | public function when(callable $condition): self 81 | { 82 | $this->condition = Closure::fromCallable($condition); 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Determines whether or not there exists a higher order callable with the message name. 89 | */ 90 | private function hasHigherOrderCallable(): bool 91 | { 92 | return in_array($this->name, get_class_methods(HigherOrderCallables::class), true); 93 | } 94 | 95 | private function getUndefinedMethodMessage(object $target, string $methodName): string 96 | { 97 | return sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /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 | $target = $message->call($target) ?? $target; 44 | } 45 | } 46 | 47 | /** 48 | * Proxy all the messages to the target. 49 | */ 50 | public function proxy(object $target): void 51 | { 52 | foreach ($this->messages as $message) { 53 | $message->call($target); 54 | } 55 | } 56 | 57 | /** 58 | * Count the number of messages with the given name. 59 | * 60 | * @param string $name A higher order message name (usually a method name) 61 | */ 62 | public function count(string $name): int 63 | { 64 | return array_reduce( 65 | $this->messages, 66 | static fn (int $total, HigherOrderMessage $message): int => $total + (int) ($name === $message->name), 67 | 0, 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Support/HigherOrderTapProxy.php: -------------------------------------------------------------------------------- 1 | target->{$property} = $value; 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}; 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/Support/NullClosure.php: -------------------------------------------------------------------------------- 1 | 0; 102 | } 103 | 104 | /** 105 | * Creates a describe block as `$describeDescription` → `$testDescription` format. 106 | * 107 | * @param array $describeDescriptions 108 | */ 109 | public static function describe(array $describeDescriptions, string $testDescription): string 110 | { 111 | $descriptionComponents = [...$describeDescriptions, $testDescription]; 112 | 113 | return sprintf(str_repeat('`%s` → ', count($describeDescriptions)).'%s', ...$descriptionComponents); 114 | } 115 | 116 | /** 117 | * Determine if a given value is a valid URL. 118 | */ 119 | public static function isUrl(string $value): bool 120 | { 121 | return (bool) filter_var($value, FILTER_VALIDATE_URL); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /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/TestCaseFilters/GitDirtyTestCaseFilter.php: -------------------------------------------------------------------------------- 1 | |null 18 | */ 19 | private ?array $changedFiles = null; 20 | 21 | /** 22 | * Creates a new instance of the filter. 23 | */ 24 | public function __construct(private readonly string $projectRoot) 25 | { 26 | // ... 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function accept(string $testCaseFilename): bool 33 | { 34 | if ($this->changedFiles === null) { 35 | $this->loadChangedFiles(); 36 | } 37 | 38 | assert(is_array($this->changedFiles)); 39 | 40 | $relativePath = str_replace($this->projectRoot, '', $testCaseFilename); 41 | $relativePath = str_replace(DIRECTORY_SEPARATOR, '/', $relativePath); 42 | 43 | if (str_starts_with($relativePath, '/')) { 44 | $relativePath = substr($relativePath, 1); 45 | } 46 | 47 | return in_array($relativePath, $this->changedFiles, true); 48 | } 49 | 50 | /** 51 | * Loads the changed files. 52 | */ 53 | private function loadChangedFiles(): void 54 | { 55 | $process = new Process(['git', 'status', '--short', '--', '*.php']); 56 | $process->run(); 57 | 58 | if (! $process->isSuccessful()) { 59 | throw new MissingDependency('Filter by dirty files', 'git'); 60 | } 61 | 62 | $output = preg_split('/\R+/', $process->getOutput(), flags: PREG_SPLIT_NO_EMPTY); 63 | assert(is_array($output)); 64 | 65 | $dirtyFiles = []; 66 | 67 | foreach ($output as $dirtyFile) { 68 | $dirtyFiles[substr($dirtyFile, 3)] = trim(substr($dirtyFile, 0, 3)); 69 | } 70 | 71 | $dirtyFiles = array_filter($dirtyFiles, fn (string $status): bool => $status !== 'D'); 72 | 73 | $dirtyFiles = array_map( 74 | fn (string $file, string $status): string => in_array($status, ['R', 'RM'], true) 75 | ? explode(' -> ', $file)[1] 76 | : $file, array_keys($dirtyFiles), $dirtyFiles, 77 | ); 78 | 79 | $dirtyFiles = array_filter( 80 | $dirtyFiles, 81 | fn (string $file): bool => str_starts_with('.'.DIRECTORY_SEPARATOR.$file, TestSuite::getInstance()->testPath) 82 | || str_starts_with($file, TestSuite::getInstance()->testPath) 83 | ); 84 | 85 | $dirtyFiles = array_values($dirtyFiles); 86 | 87 | if ($dirtyFiles === []) { 88 | Panic::with(new NoDirtyTestsFound); 89 | } 90 | 91 | $this->changedFiles = $dirtyFiles; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/TestCaseMethodFilters/AssigneeTestCaseFilter.php: -------------------------------------------------------------------------------- 1 | assignees, fn (string $assignee): bool => str_starts_with($assignee, $this->assignee)) !== []; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/TestCaseMethodFilters/IssueTestCaseFilter.php: -------------------------------------------------------------------------------- 1 | number, $factory->issues, true); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/TestCaseMethodFilters/NotesTestCaseFilter.php: -------------------------------------------------------------------------------- 1 | notes !== []; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/TestCaseMethodFilters/PrTestCaseFilter.php: -------------------------------------------------------------------------------- 1 | number, $factory->prs, true); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/TestCaseMethodFilters/TodoTestCaseFilter.php: -------------------------------------------------------------------------------- 1 | todo; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/TestCases/IgnorableTestCase.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 | -------------------------------------------------------------------------------- /stubs/Browser.php: -------------------------------------------------------------------------------- 1 | browse(function (Browser $browser) { 7 | $browser->visit('/{name}') 8 | ->assertSee('{name}'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /stubs/Dataset.php: -------------------------------------------------------------------------------- 1 | get('/{name}'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /stubs/Unit.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /stubs/init-laravel/Feature/ExampleTest.php.stub: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /stubs/init-laravel/Pest.php.stub: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class) 15 | // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) 16 | ->in('Feature'); 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Expectations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When you're writing tests, you often need to check that values meet certain conditions. The 24 | | "expect()" function gives you access to a set of "expectations" methods that you can use 25 | | to assert different things. Of course, you may extend the Expectation API at any time. 26 | | 27 | */ 28 | 29 | expect()->extend('toBeOne', function () { 30 | return $this->toBe(1); 31 | }); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Functions 36 | |-------------------------------------------------------------------------- 37 | | 38 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 39 | | project that you don't want to repeat in every file. Here you can also expose helpers as 40 | | global functions to help you to reduce the number of lines of code in your test files. 41 | | 42 | */ 43 | 44 | function something() 45 | { 46 | // .. 47 | } 48 | -------------------------------------------------------------------------------- /stubs/init-laravel/TestCase.php.stub: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /stubs/init-laravel/phpunit.xml.stub: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /stubs/init/Feature/ExampleTest.php.stub: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /stubs/init/Pest.php.stub: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class)->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 | -------------------------------------------------------------------------------- /stubs/init/TestCase.php.stub: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /stubs/init/phpunit.xml.stub: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | app 15 | src 16 | 17 | 18 | 19 | --------------------------------------------------------------------------------