diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 3373eafb9..5cce40fc6 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -19,20 +19,24 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.3 tools: composer:v2 coverage: none + extensions: sockets - name: Install Dependencies run: composer update --prefer-stable --no-interaction --no-progress --ansi - # - name: Type Check - # run: composer test:type:check + - name: Profanity Check + run: composer test:profanity + + - name: Type Check + run: composer test:type:check - name: Type Coverage run: composer test:type:coverage diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c4902b423..68d070dad 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,16 +12,16 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - symfony: ['7.1'] - php: ['8.2', '8.3', '8.4'] - dependency_version: [prefer-lowest, prefer-stable] + os: [ubuntu-latest, macos-latest] # windows-latest + symfony: ['7.3'] + php: ['8.3', '8.4', '8.5'] + dependency_version: [prefer-stable] name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -29,6 +29,7 @@ jobs: php-version: ${{ matrix.php }} tools: composer:v2 coverage: none + extensions: sockets - name: Setup Problem Matches run: | diff --git a/README.md b/README.md index acb06126f..ed0020c0c 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,24 @@

- PEST + PEST

- GitHub Workflow Status (master) + GitHub Workflow Status (master) Total Downloads Latest Version License + Why PHP in 2026

------ -> Pest v3 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest3-now-available)**. +> Pest v4 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest-v4-is-here-now-with-browser-testing)**. **Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP. - Explore our docs at **[pestphp.com »](https://pestphp.com)** - Follow the creator Nuno Maduro: - - YouTube: **[youtube.com/@nunomaduro](https://www.youtube.com/@nunomaduro)** — Videos every weekday - - Twitch: **[twitch.tv/enunomaduro](https://www.twitch.tv/enunomaduro)** — Streams (almost) every weekday + - YouTube: **[youtube.com/@nunomaduro](https://youtube.com/@nunomaduro)** — Videos every week + - Twitch: **[twitch.tv/nunomaduro](https://twitch.tv/nunomaduro)** — Live coding on Mondays, Wednesdays, and Fridays at 9PM UTC - Twitter / X: **[x.com/enunomaduro](https://x.com/enunomaduro)** - LinkedIn: **[linkedin.com/in/nunomaduro](https://www.linkedin.com/in/nunomaduro)** - Instagram: **[instagram.com/enunomaduro](https://www.instagram.com/enunomaduro)** @@ -30,23 +31,23 @@ We cannot thank our sponsors enough for their incredible support in funding Pest ### Platinum Sponsors -- **[Laracasts](https://laracasts.com/?ref=pestphp)** +- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)** +- **[Devin](https://devin.ai/?ref=nunomaduro)** +- **[Mailtrap](https://l.rw.rw/pestphp)** +- **[Tighten](https://tighten.com/?ref=nunomaduro)** +- **[Redberry](https://redberry.international/laravel-development/?utm_source=pest&utm_medium=banner&utm_campaign=pest_sponsorship)** ### Gold Sponsors -- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)** -- **[NativePHP](https://nativephp.com/mobile?ref=pestphp.com)** - **[CMS Max](https://cmsmax.com/?ref=pestphp)** ### Premium Sponsors +- [Zapiet](https://zapiet.com/?ref=pestphp) +- [Load Forge](https://loadforge.com/?ref=pestphp) +- [Route4Me](https://route4me.com/pt?ref=pestphp) +- [Nerdify](https://getnerdify.com/?ref=pestphp) - [Akaunting](https://akaunting.com/?ref=pestphp) -- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp) -- [Localazy](https://localazy.com/?ref=pestphp) -- [Forge](https://forge.laravel.com/?ref=pestphp) -- [Route4Me](https://www.route4me.com/?ref=pestphp) -- [Spatie](https://spatie.be/?ref=pestphp) -- [Worksome](https://www.worksome.com/?ref=pestphp) -- [Zapiet](https://www.zapiet.com/?ref=pestphp) +- [LambdaTest](https://lambdatest.com/?ref=pestphp) Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. diff --git a/RELEASE.md b/RELEASE.md index f0ad61139..6ba2a721e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,10 +2,10 @@ When releasing a new version of Pest there are some checks and updates that need to be done: -> **For Pest v2 you should use the `2.x` branch instead.** +> **For Pest v3 you should use the `3.x` branch instead.** -- Clear your local repository with: `git add . && git reset --hard && git checkout 3.x` -- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...3.x](https://github.com/pestphp/pest/compare/{latest_version}...3.x) +- Clear your local repository with: `git add . && git reset --hard && git checkout 4.x` +- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...4.x](https://github.com/pestphp/pest/compare/{latest_version}...4.x) - Update the version number in [src/Pest.php](src/Pest.php) - Run the tests locally using: `composer test` - Commit the Pest file with the message: `git commit -m "release: vX.X.X"` diff --git a/bin/worker.php b/bin/worker.php index c26b05eba..dc69d67e3 100644 --- a/bin/worker.php +++ b/bin/worker.php @@ -86,7 +86,7 @@ $getopt['teamcity-file'] ?? null, $getopt['testdox-file'] ?? null, isset($getopt['testdox-color']), - $getopt['testdox-columns'] ?? null, + (int) ($getopt['testdox-columns'] ?? null), ); while (true) { diff --git a/composer.json b/composer.json index 60d16d8f0..9d7fcdee7 100644 --- a/composer.json +++ b/composer.json @@ -17,19 +17,21 @@ } ], "require": { - "php": "^8.2.0", - "brianium/paratest": "^7.8.4", - "nunomaduro/collision": "^8.8.2", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest-plugin": "^3.0.0", - "pestphp/pest-plugin-arch": "^3.1.1", - "pestphp/pest-plugin-mutate": "^3.0.5", - "phpunit/phpunit": "^11.5.33" + "php": "^8.3.0", + "brianium/paratest": "^7.16.1", + "nunomaduro/collision": "^8.8.3", + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest-plugin": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.0", + "pestphp/pest-plugin-mutate": "^4.0.1", + "pestphp/pest-plugin-profanity": "^4.2.1", + "phpunit/phpunit": "^12.5.8", + "symfony/process": "^7.4.4|^8.0.0" }, "conflict": { - "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">11.5.33", - "sebastian/exporter": "<6.0.0", + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.5.8", + "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "autoload": { @@ -53,9 +55,10 @@ ] }, "require-dev": { - "pestphp/pest-dev-tools": "^3.4.0", - "pestphp/pest-plugin-type-coverage": "^3.6.1", - "symfony/process": "^7.3.0" + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-browser": "^4.2.1", + "pestphp/pest-plugin-type-coverage": "^4.0.3", + "psy/psysh": "^0.12.18" }, "minimum-stability": "dev", "prefer-stable": true, @@ -71,16 +74,17 @@ ], "scripts": { "refacto": "rector", - "lint": "pint", + "lint": "pint --parallel", "test:refacto": "rector --dry-run", - "test:lint": "pint --test", + "test:lint": "pint --parallel --test", + "test:profanity": "php bin/pest --profanity --compact", "test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100", - "test:unit": "php bin/pest --colors=always --exclude-group=integration --compact", - "test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml", - "test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3", - "test:integration": "php bin/pest --colors=always --group=integration -v", - "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots", + "test:unit": "php bin/pest --exclude-group=integration --compact", + "test:inline": "php bin/pest --configuration=phpunit.inline.xml", + "test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3", + "test:integration": "php bin/pest --group=integration -v", + "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots", "test": [ "@test:refacto", "@test:lint", @@ -111,6 +115,7 @@ "Pest\\Plugins\\Snapshot", "Pest\\Plugins\\Verbose", "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", "Pest\\Plugins\\Parallel" ] }, diff --git a/overrides/Event/Value/ThrowableBuilder.php b/overrides/Event/Value/ThrowableBuilder.php index 21d428e96..d446d03c5 100644 --- a/overrides/Event/Value/ThrowableBuilder.php +++ b/overrides/Event/Value/ThrowableBuilder.php @@ -52,6 +52,8 @@ use PHPUnit\Util\ThrowableToStringMapper; /** + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final readonly class ThrowableBuilder @@ -82,7 +84,7 @@ public static function from(\Throwable $t): Throwable $t->getMessage(), ThrowableToStringMapper::map($t), $trace, - $previous + $previous, ); } } diff --git a/overrides/Runner/Filter/NameFilterIterator.php b/overrides/Runner/Filter/NameFilterIterator.php index c637cc609..7834d9dbd 100644 --- a/overrides/Runner/Filter/NameFilterIterator.php +++ b/overrides/Runner/Filter/NameFilterIterator.php @@ -99,7 +99,9 @@ public function accept(): bool } if ($test instanceof HasPrintableTestCaseName) { - $name = $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName(); + $name = trim( + $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName().$test->dataSetAsString() + ); } else { $name = $test::class.'::'.$test->nameWithDataSet(); } diff --git a/overrides/Runner/ResultCache/DefaultResultCache.php b/overrides/Runner/ResultCache/DefaultResultCache.php index 3eb41a834..7e7f359c6 100644 --- a/overrides/Runner/ResultCache/DefaultResultCache.php +++ b/overrides/Runner/ResultCache/DefaultResultCache.php @@ -72,10 +72,7 @@ */ final class DefaultResultCache implements ResultCache { - /** - * @var string - */ - private const DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache'; + private const string DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache'; private readonly string $cacheFilename; diff --git a/overrides/TextUI/TestSuiteFilterProcessor.php b/overrides/TextUI/TestSuiteFilterProcessor.php index 536ab2081..e13d5c98a 100644 --- a/overrides/TextUI/TestSuiteFilterProcessor.php +++ b/overrides/TextUI/TestSuiteFilterProcessor.php @@ -45,6 +45,7 @@ namespace PHPUnit\TextUI; use Pest\Plugins\Only; +use Pest\Runner\Filter\EnsureTestCaseIsInitiatedFilter; use PHPUnit\Event; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\Filter\Factory; @@ -66,6 +67,12 @@ public function process(Configuration $configuration, TestSuite $suite): void { $factory = new Factory; + // @phpstan-ignore-next-line + (fn () => $this->filters[] = [ + 'className' => EnsureTestCaseIsInitiatedFilter::class, + 'argument' => '', + ])->call($factory); + if (! $configuration->hasFilter() && ! $configuration->hasGroups() && ! $configuration->hasExcludeGroups() && @@ -73,6 +80,8 @@ public function process(Configuration $configuration, TestSuite $suite): void ! $configuration->hasTestsCovering() && ! $configuration->hasTestsUsing() && ! Only::isEnabled()) { + $suite->injectFilter($factory); + return; } diff --git a/phpunit.xml b/phpunit.xml index 4aac1aa26..122f54e22 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -16,6 +16,7 @@ ./tests + ./tests-external ./tests/.snapshots ./tests/.tests ./tests/Fixtures/Inheritance diff --git a/rector.php b/rector.php index 191989dbb..caec31883 100644 --- a/rector.php +++ b/rector.php @@ -2,7 +2,10 @@ declare(strict_types=1); +use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector; use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector; +use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector; return RectorConfig::configure() @@ -12,6 +15,9 @@ ->withSkip([ __DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php', ReturnNeverTypeRector::class, + ArrowFunctionDelegatingCallToFirstClassCallableRector::class, + NarrowObjectReturnTypeRector::class, + RemoveParentDelegatingConstructorRector::class, ]) ->withPreparedSets( deadCode: true, diff --git a/resources/views/installers/plugin-browser.php b/resources/views/installers/plugin-browser.php new file mode 100644 index 000000000..d717867b2 --- /dev/null +++ b/resources/views/installers/plugin-browser.php @@ -0,0 +1,22 @@ +
+

+ Using the visit() function requires the Pest Plugin Browser to be installed. + + Run: +

+ +
+ - + composer require pestphp/pest-plugin-browser:^4.0 --dev +
+ +
+ - + npm install playwright@latest +
+ +
+ - + npx playwright install +
+
diff --git a/src/ArchPresets/Security.php b/src/ArchPresets/Security.php index c71427488..40b4f59d0 100644 --- a/src/ArchPresets/Security.php +++ b/src/ArchPresets/Security.php @@ -32,7 +32,6 @@ public function execute(): void 'create_function', 'unserialize', 'extract', - 'parse_str', 'mb_parse_str', 'dl', 'assert', diff --git a/src/Bootstrappers/BootExcludeList.php b/src/Bootstrappers/BootExcludeList.php index abd1552c6..69d9dce11 100644 --- a/src/Bootstrappers/BootExcludeList.php +++ b/src/Bootstrappers/BootExcludeList.php @@ -17,7 +17,7 @@ final class BootExcludeList implements Bootstrapper * * @var array */ - private const EXCLUDE_LIST = [ + private const array EXCLUDE_LIST = [ 'bin', 'overrides', 'resources', diff --git a/src/Bootstrappers/BootFiles.php b/src/Bootstrappers/BootFiles.php index 2017a7964..2f162eecb 100644 --- a/src/Bootstrappers/BootFiles.php +++ b/src/Bootstrappers/BootFiles.php @@ -5,6 +5,7 @@ namespace Pest\Bootstrappers; use Pest\Contracts\Bootstrapper; +use Pest\Exceptions\FatalException; use Pest\Support\DatasetInfo; use Pest\Support\Str; use Pest\TestSuite; @@ -24,7 +25,7 @@ final class BootFiles implements Bootstrapper * * @var array */ - private const STRUCTURE = [ + private const array STRUCTURE = [ 'Expectations', 'Expectations.php', 'Helpers', @@ -40,6 +41,10 @@ public function boot(): void $rootPath = TestSuite::getInstance()->rootPath; $testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory(); + if (! is_dir($testsPath)) { + throw new FatalException(sprintf('The test directory [%s] does not exist.', $testsPath)); + } + foreach (self::STRUCTURE as $filename) { $filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename); @@ -78,7 +83,7 @@ private function load(string $filename): void private function bootDatasets(string $testsPath): void { - assert(strlen($testsPath) > 0); + assert($testsPath !== ''); $files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php'); diff --git a/src/Bootstrappers/BootOverrides.php b/src/Bootstrappers/BootOverrides.php index efbcf7a35..28851f3a3 100644 --- a/src/Bootstrappers/BootOverrides.php +++ b/src/Bootstrappers/BootOverrides.php @@ -15,17 +15,17 @@ final class BootOverrides implements Bootstrapper /** * The list of files to be overridden. * - * @var array + * @var array */ - public const FILES = [ - '53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php', - '77ffb7647b583bd82e37962c6fbdc4b04d3344d8a2c1ed103e625ed1ff7cb5c2' => 'Runner/ResultCache/DefaultResultCache.php', - 'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php', - '3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', - '8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', - 'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php', - '8cfcb4999af79463eca51a42058e502ea4ddc776cba5677bf2f8eb6093e21a5c' => 'Event/Value/ThrowableBuilder.php', - '86cd9bcaa53cdd59c5b13e58f30064a015c549501e7629d93b96893d4dee1eb1' => 'Logging/JUnit/JunitXmlLogger.php', + public const array FILES = [ + 'Runner/Filter/NameFilterIterator.php', + 'Runner/ResultCache/DefaultResultCache.php', + 'Runner/TestSuiteLoader.php', + 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', + 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', + 'TextUI/TestSuiteFilterProcessor.php', + 'Event/Value/ThrowableBuilder.php', + 'Logging/JUnit/JunitXmlLogger.php', ]; /** diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php index 57f98e332..7877b2372 100644 --- a/src/Bootstrappers/BootSubscribers.php +++ b/src/Bootstrappers/BootSubscribers.php @@ -20,7 +20,7 @@ * * @var array> */ - private const SUBSCRIBERS = [ + private const array SUBSCRIBERS = [ Subscribers\EnsureConfigurationIsAvailable::class, Subscribers\EnsureIgnorableTestCasesAreIgnored::class, Subscribers\EnsureKernelDumpIsFlushed::class, diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index a2f7e40b7..1ff6626b9 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -8,6 +8,8 @@ /** * @internal + * + * @template T of object */ trait Extendable { @@ -20,6 +22,8 @@ trait Extendable /** * Register a new extend. + * + * @param-closure-this T $extend */ public function extend(string $name, Closure $extend): void { diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 37d3b175a..767a7c696 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -6,10 +6,12 @@ use Closure; use Pest\Exceptions\DatasetArgumentsMismatch; +use Pest\Panic; use Pest\Preset; use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; use Pest\Support\Reflection; +use Pest\Support\Shell; use Pest\TestSuite; use PHPUnit\Framework\Attributes\PostCondition; use PHPUnit\Framework\TestCase; @@ -101,27 +103,6 @@ trait Testable */ private array $__snapshotChanges = []; - /** - * Creates a new Test Case instance. - */ - public function __construct(string $name) - { - parent::__construct($name); - - $test = TestSuite::getInstance()->tests->get(self::$__filename); - - if ($test->hasMethod($name)) { - $method = $test->getMethod($name); - $this->__description = self::$__latestDescription = $method->description; - self::$__latestAssignees = $method->assignees; - self::$__latestNotes = $method->notes; - self::$__latestIssues = $method->issues; - self::$__latestPrs = $method->prs; - $this->__describing = $method->describing; - $this->__test = $method->getClosure(); - } - } - /** * Resets the test case static properties. */ @@ -214,7 +195,11 @@ public static function setUpBeforeClass(): void $beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll); } - call_user_func(Closure::bind($beforeAll, null, self::class)); + try { + call_user_func(Closure::bind($beforeAll, null, self::class)); + } catch (Throwable $e) { + Panic::with($e); + } } /** @@ -242,8 +227,6 @@ protected function setUp(...$arguments): void $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); - $method->setUp($this); - $description = $method->description; if ($this->dataName()) { $description = str_contains((string) $description, ':dataset') @@ -285,6 +268,33 @@ protected function setUp(...$arguments): void $this->__callClosure($beforeEach, $arguments); } + /** + * Initialize test case properties from TestSuite. + */ + public function __initializeTestCase(): void + { + // Return if the test case has already been initialized + if (isset($this->__test)) { + return; + } + + $name = $this->name(); + $test = TestSuite::getInstance()->tests->get(self::$__filename); + + if ($test->hasMethod($name)) { + $method = $test->getMethod($name); + $this->__description = self::$__latestDescription = $method->description; + self::$__latestAssignees = $method->assignees; + self::$__latestNotes = $method->notes; + self::$__latestIssues = $method->issues; + self::$__latestPrs = $method->prs; + $this->__describing = $method->describing; + $this->__test = $method->getClosure(); + + $method->setUp($this); + } + } + /** * Gets executed after the Test Case. */ @@ -434,15 +444,7 @@ protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void return; } - if (count($this->__snapshotChanges) === 1) { - $this->markTestIncomplete($this->__snapshotChanges[0]); - - return; - } - - $messages = implode(PHP_EOL, array_map(static fn (string $message): string => '- $message', $this->__snapshotChanges)); - - $this->markTestIncomplete($messages); + $this->markTestIncomplete(implode('. ', $this->__snapshotChanges)); } /** @@ -466,7 +468,7 @@ public function getPrintableTestCaseMethodName(): string */ public static function getLatestPrintableTestCaseMethodName(): string { - return self::$__latestDescription; + return self::$__latestDescription ?? ''; } /** @@ -481,4 +483,12 @@ public static function getPrintableContext(): array 'notes' => self::$__latestNotes, ]; } + + /** + * Opens a shell for the test case. + */ + public function shell(): void + { + Shell::open(); + } } diff --git a/src/Configuration.php b/src/Configuration.php index c504aa65e..4261f3ef3 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -4,6 +4,7 @@ namespace Pest; +use Pest\PendingCalls\BeforeEachCall; use Pest\PendingCalls\UsesCall; /** @@ -62,6 +63,14 @@ public function group(string ...$groups): UsesCall return (new UsesCall($this->filename, []))->group(...$groups); } + /** + * Marks all tests in the current file to be run exclusively. + */ + public function only(): void + { + (new BeforeEachCall(TestSuite::getInstance(), $this->filename))->only(); + } + /** * Depending on where is called, it will extend the given classes and traits globally or locally. */ @@ -102,6 +111,14 @@ public function project(): Configuration\Project return Configuration\Project::getInstance(); } + /** + * Gets the browser configuration. + */ + public function browser(): Browser\Configuration + { + return new Browser\Configuration; + } + /** * Proxies calls to the uses method. * diff --git a/src/Console/Help.php b/src/Console/Help.php index 3d09d5f55..50823d593 100644 --- a/src/Console/Help.php +++ b/src/Console/Help.php @@ -16,7 +16,7 @@ * * @var array */ - private const HELP_MESSAGES = [ + private const array HELP_MESSAGES = [ 'Pest Options:', ' --init Initialise a standard Pest configuration', ' --coverage Enable coverage and output to standard output', diff --git a/src/Console/Thanks.php b/src/Console/Thanks.php index 7e68f8711..8b056e481 100644 --- a/src/Console/Thanks.php +++ b/src/Console/Thanks.php @@ -22,11 +22,11 @@ * * @var array */ - private const FUNDING_MESSAGES = [ + private const array FUNDING_MESSAGES = [ 'Star' => 'https://github.com/pestphp/pest', 'YouTube' => 'https://youtube.com/@nunomaduro', - 'TikTok' => 'https://tiktok.com/@nunomaduro', - 'Twitch' => 'https://twitch.tv/enunomaduro', + 'TikTok' => 'https://tiktok.com/@enunomaduro', + 'Twitch' => 'https://twitch.tv/nunomaduro', 'LinkedIn' => 'https://linkedin.com/in/nunomaduro', 'Instagram' => 'https://instagram.com/enunomaduro', 'X' => 'https://x.com/enunomaduro', diff --git a/src/Exceptions/DatasetMissing.php b/src/Exceptions/DatasetMissing.php index dff803737..da738a448 100644 --- a/src/Exceptions/DatasetMissing.php +++ b/src/Exceptions/DatasetMissing.php @@ -22,7 +22,7 @@ final class DatasetMissing extends BadFunctionCallException implements Exception public function __construct(string $file, string $name, array $arguments) { parent::__construct(sprintf( - "A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", + 'A test with the description [%s] has [%d] argument(s) ([%s]) and no dataset(s) provided in [%s]', $name, count($arguments), implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)), diff --git a/src/Exceptions/TestCaseAlreadyInUse.php b/src/Exceptions/TestCaseAlreadyInUse.php index 7ebf62324..926c8ea6f 100644 --- a/src/Exceptions/TestCaseAlreadyInUse.php +++ b/src/Exceptions/TestCaseAlreadyInUse.php @@ -19,7 +19,11 @@ final class TestCaseAlreadyInUse extends InvalidArgumentException implements Exc */ public function __construct(string $inUse, string $newOne, string $folder) { - parent::__construct(sprintf('Test case `%s` can not be used. The folder `%s` already uses the test case `%s`', - $newOne, $folder, $inUse)); + parent::__construct(sprintf( + 'Test case [%s] can not be used. The folder [%s] already uses the test case [%s].', + $newOne, + $folder, + $inUse, + )); } } diff --git a/src/Exceptions/TestClosureMustNotBeStatic.php b/src/Exceptions/TestClosureMustNotBeStatic.php index b1f91e954..5dda67c0c 100644 --- a/src/Exceptions/TestClosureMustNotBeStatic.php +++ b/src/Exceptions/TestClosureMustNotBeStatic.php @@ -22,7 +22,7 @@ public function __construct(TestCaseMethodFactory $method) { parent::__construct( sprintf( - 'Test closure must not be static. Please remove the `static` keyword from the `%s` method in `%s`.', + 'Test closure must not be static. Please remove the [static] keyword from the [%s] method in [%s].', $method->description, $method->filename ) diff --git a/src/Expectation.php b/src/Expectation.php index 1bef5a8c4..50729d7a0 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -52,7 +52,9 @@ */ final class Expectation { + /** @use Extendable> */ use Extendable; + use Pipeable; use Retrievable; @@ -330,7 +332,7 @@ public function when(callable|bool $condition, callable $callback): self * @param array $parameters * @return Expectation|HigherOrderExpectation, TValue> */ - public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation + public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation|ArchExpectation { if (! self::hasMethod($method)) { if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $method)) { @@ -355,6 +357,10 @@ public function __call(string $method, array $parameters): Expectation|HigherOrd $reflectionClosure = new \ReflectionFunction($closure); $expectation = $reflectionClosure->getClosureThis(); + if ($reflectionClosure->getReturnType()?->__toString() === ArchExpectation::class) { + return $closure(...$parameters); + } + assert(is_object($expectation)); ExpectationPipeline::for($closure) @@ -393,7 +399,7 @@ private function getExpectationClosure(string $name): Closure * * @return Expectation|OppositeExpectation|EachExpectation|HigherOrderExpectation, TValue|null>|TValue */ - public function __get(string $name) + public function __get(string $name): mixed { if (! self::hasMethod($name)) { if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $name)) { @@ -890,6 +896,14 @@ public function toUseNothing(): ArchExpectation return ToUseNothing::make($this); } + /** + * Asserts that the source code of the given expectation target does not include suspicious characters. + */ + public function toHaveSuspiciousCharacters(): ArchExpectation + { + throw InvalidExpectation::fromMethods(['toHaveSuspiciousCharacters']); + } + /** * Not supported. */ diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index d5c3f083a..2713c7244 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -15,6 +15,7 @@ use Pest\Arch\SingleArchExpectation; use Pest\Arch\Support\FileLineFinder; use Pest\Exceptions\InvalidExpectation; +use Pest\Exceptions\MissingDependency; use Pest\Expectation; use Pest\Support\Arr; use Pest\Support\Exporter; @@ -24,6 +25,7 @@ use PHPUnit\Framework\ExpectationFailedException; use ReflectionMethod; use ReflectionProperty; +use Spoofchecker; use stdClass; /** @@ -278,6 +280,28 @@ public function toHaveMethod(array|string $method): ArchExpectation ); } + /** + * Asserts that the given expectation target does not have suspicious characters. + */ + public function toHaveSuspiciousCharacters(): ArchExpectation + { + if (! class_exists(Spoofchecker::class)) { + throw new MissingDependency(__FUNCTION__, 'ext-intl >= 2.0'); + } + + $checker = new Spoofchecker; + + /** @var Expectation|string> $original */ + $original = $this->original; + + return Targeted::make( + $original, + fn (ObjectDescription $object): bool => ! $checker->isSuspicious((string) file_get_contents($object->path)), + 'to not include suspicious characters', + FileLineFinder::where(fn (string $line): bool => $checker->isSuspicious($line)), + ); + } + /** * Asserts that the given expectation target does not have the given methods. * diff --git a/src/Factories/Covers/CoversNothing.php b/src/Factories/Covers/CoversNothing.php deleted file mode 100644 index 4215edbc1..000000000 --- a/src/Factories/Covers/CoversNothing.php +++ /dev/null @@ -1,10 +0,0 @@ - + * @var array */ public array $describing = []; diff --git a/src/Functions.php b/src/Functions.php index 1e12fe7e6..0ed631f36 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -2,11 +2,14 @@ declare(strict_types=1); +use Pest\Browser\Api\ArrayablePendingAwaitablePage; +use Pest\Browser\Api\PendingAwaitablePage; use Pest\Concerns\Expectable; use Pest\Configuration; use Pest\Exceptions\AfterAllWithinDescribe; use Pest\Exceptions\BeforeAllWithinDescribe; use Pest\Expectation; +use Pest\Installers\PluginBrowser; use Pest\Mutate\Contracts\MutationTestRunner; use Pest\Mutate\Repositories\ConfigurationRepository; use Pest\PendingCalls\AfterEachCall; @@ -18,6 +21,7 @@ use Pest\Support\Backtrace; use Pest\Support\Container; use Pest\Support\DatasetInfo; +use Pest\Support\Description; use Pest\Support\HigherOrderTapProxy; use Pest\TestSuite; use PHPUnit\Framework\TestCase; @@ -95,7 +99,7 @@ function describe(string $description, Closure $tests): DescribeCall { $filename = Backtrace::testFile(); - return new DescribeCall(TestSuite::getInstance(), $filename, $description, $tests); + return new DescribeCall(TestSuite::getInstance(), $filename, new Description($description), $tests); } } @@ -278,3 +282,51 @@ function mutates(array|string ...$targets): void } } } + +if (! function_exists('fixture')) { + /** + * Returns the absolute path to a fixture file. + */ + function fixture(string $file): string + { + $file = implode(DIRECTORY_SEPARATOR, [ + TestSuite::getInstance()->rootPath, + TestSuite::getInstance()->testPath, + 'Fixtures', + str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file), + ]); + + $fileRealPath = realpath($file); + + if ($fileRealPath === false) { + throw new InvalidArgumentException( + 'The fixture file ['.$file.'] does not exist.', + ); + } + + return $fileRealPath; + } +} + +if (! function_exists('visit')) { + /** + * Browse to the given URL. + * + * @template TUrl of array|string + * + * @param TUrl $url + * @param array $options + * @return (TUrl is array ? ArrayablePendingAwaitablePage : PendingAwaitablePage) + */ + function visit(array|string $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage + { + if (! class_exists(\Pest\Browser\Configuration::class)) { + PluginBrowser::install(); + + exit(0); + } + + // @phpstan-ignore-next-line + return test()->visit($url, $options); + } +} diff --git a/src/Installers/PluginBrowser.php b/src/Installers/PluginBrowser.php new file mode 100644 index 000000000..2d36ed3da --- /dev/null +++ b/src/Installers/PluginBrowser.php @@ -0,0 +1,15 @@ + */ - private const BOOTSTRAPPERS = [ + private const array BOOTSTRAPPERS = [ Bootstrappers\BootOverrides::class, Bootstrappers\BootSubscribers::class, Bootstrappers\BootFiles::class, @@ -71,7 +71,7 @@ public static function boot(TestSuite $testSuite, InputInterface $input, OutputI $output, ); - register_shutdown_function(fn () => $kernel->shutdown()); + register_shutdown_function($kernel->shutdown(...)); foreach (self::BOOTSTRAPPERS as $bootstrapper) { $bootstrapper = Container::getInstance()->get($bootstrapper); diff --git a/src/Logging/Converter.php b/src/Logging/Converter.php index b4560e227..50a42d1ac 100644 --- a/src/Logging/Converter.php +++ b/src/Logging/Converter.php @@ -31,7 +31,7 @@ /** * The prefix for the test suite name. */ - private const PREFIX = 'P\\'; + private const string PREFIX = 'P\\'; /** * The state generator. @@ -131,7 +131,7 @@ public function getStackTrace(Throwable $throwable): string // clean the paths of each frame. $frames = array_map( - fn (string $frame): string => $this->toRelativePath($frame), + $this->toRelativePath(...), $frames ); diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index 1566b59c9..09974787c 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -781,15 +781,13 @@ public function toMatchArray(iterable $array, string $message = ''): self foreach ($array as $key => $value) { Assert::assertArrayHasKey($key, $valueAsArray, $message); - if ($message === '') { - $message = sprintf( - 'Failed asserting that an array has a key %s with the value %s.', - $this->export($key), - $this->export($valueAsArray[$key]), - ); - } + $assertMessage = $message !== '' ? $message : sprintf( + 'Failed asserting that an array has a key %s with the value %s.', + $this->export($key), + $this->export($valueAsArray[$key]), + ); - Assert::assertEquals($value, $valueAsArray[$key], $message); + Assert::assertEquals($value, $valueAsArray[$key], $assertMessage); } return $this; @@ -802,7 +800,7 @@ public function toMatchArray(iterable $array, string $message = ''): self * @param iterable $object * @return self */ - public function toMatchObject(iterable $object, string $message = ''): self + public function toMatchObject(object|iterable $object, string $message = ''): self { foreach ((array) $object as $property => $value) { if (! is_object($this->value) && ! is_string($this->value)) { @@ -814,15 +812,13 @@ public function toMatchObject(iterable $object, string $message = ''): self /* @phpstan-ignore-next-line */ $propertyValue = $this->value->{$property}; - if ($message === '') { - $message = sprintf( - 'Failed asserting that an object has a property %s with the value %s.', - $this->export($property), - $this->export($propertyValue), - ); - } + $assertMessage = $message !== '' ? $message : sprintf( + 'Failed asserting that an object has a property %s with the value %s.', + $this->export($property), + $this->export($propertyValue), + ); - Assert::assertEquals($value, $propertyValue, $message); + Assert::assertEquals($value, $propertyValue, $assertMessage); } return $this; @@ -1158,4 +1154,21 @@ public function toBeUrl(string $message = ''): self return $this; } + + /** + * Asserts that the value can be converted to a slug + * + * @return self + */ + public function toBeSlug(string $message = ''): self + { + if ($message === '') { + $message = "Failed asserting that {$this->value} can be converted to a slug."; + } + + $slug = Str::slugify((string) $this->value); + Assert::assertNotEmpty($slug, $message); + + return $this; + } } diff --git a/src/PendingCalls/Concerns/Describable.php b/src/PendingCalls/Concerns/Describable.php index 06a7eab71..0208ea4b5 100644 --- a/src/PendingCalls/Concerns/Describable.php +++ b/src/PendingCalls/Concerns/Describable.php @@ -12,14 +12,14 @@ trait Describable /** * Note: this is property is not used; however, it gets added automatically by rector php. * - * @var array + * @var array */ public array $__describing; /** * The describing of the test case. * - * @var array + * @var array */ public array $describing = []; } diff --git a/src/PendingCalls/DescribeCall.php b/src/PendingCalls/DescribeCall.php index de4729603..08ebc15e0 100644 --- a/src/PendingCalls/DescribeCall.php +++ b/src/PendingCalls/DescribeCall.php @@ -6,6 +6,7 @@ use Closure; use Pest\Support\Backtrace; +use Pest\Support\Description; use Pest\TestSuite; /** @@ -16,7 +17,7 @@ final class DescribeCall /** * The current describe call. * - * @var array + * @var array */ private static array $describing = []; @@ -31,7 +32,7 @@ final class DescribeCall public function __construct( public readonly TestSuite $testSuite, public readonly string $filename, - public readonly string $description, + public readonly Description $description, public readonly Closure $tests ) { // @@ -40,7 +41,7 @@ public function __construct( /** * What is the current describing. * - * @return array + * @return array */ public static function describing(): array { diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 50fef3b68..a65c8bb58 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -12,6 +12,7 @@ use Pest\Factories\TestCaseMethodFactory; use Pest\Mutate\Repositories\ConfigurationRepository; use Pest\PendingCalls\Concerns\Describable; +use Pest\Plugins\Environment; use Pest\Plugins\Only; use Pest\Support\Backtrace; use Pest\Support\Container; @@ -178,10 +179,9 @@ public function throwsUnless(callable|bool $condition, string|int $exception, ?s } /** - * Runs the current test multiple times with - * each item of the given `iterable`. + * Runs the current test multiple times with each item of the given `iterable`. * - * @param array<\Closure|iterable|string> $data + * @param Closure|iterable|string $data */ public function with(Closure|iterable|string ...$data): self { @@ -315,6 +315,61 @@ private function skipOnOs(string $osFamily, string $message): self : $this; } + /** + * Weather the current test is running on a CI environment. + */ + private function runningOnCI(): bool + { + foreach ([ + 'CI', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'CIRCLECI', + 'TRAVIS', + 'APPVEYOR', + 'BITBUCKET_BUILD_NUMBER', + 'BUILDKITE', + 'TEAMCITY_VERSION', + 'JENKINS_URL', + 'SYSTEM_COLLECTIONURI', + 'CI_NAME', + 'TASKCLUSTER_ROOT_URL', + 'DRONE', + 'WERCKER', + 'NEVERCODE', + 'SEMAPHORE', + 'NETLIFY', + 'NOW_BUILDER', + ] as $env) { + if (getenv($env) !== false) { + return true; + } + } + + return Environment::name() === Environment::CI; + } + + /** + * Skips the current test when running on a CI environments. + */ + public function skipOnCI(): self + { + if ($this->runningOnCI()) { + return $this->skip('This test is skipped on [CI].'); + } + + return $this; + } + + public function skipLocally(): self + { + if ($this->runningOnCI() === false) { + return $this->skip('This test is skipped [locally].'); + } + + return $this; + } + /** * Skips the current test unless the given test is running on Windows. */ @@ -604,18 +659,29 @@ public function coversFunction(string ...$functions): self } /** - * Sets that the current test covers nothing. + * Adds one or more references to the tested method or class. This helps + * to link test cases to the source code for easier navigation. + * + * @param array|class-string ...$classes */ - public function coversNothing(): self + public function references(string|array ...$classes): self { - $this->testCaseMethod->attributes[] = new Attribute( - \PHPUnit\Framework\Attributes\CoversNothing::class, - [], - ); + assert($classes !== []); return $this; } + /** + * Adds one or more references to the tested method or class. This helps + * to link test cases to the source code for easier navigation. + * + * @param array|class-string ...$classes + */ + public function see(string|array ...$classes): self + { + return $this->references(...$classes); + } + /** * Informs the test runner that no expectations happen in this test, * and its purpose is simply to check whether the given code can @@ -693,7 +759,12 @@ public function __destruct() $this->testSuite->tests->set($this->testCaseMethod); if (! is_null($testCase = $this->testSuite->tests->get($this->filename))) { - $testCase->attributes = array_merge($testCase->attributes, $this->testCaseFactoryAttributes); + $attributesToMerge = array_filter( + $this->testCaseFactoryAttributes, + fn (Attribute $attributeToMerge): bool => array_filter($testCase->attributes, fn (Attribute $attribute): bool => serialize($attributeToMerge) === serialize($attribute)) === [] + ); + + $testCase->attributes = array_merge($testCase->attributes, $attributesToMerge); } } } diff --git a/src/Pest.php b/src/Pest.php index 0199acc72..ba3a540e4 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ function version(): string { - return '3.8.4'; + return '4.3.2'; } function testDirectory(string $file = ''): string diff --git a/src/Plugins/Cache.php b/src/Plugins/Cache.php index ea3abb78f..3ae0433b8 100644 --- a/src/Plugins/Cache.php +++ b/src/Plugins/Cache.php @@ -21,7 +21,7 @@ final class Cache implements HandlesArguments /** * The temporary folder. */ - private const TEMPORARY_FOLDER = __DIR__ + private const string TEMPORARY_FOLDER = __DIR__ .DIRECTORY_SEPARATOR .'..' .DIRECTORY_SEPARATOR diff --git a/src/Plugins/Configuration.php b/src/Plugins/Configuration.php index acae8fb41..54c9627ff 100644 --- a/src/Plugins/Configuration.php +++ b/src/Plugins/Configuration.php @@ -21,7 +21,7 @@ final class Configuration implements HandlesArguments, Terminable /** * The base PHPUnit file. */ - public const BASE_PHPUNIT_FILE = __DIR__ + public const string BASE_PHPUNIT_FILE = __DIR__ .DIRECTORY_SEPARATOR .'..' .DIRECTORY_SEPARATOR @@ -34,7 +34,7 @@ final class Configuration implements HandlesArguments, Terminable */ public function handleArguments(array $arguments): array { - if ($this->hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) { + if ($this->hasArgument('--configuration', $arguments) || $this->hasArgument('-c', $arguments) || $this->hasCustomConfigurationFile()) { return $arguments; } diff --git a/src/Plugins/Coverage.php b/src/Plugins/Coverage.php index a5061d258..712f5de55 100644 --- a/src/Plugins/Coverage.php +++ b/src/Plugins/Coverage.php @@ -17,20 +17,11 @@ */ final class Coverage implements AddsOutput, HandlesArguments { - /** - * @var string - */ - private const COVERAGE_OPTION = 'coverage'; + private const string COVERAGE_OPTION = 'coverage'; - /** - * @var string - */ - private const MIN_OPTION = 'min'; + private const string MIN_OPTION = 'min'; - /** - * @var string - */ - private const EXACTLY_OPTION = 'exactly'; + private const string EXACTLY_OPTION = 'exactly'; /** * Whether it should show the coverage or not. diff --git a/src/Plugins/Environment.php b/src/Plugins/Environment.php index 8ff10b4c1..7edbbbd3d 100644 --- a/src/Plugins/Environment.php +++ b/src/Plugins/Environment.php @@ -14,12 +14,12 @@ final class Environment implements HandlesArguments /** * The continuous integration environment. */ - public const CI = 'ci'; + public const string CI = 'ci'; /** * The local environment. */ - public const LOCAL = 'local'; + public const string LOCAL = 'local'; /** * The current environment. diff --git a/src/Plugins/Help.php b/src/Plugins/Help.php index 89a47b66e..096f2914e 100644 --- a/src/Plugins/Help.php +++ b/src/Plugins/Help.php @@ -99,6 +99,7 @@ private function getContent(): array { $helpReflection = new PHPUnitHelp; + // @phpstan-ignore-next-line $content = (fn (): array => $this->elements())->call($helpReflection); $content['Configuration'] = [...[[ @@ -141,6 +142,9 @@ private function getContent(): array ], [ 'arg' => '--retry', 'desc' => 'Run non-passing tests first and stop execution upon first error or failure', + ], [ + 'arg' => '--dirty', + 'desc' => 'Only run tests that have uncommitted changes according to Git', ], ...$content['Selection']]; $content['Reporting'] = [...$content['Reporting'], ...[ diff --git a/src/Plugins/Init.php b/src/Plugins/Init.php index eb87b0869..c31dd7599 100644 --- a/src/Plugins/Init.php +++ b/src/Plugins/Init.php @@ -20,12 +20,12 @@ /** * The option the triggers the init job. */ - private const INIT_OPTION = '--init'; + private const string INIT_OPTION = '--init'; /** * The files that will be created. */ - private const STUBS = [ + private const array STUBS = [ 'phpunit.xml.stub' => 'phpunit.xml', 'Pest.php.stub' => 'tests/Pest.php', 'TestCase.php.stub' => 'tests/TestCase.php', diff --git a/src/Plugins/Only.php b/src/Plugins/Only.php index 0d9581736..fd1001de0 100644 --- a/src/Plugins/Only.php +++ b/src/Plugins/Only.php @@ -5,7 +5,10 @@ namespace Pest\Plugins; use Pest\Contracts\Plugins\Terminable; +use Pest\Factories\Attribute; +use Pest\Factories\TestCaseMethodFactory; use Pest\PendingCalls\TestCall; +use PHPUnit\Framework\Attributes\Group; /** * @internal @@ -15,7 +18,7 @@ final class Only implements Terminable /** * The temporary folder. */ - private const TEMPORARY_FOLDER = __DIR__ + private const string TEMPORARY_FOLDER = __DIR__ .DIRECTORY_SEPARATOR .'..' .DIRECTORY_SEPARATOR @@ -23,28 +26,19 @@ final class Only implements Terminable .DIRECTORY_SEPARATOR .'.temp'; - /** - * {@inheritDoc} - */ - public function terminate(): void - { - if (Parallel::isWorker()) { - return; - } - - $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; - - if (file_exists($lockFile)) { - unlink($lockFile); - } - } - /** * Creates the lock file. */ - public static function enable(TestCall $testCall, string $group = '__pest_only'): void + public static function enable(TestCall|TestCaseMethodFactory $testCall, string $group = '__pest_only'): void { - $testCall->group($group); + if ($testCall instanceof TestCall) { + $testCall->group($group); + } else { + $testCall->attributes[] = new Attribute( + Group::class, + [$group], + ); + } if (Environment::name() === Environment::CI || Parallel::isWorker()) { return; @@ -88,4 +82,20 @@ public static function group(): string return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line } + + /** + * {@inheritDoc} + */ + public function terminate(): void + { + if (Parallel::isWorker()) { + return; + } + + $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; + + if (file_exists($lockFile)) { + unlink($lockFile); + } + } } diff --git a/src/Plugins/Parallel.php b/src/Plugins/Parallel.php index 1632a0504..949028236 100644 --- a/src/Plugins/Parallel.php +++ b/src/Plugins/Parallel.php @@ -23,9 +23,9 @@ final class Parallel implements HandlesArguments { use HandleArguments; - private const GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_'; + private const string GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_'; - private const HANDLERS = [ + private const array HANDLERS = [ Parallel\Handlers\Parallel::class, Parallel\Handlers\Pest::class, Parallel\Handlers\Laravel::class, @@ -34,7 +34,7 @@ final class Parallel implements HandlesArguments /** * @var string[] */ - private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request']; + private const array UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request']; /** * Whether the given command line arguments indicate that the test suite should be run in parallel. diff --git a/src/Plugins/Parallel/Handlers/Parallel.php b/src/Plugins/Parallel/Handlers/Parallel.php index 76a59af6c..d99139b2b 100644 --- a/src/Plugins/Parallel/Handlers/Parallel.php +++ b/src/Plugins/Parallel/Handlers/Parallel.php @@ -18,7 +18,7 @@ final class Parallel implements HandlesArguments /** * The list of arguments to remove. */ - private const ARGS_TO_REMOVE = [ + private const array ARGS_TO_REMOVE = [ '--parallel', '-p', '--no-output', diff --git a/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php b/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php index d2801ced5..cf5272b15 100644 --- a/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php +++ b/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php @@ -11,6 +11,7 @@ final class CleanConsoleOutput extends ConsoleOutput /** * {@inheritdoc} */ + #[\Override] protected function doWrite(string $message, bool $newline): void // @pest-arch-ignore-line { if ($this->isOpeningHeadline($message)) { diff --git a/src/Plugins/Parallel/Paratest/ResultPrinter.php b/src/Plugins/Parallel/Paratest/ResultPrinter.php index bd416e1e7..e7a1c24de 100644 --- a/src/Plugins/Parallel/Paratest/ResultPrinter.php +++ b/src/Plugins/Parallel/Paratest/ResultPrinter.php @@ -59,10 +59,10 @@ public function __construct( private readonly OutputInterface $output, private readonly Options $options ) { - $this->printer = new class($this->output) implements Printer + $this->printer = new readonly class($this->output) implements Printer { public function __construct( - private readonly OutputInterface $output, + private OutputInterface $output, ) {} public function print(string $buffer): void diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 282749d32..469f2aa6f 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -17,6 +17,7 @@ use Pest\Result; use Pest\TestSuite; use PHPUnit\Event\Facade as EventFacade; +use PHPUnit\Event\Test\AfterLastTestMethodFailed; use PHPUnit\Event\TestRunner\WarningTriggered; use PHPUnit\Runner\CodeCoverage; use PHPUnit\Runner\ResultCache\DefaultResultCache; @@ -50,7 +51,7 @@ final class WrapperRunner implements RunnerInterface /** * The time to sleep between cycles. */ - private const CYCLE_SLEEP = 10000; + private const int CYCLE_SLEEP = 10000; /** * The result printer. @@ -313,27 +314,42 @@ private function complete(TestResult $testResultSum): int $testResult = unserialize($contents); assert($testResult instanceof TestResult); + /** @var list $failedEvents */ + $failedEvents = array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()); + $testResultSum = new TestResult( (int) $testResultSum->hasTests() + (int) $testResult->hasTests(), $testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(), $testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(), array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()), - array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()), + $failedEvents, array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()), array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()), array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()), array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()), + array_merge_recursive($testResultSum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()), + // @phpstan-ignore-next-line + array_merge_recursive($testResultSum->testRunnerTriggeredNoticeEvents(), $testResult->testRunnerTriggeredNoticeEvents()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->errors(), $testResult->errors()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->notices(), $testResult->notices()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->warnings(), $testResult->warnings()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()), $testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(), ); @@ -351,8 +367,10 @@ private function complete(TestResult $testResultSum): int $testResultSum->testMarkedIncompleteEvents(), $testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResultSum->testTriggeredPhpunitErrorEvents(), + $testResultSum->testTriggeredPhpunitNoticeEvents(), $testResultSum->testTriggeredPhpunitWarningEvents(), $testResultSum->testRunnerTriggeredDeprecationEvents(), + $testResultSum->testRunnerTriggeredNoticeEvents(), array_values(array_filter( $testResultSum->testRunnerTriggeredWarningEvents(), fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found') diff --git a/src/Plugins/Parallel/Support/CompactPrinter.php b/src/Plugins/Parallel/Support/CompactPrinter.php index 25226b10e..aa2da210c 100644 --- a/src/Plugins/Parallel/Support/CompactPrinter.php +++ b/src/Plugins/Parallel/Support/CompactPrinter.php @@ -34,7 +34,7 @@ final class CompactPrinter /** * @var array> */ - private const LOOKUP_TABLE = [ + private const array LOOKUP_TABLE = [ '.' => ['gray', '.'], 'S' => ['yellow', 's'], 'T' => ['cyan', 't'], @@ -131,14 +131,14 @@ public function recap(State $state, PHPUnitTestResult $testResult, Duration $dur $status['collected'], $status['threshold'], $status['roots'], - null, - null, - null, - null, - null, - null, - null, - null, + 0.00, + 0.00, + 0.00, + 0.00, + false, + false, + false, + 0, ); $telemetry = new Info( diff --git a/src/Plugins/Shard.php b/src/Plugins/Shard.php new file mode 100644 index 000000000..f48260bb5 --- /dev/null +++ b/src/Plugins/Shard.php @@ -0,0 +1,177 @@ +hasArgument('--shard', $arguments)) { + return $arguments; + } + + // @phpstan-ignore-next-line + $input = new ArgvInput($arguments); + + ['index' => $index, 'total' => $total] = self::getShard($input); + + $arguments = $this->popArgument("--shard=$index/$total", $this->popArgument('--shard', $this->popArgument( + "$index/$total", + $arguments, + ))); + + /** @phpstan-ignore-next-line */ + $tests = $this->allTests($arguments); + $testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? []; + + self::$shard = [ + 'index' => $index, + 'total' => $total, + 'testsRan' => count($testsToRun), + 'testsCount' => count($tests), + ]; + + return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)]; + } + + /** + * Returns all tests that the test suite would run. + * + * @param list $arguments + * @return list + */ + private function allTests(array $arguments): array + { + $output = (new Process([ + 'php', + ...$this->removeParallelArguments($arguments), + '--list-tests', + ]))->mustRun()->getOutput(); + + preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches); + + return array_values(array_unique($matches[1])); + } + + /** + * @param array $arguments + * @return array + */ + private function removeParallelArguments(array $arguments): array + { + return array_filter($arguments, fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true)); + } + + /** + * Builds the filter argument for the given tests to run. + */ + private function buildFilterArgument(mixed $testsToRun): string + { + return addslashes(implode('|', $testsToRun)); + } + + /** + * Adds output after the Test Suite execution. + */ + public function addOutput(int $exitCode): int + { + if (self::$shard === null) { + return $exitCode; + } + + [ + 'index' => $index, + 'total' => $total, + 'testsRan' => $testsRan, + 'testsCount' => $testsCount, + ] = self::$shard; + + $this->output->writeln(sprintf( + ' Shard: %d of %d — %d file%s ran, out of %d.', + $index, + $total, + $testsRan, + $testsRan === 1 ? '' : 's', + $testsCount, + )); + + return $exitCode; + } + + /** + * Returns the shard information. + * + * @return array{index: int, total: int} + */ + public static function getShard(InputInterface $input): array + { + if ($input->hasParameterOption('--'.self::SHARD_OPTION)) { + $shard = $input->getParameterOption('--'.self::SHARD_OPTION); + } else { + $shard = null; + } + + if (! is_string($shard) || ! preg_match('/^\d+\/\d+$/', $shard)) { + throw new InvalidOption('The [--shard] option must be in the format "index/total".'); + } + + [$index, $total] = explode('/', $shard); + + if (! is_numeric($index) || ! is_numeric($total)) { + throw new InvalidOption('The [--shard] option must be in the format "index/total".'); + } + + if ($index <= 0 || $total <= 0 || $index > $total) { + throw new InvalidOption('The [--shard] option index must be a non-negative integer less than the total number of shards.'); + } + + $index = (int) $index; + $total = (int) $total; + + return [ + 'index' => $index, + 'total' => $total, + ]; + } +} diff --git a/src/Plugins/Verbose.php b/src/Plugins/Verbose.php index e37938a3a..9cec77deb 100644 --- a/src/Plugins/Verbose.php +++ b/src/Plugins/Verbose.php @@ -16,7 +16,7 @@ final class Verbose implements HandlesArguments /** * The list of verbosity levels. */ - private const VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q']; + private const array VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q']; /** * {@inheritDoc} diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index 7d318a2dd..0c6b13a8d 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -19,7 +19,7 @@ */ final class DatasetsRepository { - private const SEPARATOR = '>>'; + private const string SEPARATOR = '>>'; /** * Holds the datasets. @@ -67,11 +67,11 @@ public static function has(string $filename, string $description): bool } /** - * @return Closure|array + * @return array * * @throws ShouldNotHappen */ - public static function get(string $filename, string $description): Closure|array // @phpstan-ignore-line + public static function get(string $filename, string $description): array // @phpstan-ignore-line { $dataset = self::$withs[$filename.self::SEPARATOR.$description]; @@ -191,6 +191,7 @@ private static function getScopedDataset(string $name, string $currentTestFile): return str_starts_with($currentTestFile, $datasetScope); }, ARRAY_FILTER_USE_KEY); + /** @var string|null $closestScopeDatasetKey */ $closestScopeDatasetKey = array_reduce( array_keys($matchingDatasets), fn (string|int|null $keyA, string|int|null $keyB): string|int|null => $keyA !== null && strlen((string) $keyA) > strlen((string) $keyB) ? $keyA : $keyB diff --git a/src/Repositories/SnapshotRepository.php b/src/Repositories/SnapshotRepository.php index 7f7a95738..c719f219f 100644 --- a/src/Repositories/SnapshotRepository.php +++ b/src/Repositories/SnapshotRepository.php @@ -19,6 +19,7 @@ final class SnapshotRepository * Creates a snapshot repository instance. */ public function __construct( + private readonly string $rootPath, private readonly string $testsPath, private readonly string $snapshotsPath, ) {} @@ -103,7 +104,19 @@ public function flush(): void */ private function getSnapshotFilename(): string { - $relativePath = str_replace($this->testsPath, '', TestSuite::getInstance()->getFilename()); + $testFile = TestSuite::getInstance()->getFilename(); + + if (str_starts_with($testFile, $this->testsPath)) { + // if the test file is in the tests directory + $startPath = $this->testsPath; + } else { + // if the test file is in the app, src, etc. directory + $startPath = $this->rootPath; + } + + // relative path: we use substr() and not str_replace() to remove the start path + // for instance, if the $startPath is /app/ and the $testFile is /app/app/tests/Unit/ExampleTest.php, we should only remove the first /app/ from the path + $relativePath = substr($testFile, strlen($startPath)); // remove extension from filename $relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.')); diff --git a/src/Result.php b/src/Result.php index 233ffa2dd..97eda17f4 100644 --- a/src/Result.php +++ b/src/Result.php @@ -4,20 +4,16 @@ namespace Pest; -use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection; use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TextUI\Configuration\Configuration; +use PHPUnit\TextUI\ShellExitCodeCalculator; /** * @internal */ final class Result { - private const SUCCESS_EXIT = 0; - - private const FAILURE_EXIT = 1; - - private const EXCEPTION_EXIT = 2; + private const int SUCCESS_EXIT = 0; /** * If the exit code is different from 0. @@ -40,44 +36,8 @@ public static function ok(Configuration $configuration, TestResult $result): boo */ public static function exitCode(Configuration $configuration, TestResult $result): int { - if ($result->wasSuccessful()) { - if ($configuration->failOnWarning()) { - $warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents() - + count($result->warnings()) - + count($result->phpWarnings()); - - if ($warnings > 0) { - return self::FAILURE_EXIT; - } - } - - if (! $result->hasTestTriggeredPhpunitWarningEvents()) { - return self::SUCCESS_EXIT; - } - } - - if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) { - return self::FAILURE_EXIT; - } - - if ($result->wasSuccessful()) { - if ($configuration->failOnRisky() && $result->hasTestConsideredRiskyEvents()) { - $returnCode = self::FAILURE_EXIT; - } - - if ($configuration->failOnIncomplete() && $result->hasTestMarkedIncompleteEvents()) { - $returnCode = self::FAILURE_EXIT; - } - - if ($configuration->failOnSkipped() && $result->hasTestSkippedEvents()) { - $returnCode = self::FAILURE_EXIT; - } - } - - if ($result->hasTestErroredEvents()) { - return self::EXCEPTION_EXIT; - } + $shell = new ShellExitCodeCalculator; - return self::FAILURE_EXIT; + return $shell->calculate($configuration, $result); } } diff --git a/src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php b/src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php new file mode 100644 index 000000000..614b38a4a --- /dev/null +++ b/src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php @@ -0,0 +1,39 @@ + $iterator + */ + public function __construct(RecursiveIterator $iterator) + { + parent::__construct($iterator); + } + + /** + * {@inheritdoc} + */ + public function accept(): bool + { + $test = $this->getInnerIterator()->current(); + + if ($test instanceof HasPrintableTestCaseName) { + /** @phpstan-ignore-next-line */ + $test->__initializeTestCase(); + } + + return true; + } +} diff --git a/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php b/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php index f571c2f3d..45a7823d8 100644 --- a/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php +++ b/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php @@ -33,7 +33,7 @@ public function notify(Started $event): void /** @var array $testRunnerTriggeredWarningEvents */ $testRunnerTriggeredWarningEvents = $property->getValue($collector); - $testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => $event->message() !== 'No tests found in class "Pest\TestCases\IgnorableTestCase".')); + $testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => str_contains($event->message(), 'No tests found in class') === false)); $property->setValue($collector, $testRunnerTriggeredWarningEvents); } diff --git a/src/Support/Backtrace.php b/src/Support/Backtrace.php index 030019760..652eb4425 100644 --- a/src/Support/Backtrace.php +++ b/src/Support/Backtrace.php @@ -11,12 +11,9 @@ */ final class Backtrace { - /** - * @var string - */ - private const FILE = 'file'; + private const string FILE = 'file'; - private const BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS; + private const int BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS; /** * Returns the current test file. diff --git a/src/Support/DatasetInfo.php b/src/Support/DatasetInfo.php index 46f39c416..c67f317c2 100644 --- a/src/Support/DatasetInfo.php +++ b/src/Support/DatasetInfo.php @@ -11,9 +11,9 @@ */ final class DatasetInfo { - public const DATASETS_DIR_NAME = 'Datasets'; + public const string DATASETS_DIR_NAME = 'Datasets'; - public const DATASETS_FILE_NAME = 'Datasets.php'; + public const string DATASETS_FILE_NAME = 'Datasets.php'; public static function isInsideADatasetsDirectory(string $file): bool { diff --git a/src/Support/Description.php b/src/Support/Description.php new file mode 100644 index 000000000..d7d775f22 --- /dev/null +++ b/src/Support/Description.php @@ -0,0 +1,21 @@ +description; + } +} diff --git a/src/Support/ExceptionTrace.php b/src/Support/ExceptionTrace.php index 9af6aa5bc..9d4132e27 100644 --- a/src/Support/ExceptionTrace.php +++ b/src/Support/ExceptionTrace.php @@ -13,7 +13,7 @@ */ final class ExceptionTrace { - private const UNDEFINED_METHOD = 'Call to undefined method P\\'; + private const string UNDEFINED_METHOD = 'Call to undefined method P\\'; /** * Ensures the given closure reports the good execution context. diff --git a/src/Support/Exporter.php b/src/Support/Exporter.php index 169f4891b..44367c08e 100644 --- a/src/Support/Exporter.php +++ b/src/Support/Exporter.php @@ -15,7 +15,7 @@ /** * The maximum number of items in an array to export. */ - private const MAX_ARRAY_ITEMS = 3; + private const int MAX_ARRAY_ITEMS = 3; /** * Creates a new Exporter instance. diff --git a/src/Support/HigherOrderCallables.php b/src/Support/HigherOrderCallables.php index 9e5bba36f..358b4da5f 100644 --- a/src/Support/HigherOrderCallables.php +++ b/src/Support/HigherOrderCallables.php @@ -46,6 +46,7 @@ public function expect(mixed $value): Expectation */ public function and(mixed $value): Expectation { + // @phpstan-ignore-next-line return $this->expect($value); } diff --git a/src/Support/HigherOrderMessage.php b/src/Support/HigherOrderMessage.php index 89c3e1f10..ce9482445 100644 --- a/src/Support/HigherOrderMessage.php +++ b/src/Support/HigherOrderMessage.php @@ -13,7 +13,7 @@ */ final class HigherOrderMessage { - public const UNDEFINED_METHOD = 'Method %s does not exist'; + public const string UNDEFINED_METHOD = 'Method %s does not exist'; /** * An optional condition that will determine if the message will be executed. diff --git a/src/Support/HigherOrderTapProxy.php b/src/Support/HigherOrderTapProxy.php index 08eb5ea77..60e65bac9 100644 --- a/src/Support/HigherOrderTapProxy.php +++ b/src/Support/HigherOrderTapProxy.php @@ -31,10 +31,8 @@ public function __set(string $property, mixed $value): void /** * Dynamically pass properties gets to the target. - * - * @return mixed */ - public function __get(string $property) + public function __get(string $property): mixed { if (property_exists($this->target, $property)) { return $this->target->{$property}; diff --git a/src/Support/Shell.php b/src/Support/Shell.php new file mode 100644 index 000000000..b5c5b1573 --- /dev/null +++ b/src/Support/Shell.php @@ -0,0 +1,101 @@ +setUpdateCheck(Checker::NEVER); + + $config->getPresenter()->addCasters(self::casters()); + + $shell = new PsyShell($config); + + $loader = self::tinkered($shell); + + try { + $shell->run(); + } finally { + $loader?->unregister(); // @phpstan-ignore-line + } + } + + /** + * Returns the casters for the Psy Shell. + * + * @return array + */ + private static function casters(): array + { + $casters = [ + 'Illuminate\Support\Collection' => 'Laravel\Tinker\TinkerCaster::castCollection', + 'Illuminate\Support\HtmlString' => 'Laravel\Tinker\TinkerCaster::castHtmlString', + 'Illuminate\Support\Stringable' => 'Laravel\Tinker\TinkerCaster::castStringable', + ]; + + if (class_exists('Illuminate\Database\Eloquent\Model')) { + $casters['Illuminate\Database\Eloquent\Model'] = 'Laravel\Tinker\TinkerCaster::castModel'; + } + + if (class_exists('Illuminate\Process\ProcessResult')) { + $casters['Illuminate\Process\ProcessResult'] = 'Laravel\Tinker\TinkerCaster::castProcessResult'; + } + + if (class_exists('Illuminate\Foundation\Application')) { + $casters['Illuminate\Foundation\Application'] = 'Laravel\Tinker\TinkerCaster::castApplication'; + } + + if (function_exists('app') === false) { + return $casters; // @phpstan-ignore-line + } + + $config = app()->make('config'); + + return array_merge($casters, (array) $config->get('tinker.casters', [])); + } + + /** + * Tinkers the current shell, if the Tinker package is available. + */ + private static function tinkered(PsyShell $shell): ?object + { + if (function_exists('app') === false + || ! class_exists(Env::class) + || ! class_exists(ClassAliasAutoloader::class) + ) { + return null; + } + + $path = Env::get('COMPOSER_VENDOR_DIR', app()->basePath().DIRECTORY_SEPARATOR.'vendor'); + + $path .= '/composer/autoload_classmap.php'; + + if (! file_exists($path)) { + $path = TestSuite::getInstance()->rootPath.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'composer'.DIRECTORY_SEPARATOR.'autoload_classmap.php'; + } + + $config = app()->make('config'); + + return ClassAliasAutoloader::register( + $shell, $path, $config->get('tinker.alias', []), $config->get('tinker.dont_alias', []) + ); + } +} diff --git a/src/Support/StateGenerator.php b/src/Support/StateGenerator.php index a7ddba1a9..ddff338b0 100644 --- a/src/Support/StateGenerator.php +++ b/src/Support/StateGenerator.php @@ -10,6 +10,8 @@ use PHPUnit\Event\Code\TestDoxBuilder; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Code\ThrowableBuilder; +use PHPUnit\Event\Test\AfterLastTestMethodErrored; +use PHPUnit\Event\Test\BeforeFirstTestMethodErrored; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\TestData\TestDataCollection; use PHPUnit\Framework\SkippedWithMessageException; @@ -29,9 +31,17 @@ public function fromPhpUnitTestResult(int $passedTests, PHPUnitTestResult $testR TestResult::FAIL, $testResultEvent->throwable() )); - } else { - // @phpstan-ignore-next-line + } elseif ($testResultEvent instanceof BeforeFirstTestMethodErrored) { $state->add(TestResult::fromBeforeFirstTestMethodErrored($testResultEvent)); + } elseif ($testResultEvent instanceof AfterLastTestMethodErrored) { + $state->add(TestResult::fromBeforeFirstTestMethodErrored( + new BeforeFirstTestMethodErrored( + $testResultEvent->telemetryInfo(), + $testResultEvent->testClassName(), + $testResultEvent->calledMethod(), + $testResultEvent->throwable(), + ) + )); } } diff --git a/src/Support/Str.php b/src/Support/Str.php index 0e654bc80..04f4b1fdd 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -13,12 +13,9 @@ final class Str * Pool of alpha-numeric characters for generating (unsafe) random strings * from. */ - private const POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + private const string POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - /** - * @var string - */ - private const PREFIX = '__pest_evaluable_'; + private const string PREFIX = '__pest_evaluable_'; /** * Create a (unsecure & non-cryptographically safe) random alpha-numeric @@ -82,7 +79,7 @@ public static function beforeLast(string $subject, string $search): string return $subject; } - return substr($subject, 0, $pos); + return mb_substr($subject, 0, $pos); } /** @@ -104,7 +101,7 @@ public static function isUuid(string $value): bool /** * Creates a describe block as `$describeDescription` → `$testDescription` format. * - * @param array $describeDescriptions + * @param array $describeDescriptions */ public static function describe(array $describeDescriptions, string $testDescription): string { @@ -120,4 +117,14 @@ public static function isUrl(string $value): bool { return (bool) filter_var($value, FILTER_VALIDATE_URL); } + + /** + * Converts the given `$target` to a URL-friendly "slug". + */ + public static function slugify(string $target): string + { + $target = preg_replace('/[^a-zA-Z0-9]+/', '-', $target); + + return strtolower(trim((string) $target, '-')); + } } diff --git a/src/TestSuite.php b/src/TestSuite.php index ca35a0403..df17ec2df 100644 --- a/src/TestSuite.php +++ b/src/TestSuite.php @@ -78,6 +78,7 @@ public function __construct( $this->afterAll = new AfterAllRepository; $this->rootPath = (string) realpath($rootPath); $this->snapshots = new SnapshotRepository( + $this->rootPath, implode(DIRECTORY_SEPARATOR, [$this->rootPath, $this->testPath]), implode(DIRECTORY_SEPARATOR, ['.pest', 'snapshots']), ); @@ -101,7 +102,7 @@ public static function getInstance( } if (! self::$instance instanceof self) { - Panic::with(new InvalidPestCommand); + throw new InvalidPestCommand; } return self::$instance; @@ -119,7 +120,7 @@ public function getDescription(): string assert($this->test instanceof TestCase); $description = str_replace('__pest_evaluable_', '', $this->test->name()); - $datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($this->test->dataSetAsStringWithData())); + $datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($this->test->dataSetAsString())); return str_replace(' ', '_', $description.$datasetAsString); } diff --git a/tests-external/Features/Expect/toMatchSnapshot.php b/tests-external/Features/Expect/toMatchSnapshot.php new file mode 100644 index 000000000..412ed8ef6 --- /dev/null +++ b/tests-external/Features/Expect/toMatchSnapshot.php @@ -0,0 +1,35 @@ +snapshotable = <<<'HTML' +
+
+
+

Snapshot

+
+
+
+ HTML; +}); + +test('pass with dataset', function ($data) { + TestSuite::getInstance()->snapshots->save($this->snapshotable); + [$filename] = TestSuite::getInstance()->snapshots->get(); + + expect($filename)->toStartWith('tests/.pest/snapshots-external/') + ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap') + ->and($this->snapshotable)->toMatchSnapshot(); +})->with(['my-datas-set-value']); + +describe('within describe', function () { + test('pass with dataset', function ($data) { + TestSuite::getInstance()->snapshots->save($this->snapshotable); + [$filename] = TestSuite::getInstance()->snapshots->get(); + + expect($filename)->toStartWith('tests/.pest/snapshots-external/') + ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap') + ->and($this->snapshotable)->toMatchSnapshot(); + }); +})->with(['my-datas-set-value']); diff --git "a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap" "b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap" similarity index 100% rename from "tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap" rename to "tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap" diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap "b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set_____my_datas_set_value___.snap" similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap rename to "tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set_____my_datas_set_value___.snap" diff --git "a/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap" "b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap" new file mode 100644 index 000000000..c2b4dc0a1 --- /dev/null +++ "b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap" @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap new file mode 100644 index 000000000..c2b4dc0a1 --- /dev/null +++ b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set_____my_datas_set_value___.snap b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set_____my_datas_set_value___.snap new file mode 100644 index 000000000..c2b4dc0a1 --- /dev/null +++ b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set_____my_datas_set_value___.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap new file mode 100644 index 000000000..c2b4dc0a1 --- /dev/null +++ b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git "a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap" "b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap" new file mode 100644 index 000000000..c2b4dc0a1 --- /dev/null +++ "b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap" @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1__.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1_.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1__.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1___2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar__.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar___.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar__.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar___.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar____2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar_____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar____2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar_____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz__.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz___.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz__.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz___.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz____2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz_____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz____2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz_____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo__.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo___.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo__.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo___.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo____2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo_____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo____2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo_____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____10_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10__.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____10_.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10__.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____10___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____10___2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____1_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1__.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____1_.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1__.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____1___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____1___2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2__.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2_.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2__.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2___2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____3_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3__.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____3_.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3__.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____3___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____3___2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____4_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4__.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____4_.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4__.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____4___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____4___2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____5_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5__.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____5_.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5__.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____5___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____5___2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____6_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6__.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____6_.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6__.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____6___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____6___2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____7_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7__.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____7_.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7__.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____7___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____7___2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____8_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8__.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____8_.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8__.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____8___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____8___2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____9_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9__.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____9_.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9__.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____9___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____2.snap similarity index 100% rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____9___2.snap rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____2.snap diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap new file mode 100644 index 000000000..c2b4dc0a1 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots/Visual/Collision/collision_with_data_set________________.snap b/tests/.pest/snapshots/Visual/Collision/collision_with_data_set_________.snap similarity index 100% rename from tests/.pest/snapshots/Visual/Collision/collision_with_data_set________________.snap rename to tests/.pest/snapshots/Visual/Collision/collision_with_data_set_________.snap diff --git a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap index 27bb9dea7..ef023f6cf 100644 --- a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap @@ -1,5 +1,5 @@ - Pest Testing Framework 3.8.4. + Pest Testing Framework 4.3.2. USAGE: pest [options] @@ -27,6 +27,8 @@ --pr .... Output to standard output tests with the given pull request number --pull-request Output to standard output tests with the given pull request number (alias for --pr) --retry Run non-passing tests first and stop execution upon first error or failure + --dirty ...... Only run tests that have uncommitted changes according to Git + --all .................... Ignore test selection from XML configuration file --list-suites ................................... List available test suites --testsuite [name] ......... Only run tests from the specified test suite(s) --exclude-testsuite [name] .. Exclude tests from the specified test suite(s) @@ -68,6 +70,7 @@ --fail-on-risky Signal failure using shell exit code when a test was considered risky --fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered --fail-on-phpunit-deprecation Signal failure using shell exit code when a PHPUnit deprecation was triggered + --fail-on-phpunit-notice Signal failure using shell exit code when a PHPUnit notice was triggered --fail-on-phpunit-warning Signal failure using shell exit code when a PHPUnit warning was triggered --fail-on-notice Signal failure using shell exit code when a notice was triggered --fail-on-skipped Signal failure using shell exit code when a test was skipped @@ -78,6 +81,7 @@ --do-not-fail-on-risky Do not signal failure using shell exit code when a test was considered risky --do-not-fail-on-deprecation Do not signal failure using shell exit code when a deprecation was triggered --do-not-fail-on-phpunit-deprecation Do not signal failure using shell exit code when a PHPUnit deprecation was triggered + --do-not-fail-on-phpunit-notice Do not signal failure using shell exit code when a PHPUnit notice was triggered --do-not-fail-on-phpunit-warning Do not signal failure using shell exit code when a PHPUnit warning was triggered --do-not-fail-on-notice Do not signal failure using shell exit code when a notice was triggered --do-not-fail-on-skipped Do not signal failure using shell exit code when a test was skipped @@ -99,6 +103,7 @@ --display-skipped ........................ Display details for skipped tests --display-deprecations . Display details for deprecations triggered by tests --display-phpunit-deprecations .... Display details for PHPUnit deprecations + --display-phpunit-notices .............. Display details for PHPUnit notices --display-errors ............. Display details for errors triggered by tests --display-notices ........... Display details for notices triggered by tests --display-warnings ......... Display details for warnings triggered by tests @@ -108,10 +113,13 @@ --testdox ................ Replace default result output with TestDox format --testdox-summary Repeat TestDox output for tests with errors, failures, or issues --debug Replace default progress and result output with debugging information + --with-telemetry Include telemetry information in debugging information output --compact ................ Replace default result output with Compact format LOGGING OPTIONS: --log-junit [file] .......... Write test results in JUnit XML format to file + --log-otr [file] Write test results in Open Test Reporting XML format to file + --include-git-information Include Git information in Open Test Reporting XML logfile --log-teamcity [file] ........ Write test results in TeamCity format to file --testdox-html [file] .. Write test results in TestDox format (HTML) to file --testdox-text [file] Write test results in TestDox format (plain text) to file @@ -123,6 +131,7 @@ --coverage ..... Generate code coverage report and output to standard output --coverage --min Set the minimum required coverage percentage, and fail if not met --coverage-clover [file] Write code coverage report in Clover XML format to file + --coverage-openclover [file] Write code coverage report in OpenClover XML format to file --coverage-cobertura [file] Write code coverage report in Cobertura XML format to file --coverage-crap4j [file] Write code coverage report in Crap4J XML format to file --coverage-html [dir] Write code coverage report in HTML format to directory @@ -131,6 +140,7 @@ --only-summary-for-coverage-text Option for code coverage report in text format: only show summary --show-uncovered-for-coverage-text Option for code coverage report in text format: show uncovered files --coverage-xml [dir] . Write code coverage report in XML format to directory + --exclude-source-from-xml-coverage Exclude [source] element from code coverage report in XML format --warm-coverage-cache ........................... Warm static analysis cache --coverage-filter [dir] ........... Include [dir] in code coverage reporting --path-coverage .......... Report path coverage in addition to line coverage diff --git a/tests/.pest/snapshots/Visual/Todo/todo.snap b/tests/.pest/snapshots/Visual/Todo/todo.snap index 5ab4f7fa6..c50794f7a 100644 --- a/tests/.pest/snapshots/Visual/Todo/todo.snap +++ b/tests/.pest/snapshots/Visual/Todo/todo.snap @@ -15,7 +15,7 @@ ↓ todo on describe → should not fail ↓ todo on describe → should run - TODO Tests\Features\Todo - 28 todos + TODO Tests\Features\Todo - 29 todos ↓ something todo later ↓ something todo later chained ↓ something todo later chained and with function body @@ -45,6 +45,7 @@ // nested describe note // test note ↓ todo on describe → todo block → it should not execute + ↓ todo on describe with matching name → describe block → it should not execute ↓ todo on test after describe block ↓ todo with note on test after describe block // test note @@ -80,6 +81,6 @@ PASS Tests\CustomTestCase\ParentTest ✓ override method - Tests: 38 todos, 3 passed (20 assertions) + Tests: 39 todos, 3 passed (21 assertions) Duration: x.xxs diff --git a/tests/.pest/snapshots/Visual/Todo/todo_in_parallel.snap b/tests/.pest/snapshots/Visual/Todo/todo_in_parallel.snap index 5ab4f7fa6..c50794f7a 100644 --- a/tests/.pest/snapshots/Visual/Todo/todo_in_parallel.snap +++ b/tests/.pest/snapshots/Visual/Todo/todo_in_parallel.snap @@ -15,7 +15,7 @@ ↓ todo on describe → should not fail ↓ todo on describe → should run - TODO Tests\Features\Todo - 28 todos + TODO Tests\Features\Todo - 29 todos ↓ something todo later ↓ something todo later chained ↓ something todo later chained and with function body @@ -45,6 +45,7 @@ // nested describe note // test note ↓ todo on describe → todo block → it should not execute + ↓ todo on describe with matching name → describe block → it should not execute ↓ todo on test after describe block ↓ todo with note on test after describe block // test note @@ -80,6 +81,6 @@ PASS Tests\CustomTestCase\ParentTest ✓ override method - Tests: 38 todos, 3 passed (20 assertions) + Tests: 39 todos, 3 passed (21 assertions) Duration: x.xxs diff --git a/tests/.pest/snapshots/Visual/Todo/todos.snap b/tests/.pest/snapshots/Visual/Todo/todos.snap index 5ab4f7fa6..c50794f7a 100644 --- a/tests/.pest/snapshots/Visual/Todo/todos.snap +++ b/tests/.pest/snapshots/Visual/Todo/todos.snap @@ -15,7 +15,7 @@ ↓ todo on describe → should not fail ↓ todo on describe → should run - TODO Tests\Features\Todo - 28 todos + TODO Tests\Features\Todo - 29 todos ↓ something todo later ↓ something todo later chained ↓ something todo later chained and with function body @@ -45,6 +45,7 @@ // nested describe note // test note ↓ todo on describe → todo block → it should not execute + ↓ todo on describe with matching name → describe block → it should not execute ↓ todo on test after describe block ↓ todo with note on test after describe block // test note @@ -80,6 +81,6 @@ PASS Tests\CustomTestCase\ParentTest ✓ override method - Tests: 38 todos, 3 passed (20 assertions) + Tests: 39 todos, 3 passed (21 assertions) Duration: x.xxs diff --git a/tests/.pest/snapshots/Visual/Todo/todos_in_parallel.snap b/tests/.pest/snapshots/Visual/Todo/todos_in_parallel.snap index 5ab4f7fa6..c50794f7a 100644 --- a/tests/.pest/snapshots/Visual/Todo/todos_in_parallel.snap +++ b/tests/.pest/snapshots/Visual/Todo/todos_in_parallel.snap @@ -15,7 +15,7 @@ ↓ todo on describe → should not fail ↓ todo on describe → should run - TODO Tests\Features\Todo - 28 todos + TODO Tests\Features\Todo - 29 todos ↓ something todo later ↓ something todo later chained ↓ something todo later chained and with function body @@ -45,6 +45,7 @@ // nested describe note // test note ↓ todo on describe → todo block → it should not execute + ↓ todo on describe with matching name → describe block → it should not execute ↓ todo on test after describe block ↓ todo with note on test after describe block // test note @@ -80,6 +81,6 @@ PASS Tests\CustomTestCase\ParentTest ✓ override method - Tests: 38 todos, 3 passed (20 assertions) + Tests: 39 todos, 3 passed (21 assertions) Duration: x.xxs diff --git a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap index 557f198ed..c6a7cadfc 100644 --- a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap @@ -1,3 +1,3 @@ - Pest Testing Framework 3.8.4. + Pest Testing Framework 4.3.2. diff --git a/tests/.snapshots/SuccessOnly.php.inc b/tests/.snapshots/SuccessOnly.php.inc index b00a0e36c..b940b7b63 100644 --- a/tests/.snapshots/SuccessOnly.php.inc +++ b/tests/.snapshots/SuccessOnly.php.inc @@ -1,5 +1,5 @@ ##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234'] -##teamcity[testCount count='3' flowId='1234'] +##teamcity[testCount count='4' flowId='1234'] ##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234'] ##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234'] ##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234'] @@ -8,8 +8,12 @@ ##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234'] ##teamcity[testFinished name='can pass with dataset with data set "(true)"' duration='100000' flowId='1234'] ##teamcity[testSuiteFinished name='can pass with dataset' flowId='1234'] +##teamcity[testSuiteStarted name='`block` → can pass with dataset in describe block' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block' flowId='1234'] +##teamcity[testStarted name='`block` → can pass with dataset in describe block with data set "(1)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block with data set "(1)"' flowId='1234'] +##teamcity[testFinished name='`block` → can pass with dataset in describe block with data set "(1)"' duration='100000' flowId='1234'] +##teamcity[testSuiteFinished name='`block` → can pass with dataset in describe block' flowId='1234'] ##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234'] - Tests: 3 passed (3 assertions) + Tests: 4 passed (4 assertions) Duration: 1.00s diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 513a804ea..7b3d6f9b8 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -4,7 +4,6 @@ ✓ preset → strict → ignoring ['usleep'] ✓ preset → security → ignoring ['eval', 'str_shuffle', 'exec', …] ✓ globals - ✓ dependencies ✓ contracts PASS Tests\Environments\Windows @@ -29,6 +28,12 @@ ✓ it gets executed after the test ✓ outer → inner → it does not get executed before the test ✓ outer → inner → it should call all parent afterEach functions + ✓ matching describe block names → outer → middle → inner → it does not get executed before the test + ✓ matching describe block names → outer → middle → inner → it should call all parent afterEach functions + ✓ matching describe block names → outer → middle → it does not get executed before the test + ✓ matching describe block names → outer → middle → it should not call afterEach functions for sibling describe blocks with the same name + ✓ matching describe block names → outer → inner → it does not get executed before the test + ✓ matching describe block names → outer → inner → it should not call afterEach functions for descendent of sibling describe blocks with the same name PASS Tests\Features\Assignee ✓ it may be associated with an assignee [@nunomaduro, @taylorotwell] @@ -45,6 +50,13 @@ ✓ outer → inner → it should call all parent beforeEach functions ✓ with expectations → nested block → test ✓ with expectations → test + ✓ matching describe block names → outer → middle → inner → it should call all parent beforeEach functions + ✓ matching describe block names → outer → middle → it should not call beforeEach functions for sibling describe blocks with the same name + ✓ matching describe block names → outer → inner → it should not call beforeEach functions for descendent of sibling describe blocks with the same name + ✓ matching name → it should call the before each + ✓ matching name → it should not call the before each on the describe block with the same name + ✓ called on all tests → beforeEach should be called + ✓ called on all tests → beforeEach should be called for all tests PASS Tests\Features\BeforeEachProxiesToTestCallWithExpectations ✓ runs 1 @@ -68,13 +80,20 @@ ✓ it adds coverage if --min exist ✓ it generates coverage based on file input - PASS Tests\Features\Covers + PASS Tests\Features\Covers\ClassCoverage ✓ it uses the correct PHPUnit attribute for class + + PASS Tests\Features\Covers\ExceptionHandling + ✓ it throws exception if no class nor method has been found + + PASS Tests\Features\Covers\FunctionCoverage ✓ it uses the correct PHPUnit attribute for function + + PASS Tests\Features\Covers\GuessCoverage ✓ it guesses if the given argument is a class or function + + PASS Tests\Features\Covers\TraitCoverage ✓ it uses the correct PHPUnit attribute for trait - ✓ it uses the correct PHPUnit attribute for covers nothing - ✓ it throws exception if no class nor method has been found PASS Tests\Features\DatasetsTests - 1 todo ✓ it throws exception if dataset does not exist @@ -187,6 +206,11 @@ ✓ with on nested describe → nested → describe → it should include the with value from all parent describe blocks with (1) / (2) ✓ with on nested describe → nested → describe → should include the with value from all parent describe blocks and the test with (1) / (2) / (3) ✓ with on nested describe → nested → after inner describe block with (1) + ✓ matching describe block names → outer → before inner describe block with (1) + ✓ matching describe block names → outer → inner → it should include the with value from all parent describe blocks with (1) / (2) + ✓ matching describe block names → outer → inner → should include the with value from all parent describe blocks and the test with (1) / (2) / (3) + ✓ matching describe block names → outer → inner → it should not include the value from the other describe block with the same name with (1) + ✓ matching describe block names → outer → after inner describe block with (1) ✓ after describe block with (5) ✓ it may be used with high order after describe block with dataset "formal" ✓ it may be used with high order after describe block with dataset "informal" @@ -619,6 +643,13 @@ ✓ pass ✓ failures ✓ failures with custom message + ✓ not failures + + PASS Tests\Features\Expect\toBeSlug + ✓ pass + ✓ failures + ✓ failures with custom message + ✓ failures with default message ✓ not failures PASS Tests\Features\Expect\toBeSnakeCase @@ -1030,6 +1061,10 @@ ✓ it may fail ✓ it may fail with the given message + PASS Tests\Features\Fixture + ✓ it may return a file path + ✓ it may throw an exception if the file does not exist + WARN Tests\Features\Helpers ✓ it can set/get properties on $this ! it gets null if property do not exist → Undefined property Tests\Features\Helpers::$wqdwqdqw @@ -1104,6 +1139,25 @@ // This is before each describe runtime note // This is before each nested describe runtime note // This is a runtime note within a nested describe + ✓ matching describe names → describe block → it may have a static note and runtime note + // This is before each static note + // This is before each matching describe static note + // This is a nested matching static note + // This is a static note within a matching describe + // This is before each runtime note + // This is before each matching describe runtime note + // This is before each matching describe runtime note + // This is a runtime note within a matching describe + ✓ matching describe names → describe block → it may have a static note and runtime note, that are different than the matching describe block + // This is before each static note + // This is before each matching describe static note + // This is a nested matching static note, and should not contain the matching describe notes + // This is before each matching describe static note, and should not contain the matching describe notes + // This is a static note within a matching describe, and should not contain the matching describe notes + // This is before each runtime note + // This is before each matching describe runtime note + // This is before each matching describe runtime note, and should not contain the matching describe notes + // This is a runtime note within a matching describe, and should not contain the matching describe notes ✓ multiple notes // This is before each static note // This is before each runtime note @@ -1119,6 +1173,10 @@ ✓ nested → it may be associated with an pr #1, #4, #5, #6, #3 // an note between an the pr + PASS Tests\Features\References + ✓ it can reference a specific class + ✓ it can reference a specific class method + PASS Tests\Features\Repeat ✓ once ✓ multiple times @ repetition 1 of 5 @@ -1250,6 +1308,10 @@ ✓ describe blocks → describe with repeat → nested describe with repeat → test with no repeat should repeat the number of times specified in the parent describe block @ repetition 2 of 2 ✓ describe blocks → describe with repeat → nested describe with repeat → test with repeat should repeat the number of times specified in the test @ repetition 1 of 2 ✓ describe blocks → describe with repeat → nested describe with repeat → test with repeat should repeat the number of times specified in the test @ repetition 2 of 2 + ✓ matching describe blocks → describe block → it should repeat the number of times specified in the parent describe block @ repetition 1 of 3 + ✓ matching describe blocks → describe block → it should repeat the number of times specified in the parent describe block @ repetition 2 of 3 + ✓ matching describe blocks → describe block → it should repeat the number of times specified in the parent describe block @ repetition 3 of 3 + ✓ matching describe blocks → describe block → should not repeat the number of times of the describe block with the same name PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory1\TestFileInNestedDirectoryWithDatasetsFile ✓ uses dataset with (1) @@ -1295,6 +1357,10 @@ ✓ it can see datasets defined in Pest.php file with ('B') ✓ Pest.php dataset is taken + PASS Tests\Features\See + ✓ it can reference a specific class + ✓ it can reference a specific class method + WARN Tests\Features\Skip ✓ it do not skips - it skips with truthy → 1 @@ -1312,6 +1378,12 @@ - skip on beforeEach → skipped tests → nested inside skipped block → it should not execute - skip on beforeEach → skipped tests → it should not execute ✓ skip on beforeEach → it should execute + - matching describe with skip → describe block → it should not execute + ✓ matching describe with skip → describe block → it should execute a test in a describe block with the same name as a skipped describe block + ✓ matching describe with skip → it should execute + - matching describe with skip on beforeEach → describe block → it should not execute + ✓ matching describe with skip on beforeEach → describe block → it should execute a test in a describe block with the same name as a skipped describe block + ✓ matching describe with skip on beforeEach → it should execute ✓ it does not skip after the describe block - it can skip after the describe block @@ -1334,7 +1406,7 @@ ✓ nested → it may be associated with an ticket #1, #4, #5, #6, #3 // an note between an the ticket - PASS Tests\Features\Todo - 28 todos + PASS Tests\Features\Todo - 29 todos ↓ something todo later ↓ something todo later chained ↓ something todo later chained and with function body @@ -1366,6 +1438,9 @@ // test note ↓ todo on describe → todo block → it should not execute ✓ todo on describe → it should execute + ↓ todo on describe with matching name → describe block → it should not execute + ✓ todo on describe with matching name → describe block → it should execute a test in a describe block with the same name as a todo describe block + ✓ todo on describe with matching name → it should execute ↓ todo on test after describe block ↓ todo with note on test after describe block // test note @@ -1425,6 +1500,11 @@ ✓ nested → nested afterEach execution order ✓ global afterEach execution order + PASS Tests\Hooks\BeforeAllTest + ✓ it gets called before all tests 1 @ repetition 1 of 2 + ✓ it gets called before all tests 1 @ repetition 2 of 2 + ✓ it gets called before all tests 2 + PASS Tests\Hooks\BeforeEachTest ✓ global beforeEach execution order @@ -1698,4 +1778,8 @@ WARN Tests\Visual\Version - visual snapshot of help command output - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1144 passed (2736 assertions) \ No newline at end of file + PASS Testsexternal\Features\Expect\toMatchSnapshot + ✓ pass with dataset with ('my-datas-set-value') + ✓ within describe → pass with dataset with ('my-datas-set-value') + + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 35 skipped, 1188 passed (2813 assertions) \ No newline at end of file diff --git a/tests/.tests/SuccessOnly.php b/tests/.tests/SuccessOnly.php index cb4009a68..4d231a8dd 100644 --- a/tests/.tests/SuccessOnly.php +++ b/tests/.tests/SuccessOnly.php @@ -13,3 +13,9 @@ test('can pass with dataset', function ($value) { expect($value)->toEqual(true); })->with([true]); + +describe('block', function () { + test('can pass with dataset in describe block', function ($number) { + expect($number)->toBeInt(); + })->with([1]); +}); diff --git a/tests/Arch.php b/tests/Arch.php index d8deb4609..6348a0f31 100644 --- a/tests/Arch.php +++ b/tests/Arch.php @@ -27,26 +27,6 @@ ->not->toBeUsed() ->ignoring(Expectation::class); -arch('dependencies') - ->expect('Pest') - ->toOnlyUse([ - 'dd', - 'dump', - 'expect', - 'uses', - 'Termwind', - 'ParaTest', - 'Pest\Arch', - 'Pest\Mutate\Contracts\Configuration', - 'Pest\Mutate\Decorators\TestCallDecorator', - 'Pest\Mutate\Repositories\ConfigurationRepository', - 'Pest\Plugin', - 'NunoMaduro\Collision', - 'Whoops', - 'Symfony\Component\Console', - 'Symfony\Component\Process', - ])->ignoring(['Composer', 'PHPUnit', 'SebastianBergmann']); - arch('contracts') ->expect('Pest\Contracts') ->toOnlyUse([ diff --git a/tests/Features/AfterEach.php b/tests/Features/AfterEach.php index ea0dd7ea6..aff68283c 100644 --- a/tests/Features/AfterEach.php +++ b/tests/Features/AfterEach.php @@ -48,3 +48,55 @@ }); }); }); + +describe('matching describe block names', function () { + afterEach(function () { + $this->state->foo = 1; + }); + + describe('outer', function () { + afterEach(function () { + $this->state->foo++; + }); + + describe('middle', function () { + afterEach(function () { + $this->state->foo++; + }); + + describe('inner', function () { + afterEach(function () { + $this->state->foo++; + }); + + it('does not get executed before the test', function () { + expect($this)->not->toHaveProperty('foo'); + }); + + it('should call all parent afterEach functions', function () { + expect($this->state->foo)->toBe(4); + }); + }); + }); + + describe('middle', function () { + it('does not get executed before the test', function () { + expect($this)->not->toHaveProperty('foo'); + }); + + it('should not call afterEach functions for sibling describe blocks with the same name', function () { + expect($this)->not->toHaveProperty('foo'); + }); + }); + + describe('inner', function () { + it('does not get executed before the test', function () { + expect($this)->not->toHaveProperty('foo'); + }); + + it('should not call afterEach functions for descendent of sibling describe blocks with the same name', function () { + expect($this)->not->toHaveProperty('foo'); + }); + }); + }); +}); diff --git a/tests/Features/BeforeEach.php b/tests/Features/BeforeEach.php index 04f0dd13e..a7c7befe2 100644 --- a/tests/Features/BeforeEach.php +++ b/tests/Features/BeforeEach.php @@ -51,3 +51,78 @@ test('test', function () {}); }); + +describe('matching describe block names', function () { + beforeEach(function () { + $this->foo = 1; + }); + + describe('outer', function () { + beforeEach(function () { + $this->foo++; + }); + + describe('middle', function () { + beforeEach(function () { + $this->foo++; + }); + + describe('inner', function () { + beforeEach(function () { + $this->foo++; + }); + + it('should call all parent beforeEach functions', function () { + expect($this->foo)->toBe(4); + }); + }); + }); + + describe('middle', function () { + it('should not call beforeEach functions for sibling describe blocks with the same name', function () { + expect($this->foo)->toBe(2); + }); + }); + + describe('inner', function () { + it('should not call beforeEach functions for descendent of sibling describe blocks with the same name', function () { + expect($this->foo)->toBe(2); + }); + }); + }); +}); + +$matchingNameCalls = 0; +describe('matching name', function () use (&$matchingNameCalls) { + beforeEach(function () use (&$matchingNameCalls) { + $matchingNameCalls++; + }); + + it('should call the before each', function () use (&$matchingNameCalls) { + expect($matchingNameCalls)->toBe(1); + }); +}); + +describe('matching name', function () use (&$matchingNameCalls) { + it('should not call the before each on the describe block with the same name', function () use (&$matchingNameCalls) { + expect($matchingNameCalls)->toBe(1); + }); +}); + +beforeEach(function () { + $this->baz = 1; +}); + +describe('called on all tests', function () { + beforeEach(function () { + $this->baz++; + }); + + test('beforeEach should be called', function () { + expect($this->baz)->toBe(2); + }); + + test('beforeEach should be called for all tests', function () { + expect($this->baz)->toBe(2); + }); +}); diff --git a/tests/Features/Covers.php b/tests/Features/Covers.php deleted file mode 100644 index 386d523f3..000000000 --- a/tests/Features/Covers.php +++ /dev/null @@ -1,59 +0,0 @@ -getAttributes(); - - expect($attributes[1]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); - expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1'); -}); - -it('uses the correct PHPUnit attribute for function', function () { - $attributes = (new ReflectionClass($this))->getAttributes(); - - expect($attributes[3]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction'); - expect($attributes[3]->getArguments()[0])->toBe('testCoversFunction'); -})->coversFunction('testCoversFunction'); - -it('guesses if the given argument is a class or function', function () { - $attributes = (new ReflectionClass($this))->getAttributes(); - - expect($attributes[5]->getName())->toBe(CoversClass::class); - expect($attributes[5]->getArguments()[0])->toBe(CoversClass3::class); - - expect($attributes[6]->getName())->toBe(CoversFunction::class); - expect($attributes[6]->getArguments()[0])->toBe('testCoversFunction'); -})->covers(CoversClass3::class, 'testCoversFunction'); - -it('uses the correct PHPUnit attribute for trait', function () { - $attributes = (new ReflectionClass($this))->getAttributes(); - - expect($attributes[8]->getName())->toBe('PHPUnit\Framework\Attributes\CoversTrait'); - expect($attributes[8]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait'); -})->coversTrait(CoversTrait::class); - -it('uses the correct PHPUnit attribute for covers nothing', function () { - $attributes = (new ReflectionMethod($this, $this->name()))->getAttributes(); - - expect($attributes[3]->getName())->toBe('PHPUnit\Framework\Attributes\CoversNothing'); - expect($attributes[3]->getArguments())->toHaveCount(0); -})->coversNothing(); - -it('throws exception if no class nor method has been found', function () { - $testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure'); - - $testCall->covers('fakeName'); -})->throws(InvalidArgumentException::class, 'No class, trait or method named "fakeName" has been found.'); diff --git a/tests/Features/Covers/ClassCoverage.php b/tests/Features/Covers/ClassCoverage.php new file mode 100644 index 000000000..ab805e170 --- /dev/null +++ b/tests/Features/Covers/ClassCoverage.php @@ -0,0 +1,13 @@ +getAttributes(); + + expect($attributes[1]->getName())->toBe(CoversClass::class); + expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1'); +}); diff --git a/tests/Features/Covers/ExceptionHandling.php b/tests/Features/Covers/ExceptionHandling.php new file mode 100644 index 000000000..de86bb023 --- /dev/null +++ b/tests/Features/Covers/ExceptionHandling.php @@ -0,0 +1,10 @@ + 'closure'); + + $testCall->covers('fakeName'); +})->throws(InvalidArgumentException::class, 'No class, trait or method named "fakeName" has been found.'); diff --git a/tests/Features/Covers/FunctionCoverage.php b/tests/Features/Covers/FunctionCoverage.php new file mode 100644 index 000000000..fba97080f --- /dev/null +++ b/tests/Features/Covers/FunctionCoverage.php @@ -0,0 +1,12 @@ +getAttributes(); + + expect($attributes[1]->getName())->toBe(CoversFunction::class); + expect($attributes[1]->getArguments()[0])->toBe('testCoversFunction'); +})->coversFunction('testCoversFunction'); diff --git a/tests/Features/Covers/GuessCoverage.php b/tests/Features/Covers/GuessCoverage.php new file mode 100644 index 000000000..e8a84035b --- /dev/null +++ b/tests/Features/Covers/GuessCoverage.php @@ -0,0 +1,17 @@ +getAttributes(); + + expect($attributes[1]->getName())->toBe(CoversClass::class); + expect($attributes[1]->getArguments()[0])->toBe(CoversClass3::class); + + expect($attributes[2]->getName())->toBe(CoversFunction::class); + expect($attributes[2]->getArguments()[0])->toBe('testCoversFunction2'); +})->covers(CoversClass3::class, 'testCoversFunction2'); diff --git a/tests/Features/Covers/TraitCoverage.php b/tests/Features/Covers/TraitCoverage.php new file mode 100644 index 000000000..57bc36806 --- /dev/null +++ b/tests/Features/Covers/TraitCoverage.php @@ -0,0 +1,11 @@ +getAttributes(); + + expect($attributes[1]->getName())->toBe(PHPUnitCoversTrait::class); + expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait'); +})->coversTrait(CoversTrait::class); diff --git a/tests/Features/DatasetsTests.php b/tests/Features/DatasetsTests.php index 54cb77a83..837b56f62 100644 --- a/tests/Features/DatasetsTests.php +++ b/tests/Features/DatasetsTests.php @@ -415,6 +415,34 @@ function () { })->with([1]); }); +describe('matching describe block names', function () { + describe('outer', function () { + test('before inner describe block', function (...$args) { + expect($args)->toBe([1]); + }); + + describe('inner', function () { + it('should include the with value from all parent describe blocks', function (...$args) { + expect($args)->toBe([1, 2]); + }); + + test('should include the with value from all parent describe blocks and the test', function (...$args) { + expect($args)->toBe([1, 2, 3]); + })->with([3]); + })->with([2]); + + describe('inner', function () { + it('should not include the value from the other describe block with the same name', function (...$args) { + expect($args)->toBe([1]); + }); + }); + + test('after inner describe block', function (...$args) { + expect($args)->toBe([1]); + }); + })->with([1]); +}); + test('after describe block', function (...$args) { expect($args)->toBe([5]); })->with([5]); diff --git a/tests/Features/Expect/toBeSlug.php b/tests/Features/Expect/toBeSlug.php new file mode 100644 index 000000000..2d7c19f8a --- /dev/null +++ b/tests/Features/Expect/toBeSlug.php @@ -0,0 +1,24 @@ +toBeSlug() + ->and('Another Test String')->toBeSlug(); +}); + +test('failures', function () { + expect('')->toBeSlug(); +})->throws(ExpectationFailedException::class); + +test('failures with custom message', function () { + expect('')->toBeSlug('oh no!'); +})->throws(ExpectationFailedException::class, 'oh no!'); + +test('failures with default message', function () { + expect('')->toBeSlug(); +})->throws(ExpectationFailedException::class, 'Failed asserting that can be converted to a slug.'); + +test('not failures', function () { + expect('This is a Test String!')->not->toBeSlug(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toMatchSnapshot.php b/tests/Features/Expect/toMatchSnapshot.php index c3df8bace..0c09be936 100644 --- a/tests/Features/Expect/toMatchSnapshot.php +++ b/tests/Features/Expect/toMatchSnapshot.php @@ -74,7 +74,8 @@ public function toString() TestSuite::getInstance()->snapshots->save($this->snapshotable); [$filename] = TestSuite::getInstance()->snapshots->get(); - expect($filename)->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap') + expect($filename)->toStartWith('tests/.pest/snapshots/') + ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap') ->and($this->snapshotable)->toMatchSnapshot(); })->with(['my-datas-set-value']); @@ -83,7 +84,8 @@ public function toString() TestSuite::getInstance()->snapshots->save($this->snapshotable); [$filename] = TestSuite::getInstance()->snapshots->get(); - expect($filename)->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap') + expect($filename)->toStartWith('tests/.pest/snapshots/') + ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap') ->and($this->snapshotable)->toMatchSnapshot(); }); })->with(['my-datas-set-value']); diff --git a/tests/Features/Fixture.php b/tests/Features/Fixture.php new file mode 100644 index 000000000..1ac538b84 --- /dev/null +++ b/tests/Features/Fixture.php @@ -0,0 +1,12 @@ +toBeString() + ->toBeFile(); +}); + +it('may throw an exception if the file does not exist', function () { + fixture('file-that-does-not-exist.php'); +})->throws(InvalidArgumentException::class); diff --git a/tests/Features/Note.php b/tests/Features/Note.php index 571d7c851..22d73b4ff 100644 --- a/tests/Features/Note.php +++ b/tests/Features/Note.php @@ -44,6 +44,36 @@ })->note('This is a nested describe static note'); })->note('This is describe static note'); +describe('matching describe names', function () { + beforeEach(function () { + $this->note('This is before each matching describe runtime note'); + })->note('This is before each matching describe static note'); + + describe('describe block', function () { + beforeEach(function () { + $this->note('This is before each matching describe runtime note'); + })->note('This is before each matching describe static note'); + + it('may have a static note and runtime note', function () { + expect(true)->toBeTrue(true); + + $this->note('This is a runtime note within a matching describe'); + })->note('This is a static note within a matching describe'); + })->note('This is a nested matching static note'); + + describe('describe block', function () { + beforeEach(function () { + $this->note('This is before each matching describe runtime note, and should not contain the matching describe notes'); + })->note('This is before each matching describe static note, and should not contain the matching describe notes'); + + it('may have a static note and runtime note, that are different than the matching describe block', function () { + expect(true)->toBeTrue(true); + + $this->note('This is a runtime note within a matching describe, and should not contain the matching describe notes'); + })->note('This is a static note within a matching describe, and should not contain the matching describe notes'); + })->note('This is a nested matching static note, and should not contain the matching describe notes'); +}); + test('multiple notes', function () { expect(true)->toBeTrue(true); diff --git a/tests/Features/References.php b/tests/Features/References.php new file mode 100644 index 000000000..a19db35a1 --- /dev/null +++ b/tests/Features/References.php @@ -0,0 +1,11 @@ +toBeString(); +})->references(Panic::class); + +it('can reference a specific class method', function () { + expect(Panic::with(...))->toBeCallable(); +})->references([Panic::class, 'with']); diff --git a/tests/Features/Repeat.php b/tests/Features/Repeat.php index 5a3370cd7..89d0c322e 100644 --- a/tests/Features/Repeat.php +++ b/tests/Features/Repeat.php @@ -79,3 +79,17 @@ })->repeat(times: 2); })->repeat(times: 3); }); + +describe('matching describe blocks', function () { + describe('describe block', function () { + it('should repeat the number of times specified in the parent describe block', function () { + expect(true)->toBeTrue(); + }); + })->repeat(times: 3); + + describe('describe block', function () { + test('should not repeat the number of times of the describe block with the same name', function () { + expect(true)->toBeTrue(); + }); + }); +}); diff --git a/tests/Features/See.php b/tests/Features/See.php new file mode 100644 index 000000000..7c9393eeb --- /dev/null +++ b/tests/Features/See.php @@ -0,0 +1,11 @@ +toBeString(); +})->see(Panic::class); + +it('can reference a specific class method', function () { + expect(Panic::with(...))->toBeCallable(); +})->see([Panic::class, 'with']); diff --git a/tests/Features/Skip.php b/tests/Features/Skip.php index 352189458..9c305d356 100644 --- a/tests/Features/Skip.php +++ b/tests/Features/Skip.php @@ -125,6 +125,74 @@ }); }); +describe('matching describe with skip', function () { + beforeEach(function () { + $this->ran = false; + }); + + afterEach(function () { + match ($this->name()) { + '__pest_evaluable__matching_describe_with_skip__→__describe_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), + '__pest_evaluable__matching_describe_with_skip__→__describe_block__→_it_should_execute_a_test_in_a_describe_block_with_the_same_name_as_a_skipped_describe_block' => expect($this->ran)->toBe(true), + '__pest_evaluable__matching_describe_with_skip__→_it_should_execute' => expect($this->ran)->toBe(true), + default => $this->fail('Unexpected test name: '.$this->name()), + }; + }); + + describe('describe block', function () { + it('should not execute', function () { + $this->ran = true; + $this->fail(); + }); + })->skip(); + + describe('describe block', function () { + it('should execute a test in a describe block with the same name as a skipped describe block', function () { + $this->ran = true; + }); + }); + + it('should execute', function () { + $this->ran = true; + expect($this->ran)->toBe(true); + }); +}); + +describe('matching describe with skip on beforeEach', function () { + beforeEach(function () { + $this->ran = false; + }); + + afterEach(function () { + match ($this->name()) { + '__pest_evaluable__matching_describe_with_skip_on_beforeEach__→__describe_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), + '__pest_evaluable__matching_describe_with_skip_on_beforeEach__→__describe_block__→_it_should_execute_a_test_in_a_describe_block_with_the_same_name_as_a_skipped_describe_block' => expect($this->ran)->toBe(true), + '__pest_evaluable__matching_describe_with_skip_on_beforeEach__→_it_should_execute' => expect($this->ran)->toBe(true), + default => $this->fail('Unexpected test name: '.$this->name()), + }; + }); + + describe('describe block', function () { + beforeEach()->skip(); + + it('should not execute', function () { + $this->ran = true; + $this->fail(); + }); + }); + + describe('describe block', function () { + it('should execute a test in a describe block with the same name as a skipped describe block', function () { + $this->ran = true; + }); + }); + + it('should execute', function () { + $this->ran = true; + expect($this->ran)->toBe(true); + }); +}); + it('does not skip after the describe block', function () { expect(true)->toBeTrue(); }); diff --git a/tests/Features/Todo.php b/tests/Features/Todo.php index f979a2ff0..d6e8d9781 100644 --- a/tests/Features/Todo.php +++ b/tests/Features/Todo.php @@ -108,6 +108,40 @@ }); }); +describe('todo on describe with matching name', function () { + beforeEach(function () { + $this->ran = false; + }); + + afterEach(function () { + match ($this->name()) { + '__pest_evaluable__todo_on_describe_with_matching_name__→__describe_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), + '__pest_evaluable__todo_on_describe_with_matching_name__→__describe_block__→_it_should_execute_a_test_in_a_describe_block_with_the_same_name_as_a_todo_describe_block' => expect($this->ran)->toBe(true), + '__pest_evaluable__todo_on_describe_with_matching_name__→_it_should_execute' => expect($this->ran)->toBe(true), + + default => $this->fail('Unexpected test name: '.$this->name()), + }; + }); + + describe('describe block', function () { + it('should not execute', function () { + $this->ran = true; + $this->fail(); + }); + })->todo(); + + describe('describe block', function () { + it('should execute a test in a describe block with the same name as a todo describe block', function () { + $this->ran = true; + }); + }); + + it('should execute', function () { + $this->ran = true; + expect($this->ran)->toBe(true); + }); +}); + test('todo on test after describe block', function () { $this->fail(); })->todo(); diff --git a/tests/Hooks/BeforeAllTest.php b/tests/Hooks/BeforeAllTest.php new file mode 100644 index 000000000..d411d263e --- /dev/null +++ b/tests/Hooks/BeforeAllTest.php @@ -0,0 +1,16 @@ +beforeAll(function () { + expect($_SERVER['globalHook']->calls->beforeAll) + ->toBe(0); + + $_SERVER['globalHook']->calls->beforeAll++; +}); + +it('gets called before all tests 1', function () { + expect($_SERVER['globalHook']->calls->beforeAll)->toBe(1); +})->repeat(2); + +it('gets called before all tests 2', function () { + expect($_SERVER['globalHook']->calls->beforeAll)->toBe(1); +}); diff --git a/tests/Pest.php b/tests/Pest.php index a938fc7e4..e498450cd 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -29,7 +29,6 @@ }) ->beforeAll(function () { $_SERVER['globalHook']->beforeAll = 0; - $_SERVER['globalHook']->calls->beforeAll++; }) ->afterEach(function () { if (! isset($this->ith)) { diff --git a/tests/Unit/Support/StateGenerator.php b/tests/Unit/Support/StateGenerator.php new file mode 100644 index 000000000..44b2ca43b --- /dev/null +++ b/tests/Unit/Support/StateGenerator.php @@ -0,0 +1,106 @@ +fromPhpUnitTestResult(0, $phpunitResult); + + // The AfterLastTestMethodErrored event should be processed and added to state + expect($state->suiteTests)->toHaveCount(1); +}); + +it('handles BeforeFirstTestMethodErrored correctly', function (): void { + $generator = new StateGenerator; + + $throwable = ThrowableBuilder::from(new RuntimeException('Setup error')); + + $beforeFirstEvent = new BeforeFirstTestMethodErrored( + makeTelemetryInfo(), + \PHPUnit\Framework\TestCase::class, + new ClassMethod(\PHPUnit\Framework\TestCase::class, 'setUp'), + $throwable, + ); + + $phpunitResult = makePHPUnitTestResult([$beforeFirstEvent]); + $state = $generator->fromPhpUnitTestResult(0, $phpunitResult); + + expect($state->suiteTests)->toHaveCount(1); +}); + +it('handles mixed errored events without TypeError', function (): void { + $generator = new StateGenerator; + + $throwable = ThrowableBuilder::from(new RuntimeException('Error')); + + $beforeEvent = new BeforeFirstTestMethodErrored( + makeTelemetryInfo(), + \PHPUnit\Framework\TestCase::class, + new ClassMethod(\PHPUnit\Framework\TestCase::class, 'setUp'), + $throwable, + ); + + $afterEvent = new AfterLastTestMethodErrored( + makeTelemetryInfo(), + \PHPUnit\Framework\TestCase::class, + new ClassMethod(\PHPUnit\Framework\TestCase::class, 'tearDown'), + $throwable, + ); + + $phpunitResult = makePHPUnitTestResult([$beforeEvent, $afterEvent]); + $state = $generator->fromPhpUnitTestResult(0, $phpunitResult); + + // Both events share the same testClassName key, so the second overwrites the first + expect($state->suiteTests)->toHaveCount(1); +}); diff --git a/tests/Unit/TestSuite.php b/tests/Unit/TestSuite.php index 5de7a9020..93c8a294e 100644 --- a/tests/Unit/TestSuite.php +++ b/tests/Unit/TestSuite.php @@ -27,7 +27,7 @@ $testSuite->tests->set($method); })->throws( TestClosureMustNotBeStatic::class, - 'Test closure must not be static. Please remove the `static` keyword from the `bar` method in `foo`.', + 'Test closure must not be static. Please remove the [static] keyword from the [bar] method in [foo].', ); it('alerts users about tests with arguments but no input', function () { @@ -40,7 +40,7 @@ $testSuite->tests->set($method); })->throws( DatasetMissing::class, - sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'bar', 1, 'int $arg', 'foo'), + sprintf('A test with the description [%s] has [%d] argument(s) ([%s]) and no dataset(s) provided in [%s]', 'bar', 1, 'int $arg', 'foo'), ); it('can return an array of all test suite filenames', function () { diff --git a/tests/Visual/JUnit.php b/tests/Visual/JUnit.php index 3523bdd6f..fd34ea7a9 100644 --- a/tests/Visual/JUnit.php +++ b/tests/Visual/JUnit.php @@ -36,8 +36,8 @@ expect($result['testsuite']['@attributes']) ->name->toBe('Tests\tests\SuccessOnly') ->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php')) - ->tests->toBe('3') - ->assertions->toBe('3') + ->tests->toBe('4') + ->assertions->toBe('4') ->errors->toBe('0') ->failures->toBe('0') ->skipped->toBe('0'); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index 313f82088..1aced21dc 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -16,7 +16,7 @@ test('parallel', function () use ($run) { expect($run('--exclude-group=integration')) - ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 24 skipped, 1134 passed (2712 assertions)') + ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 26 skipped, 1177 passed (2789 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows();