From dc9a1e8acef05e2bb5325faeab8d881a1f5b7d36 Mon Sep 17 00:00:00 2001 From: Malico Date: Sat, 20 Sep 2025 19:06:23 +0100 Subject: [PATCH 01/40] BugFix: Fix toUseTrait to detect inherited and nested traits --- src/Expectation.php | 17 ++++++++++++++++- tests/Features/Expect/toUseTrait.php | 16 ++++++++++++++++ .../ChildClassExtendingParent.php | 11 +++++++++++ .../ToUseTrait/HasNestedTrait/NestedTrait.php | 13 +++++++++++++ .../HasTrait/ParentClassWithTrait.php | 10 ++++++++++ .../HasTrait/TestTraitForInheritance.php | 17 +++++++++++++++++ 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/Arch/ToUseTrait/HasInheritedTrait/ChildClassExtendingParent.php create mode 100644 tests/Fixtures/Arch/ToUseTrait/HasNestedTrait/NestedTrait.php create mode 100644 tests/Fixtures/Arch/ToUseTrait/HasTrait/ParentClassWithTrait.php create mode 100644 tests/Fixtures/Arch/ToUseTrait/HasTrait/TestTraitForInheritance.php diff --git a/src/Expectation.php b/src/Expectation.php index 5c8a076b..f03a6c4b 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -781,7 +781,22 @@ final class Expectation return false; } - if (! in_array($trait, $object->reflectionClass->getTraitNames(), true)) { + $currentClass = $object->reflectionClass; + $usedTraits = []; + + do { + $classTraits = $currentClass->getTraits(); + foreach ($classTraits as $traitReflection) { + $usedTraits[$traitReflection->getName()] = $traitReflection->getName(); + + $nestedTraits = $traitReflection->getTraits(); + foreach ($nestedTraits as $nestedTrait) { + $usedTraits[$nestedTrait->getName()] = $nestedTrait->getName(); + } + } + } while ($currentClass = $currentClass->getParentClass()); + + if (! array_key_exists($trait, $usedTraits)) { return false; } } diff --git a/tests/Features/Expect/toUseTrait.php b/tests/Features/Expect/toUseTrait.php index fd9c933f..806490b3 100644 --- a/tests/Features/Expect/toUseTrait.php +++ b/tests/Features/Expect/toUseTrait.php @@ -14,3 +14,19 @@ test('failures', function () { test('not failures', function () { expect('Pest\Expectations\HigherOrderExpectation')->not->toUseTrait('Pest\Concerns\Retrievable'); })->throws(ArchExpectationFailedException::class); + +test('trait inheritance - direct usage', function () { + expect('Tests\Fixtures\Arch\ToUseTrait\HasTrait\ParentClassWithTrait')->toUseTrait('Tests\Fixtures\Arch\ToUseTrait\HasTrait\TestTraitForInheritance'); +}); + +test('trait inheritance - inherited usage', function () { + expect('Tests\Fixtures\Arch\ToUseTrait\HasInheritedTrait\ChildClassExtendingParent')->toUseTrait('Tests\Fixtures\Arch\ToUseTrait\HasTrait\TestTraitForInheritance'); +}); + +test('trait inheritance - negative case', function () { + expect('Tests\Fixtures\Arch\ToUseTrait\HasInheritedTrait\ChildClassExtendingParent')->not->toUseTrait('NonExistentTrait'); +}); + +test('nested trait inheritance', function () { + expect('Tests\Fixtures\Arch\ToUseTrait\HasInheritedTrait\ChildClassExtendingParent')->toUseTrait('Tests\Fixtures\Arch\ToUseTrait\HasNestedTrait\NestedTrait'); +}); diff --git a/tests/Fixtures/Arch/ToUseTrait/HasInheritedTrait/ChildClassExtendingParent.php b/tests/Fixtures/Arch/ToUseTrait/HasInheritedTrait/ChildClassExtendingParent.php new file mode 100644 index 00000000..df7ce9ec --- /dev/null +++ b/tests/Fixtures/Arch/ToUseTrait/HasInheritedTrait/ChildClassExtendingParent.php @@ -0,0 +1,11 @@ + Date: Wed, 5 Nov 2025 17:15:51 +0100 Subject: [PATCH 02/40] Pass test dir to worker #1444 Test directory argument is lost when spawning workers, add it again. --- src/Plugins/Parallel/Paratest/WrapperRunner.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 469f2aa6..1d73e2db 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -131,6 +131,7 @@ final class WrapperRunner implements RunnerInterface $parameters = $this->handleLaravelHerd($parameters); $parameters[] = $wrapper; + $parameters[] = '--test-directory='.TestSuite::getInstance()->testPath; $this->parameters = $parameters; $this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry; From bd5fed9e12be2e9e4fd603214c41238d1716403f Mon Sep 17 00:00:00 2001 From: Nino <18269685+treyssatvincent@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:26:56 +0000 Subject: [PATCH 03/40] add missing classes before toExtend on laravel preset Add missing `->classes()` before `->toExtend()` on the laravel preset for 2 namespaces. Otherwise you can't use interfaces on theses namespace. For example, if you create an interface `YourSuperRequestContract` on the `app/Http/Requests` namespace you will get this error: ``` Expecting 'app/Http/Requests/YourSuperRequestContract.php' to extend 'Illuminate\Foundation\Http\FormRequest'. ``` --- src/ArchPresets/Laravel.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ArchPresets/Laravel.php b/src/ArchPresets/Laravel.php index 7d094c36..85f72b5d 100644 --- a/src/ArchPresets/Laravel.php +++ b/src/ArchPresets/Laravel.php @@ -69,6 +69,7 @@ final class Laravel extends AbstractPreset ->toHaveSuffix('Request'); $this->expectations[] = expect('App\Http\Requests') + ->classes() ->toExtend('Illuminate\Foundation\Http\FormRequest'); $this->expectations[] = expect('App\Http\Requests') @@ -118,6 +119,7 @@ final class Laravel extends AbstractPreset ->toHaveMethod('handle'); $this->expectations[] = expect('App\Notifications') + ->classes() ->toExtend('Illuminate\Notifications\Notification'); $this->expectations[] = expect('App') @@ -128,6 +130,7 @@ final class Laravel extends AbstractPreset ->toHaveSuffix('ServiceProvider'); $this->expectations[] = expect('App\Providers') + ->classes() ->toExtend('Illuminate\Support\ServiceProvider'); $this->expectations[] = expect('App\Providers') From 0e7c2abe8b2fbea4c5939d201398804a89723adc Mon Sep 17 00:00:00 2001 From: Bakhromjon <74450789+bibrokhim@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:32:36 +0500 Subject: [PATCH 04/40] Add Rules to Laravel preset --- src/ArchPresets/Laravel.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ArchPresets/Laravel.php b/src/ArchPresets/Laravel.php index 7d094c36..91f32196 100644 --- a/src/ArchPresets/Laravel.php +++ b/src/ArchPresets/Laravel.php @@ -173,5 +173,9 @@ final class Laravel extends AbstractPreset ->toImplement('Illuminate\Contracts\Container\ContextualAttribute') ->toHaveAttribute('Attribute') ->toHaveMethod('resolve'); + + $this->expectations[] = expect('App\Rules') + ->classes() + ->toImplement('Illuminate\Contracts\Validation\ValidationRule'); } } From e6f511302b24c1c3ce99ed66ab00af9e5dc2bc15 Mon Sep 17 00:00:00 2001 From: Ilia Smirnov Date: Tue, 27 Jan 2026 16:16:03 +0100 Subject: [PATCH 05/40] fix: enhance support for --parallel and --teamcity arguments by restoring --teamcity for ParaTest and fixing teamcity output concurrency --- src/Plugins/Parallel.php | 18 +++++++++++++++++- .../Parallel/Paratest/ResultPrinter.php | 7 +++---- .../Parallel/Paratest/WrapperRunner.php | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Plugins/Parallel.php b/src/Plugins/Parallel.php index 94902823..d2509695 100644 --- a/src/Plugins/Parallel.php +++ b/src/Plugins/Parallel.php @@ -127,7 +127,9 @@ final class Parallel implements HandlesArguments $arguments ); - $exitCode = $this->paratestCommand()->run(new ArgvInput($filteredArguments), new CleanConsoleOutput); + $filteredArguments = $this->processTeamcityArguments($filteredArguments); + + $exitCode = $this->paratestCommand()->run(new ArgvInput(array_values($filteredArguments)), new CleanConsoleOutput); return CallsAddsOutput::execute($exitCode); } @@ -197,4 +199,18 @@ final class Parallel implements HandlesArguments return $this->popArgument('-p', $arguments); } + + /** + * @param string[] $arguments + * @return string[] + */ + public function processTeamcityArguments(array $arguments): array + { + $argv = new ArgvInput; + if ($argv->hasParameterOption('--teamcity')) { + $arguments[] = '--teamcity'; + } + + return $arguments; + } } diff --git a/src/Plugins/Parallel/Paratest/ResultPrinter.php b/src/Plugins/Parallel/Paratest/ResultPrinter.php index e7a1c24d..4a2082a8 100644 --- a/src/Plugins/Parallel/Paratest/ResultPrinter.php +++ b/src/Plugins/Parallel/Paratest/ResultPrinter.php @@ -92,14 +92,13 @@ final class ResultPrinter $this->teamcityLogFileHandle = $teamcityLogFileHandle; } - /** @param list $teamcityFiles */ public function printFeedback( SplFileInfo $progressFile, SplFileInfo $outputFile, - array $teamcityFiles + ?SplFileInfo $teamcityFile, ): void { - if ($this->options->needsTeamcity) { - $teamcityProgress = $this->tailMultiple($teamcityFiles); + if ($this->options->needsTeamcity && $teamcityFile instanceof SplFileInfo) { + $teamcityProgress = $this->tailMultiple([$teamcityFile]); if ($this->teamcityLogFileHandle !== null) { fwrite($this->teamcityLogFileHandle, $teamcityProgress); diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 469f2aa6..147cbc60 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -225,7 +225,7 @@ final class WrapperRunner implements RunnerInterface $this->printer->printFeedback( $worker->progressFile, $worker->unexpectedOutputFile, - $this->teamcityFiles, + $worker->teamcityFile ?? null, ); $worker->reset(); } From 9fcbca69d46f395c5f403b8dccc7277b43678cda Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Fri, 13 Feb 2026 10:41:22 +0000 Subject: [PATCH 06/40] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed0020c0..33cab137 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,6 @@ We cannot thank our sponsors enough for their incredible support in funding Pest - [Route4Me](https://route4me.com/pt?ref=pestphp) - [Nerdify](https://getnerdify.com/?ref=pestphp) - [Akaunting](https://akaunting.com/?ref=pestphp) -- [LambdaTest](https://lambdatest.com/?ref=pestphp) +- [TestMu AI](https://www.testmuai.com/?utm_medium=sponsor&utm_source=pest) Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. From 1f39b28e2c18ead15170e52c1d8322a537a5ca62 Mon Sep 17 00:00:00 2001 From: Liam Hammett Date: Mon, 16 Feb 2026 00:25:47 +0100 Subject: [PATCH 07/40] Allow App\Http to be used in providers --- src/ArchPresets/Laravel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ArchPresets/Laravel.php b/src/ArchPresets/Laravel.php index 7d094c36..167725b6 100644 --- a/src/ArchPresets/Laravel.php +++ b/src/ArchPresets/Laravel.php @@ -150,7 +150,7 @@ final class Laravel extends AbstractPreset ->toHaveSuffix('Controller'); $this->expectations[] = expect('App\Http') - ->toOnlyBeUsedIn('App\Http'); + ->toOnlyBeUsedIn(['App\Http', 'App\Providers']); $this->expectations[] = expect('App\Http\Controllers') ->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy', 'middleware']); From cf00e58b7d2741eb50a038b20e370c02561d6cfd Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 17 Feb 2026 11:22:04 +0000 Subject: [PATCH 08/40] chore: bumps dependencies --- composer.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 9d7fcdee..36540eb1 100644 --- a/composer.json +++ b/composer.json @@ -18,19 +18,19 @@ ], "require": { "php": "^8.3.0", - "brianium/paratest": "^7.16.1", - "nunomaduro/collision": "^8.8.3", - "nunomaduro/termwind": "^2.3.3", + "brianium/paratest": "^7.19.0", + "nunomaduro/collision": "^8.9.0", + "nunomaduro/termwind": "^2.4.0", "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" + "phpunit/phpunit": "^12.5.12", + "symfony/process": "^7.4.5|^8.0.5" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.8", + "phpunit/phpunit": ">12.5.12", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, @@ -58,7 +58,7 @@ "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" + "psy/psysh": "^0.12.20" }, "minimum-stability": "dev", "prefer-stable": true, From 69cb752d02a2e80e5ebe85e73d9a2f0cea53dd5c Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 17 Feb 2026 15:01:37 +0000 Subject: [PATCH 09/40] chore: bumps dependencies --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 36540eb1..9f4ffa85 100644 --- a/composer.json +++ b/composer.json @@ -55,8 +55,8 @@ ] }, "require-dev": { - "pestphp/pest-dev-tools": "^4.0.0", - "pestphp/pest-plugin-browser": "^4.2.1", + "pestphp/pest-dev-tools": "^4.1.0", + "pestphp/pest-plugin-browser": "^4.3.0", "pestphp/pest-plugin-type-coverage": "^4.0.3", "psy/psysh": "^0.12.20" }, From aaa226f6a647a2ddd176863ec9fd2d6b0237efbc Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 17 Feb 2026 15:14:45 +0000 Subject: [PATCH 10/40] chore: tests against symfony 8 --- .github/workflows/tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 68d070da..8ab19811 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,9 +13,12 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, macos-latest] # windows-latest - symfony: ['7.3'] + symfony: ['7.4', '8.0'] php: ['8.3', '8.4', '8.5'] dependency_version: [prefer-stable] + exclude: + - php: '8.3' + symfony: '8.0' name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }} From b0f6a74cb6419b620318eb2712304f2df6d7d6a2 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 17 Feb 2026 15:18:33 +0000 Subject: [PATCH 11/40] ci: makes jobs faster --- .github/workflows/static.yml | 14 ++++++++++++++ .github/workflows/tests.yml | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 5cce40fc..ef51a559 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -29,6 +29,20 @@ jobs: coverage: none extensions: sockets + - name: Get Composer cache directory + id: composer-cache + shell: bash + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: static-php-8.3-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + static-php-8.3-${{ matrix.dependency-version }}-composer- + static-php-8.3-composer- + - name: Install Dependencies run: composer update --prefer-stable --no-interaction --no-progress --ansi diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8ab19811..a0c1ee2c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,6 +34,20 @@ jobs: coverage: none extensions: sockets + - name: Get Composer cache directory + id: composer-cache + shell: bash + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer- + ${{ matrix.os }}-php-${{ matrix.php }}-composer- + - name: Setup Problem Matches run: | echo "::add-matcher::${{ runner.tool_cache }}/php.json" From a49cf7edc5b247c94e4e155fc1bf282df59d1edb Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 17 Feb 2026 15:21:20 +0000 Subject: [PATCH 12/40] ci: speed up ci --- .github/workflows/static.yml | 7 ++++++- .github/workflows/tests.yml | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index ef51a559..ae45ea68 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -2,10 +2,15 @@ name: Static Analysis on: push: + branches: [4.x] pull_request: schedule: - cron: '0 0 * * *' +concurrency: + group: static-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: static: if: github.event_name != 'schedule' || github.repository == 'pestphp/pest' @@ -44,7 +49,7 @@ jobs: static-php-8.3-composer- - name: Install Dependencies - run: composer update --prefer-stable --no-interaction --no-progress --ansi + run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi - name: Profanity Check run: composer test:profanity diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a0c1ee2c..1f74b76e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,8 +2,13 @@ name: Tests on: push: + branches: [4.x] pull_request: +concurrency: + group: tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: tests: if: github.event_name != 'schedule' || github.repository == 'pestphp/pest' From f96a1b27864b585b0b29b0ee7331176726f7e54a Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 17 Feb 2026 15:27:18 +0000 Subject: [PATCH 13/40] release: v4.4.1 --- src/Pest.php | 2 +- .../Visual/Help/visual_snapshot_of_help_command_output.snap | 2 +- .../Visual/Version/visual_snapshot_of_help_command_output.snap | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Pest.php b/src/Pest.php index ba3a540e..a66b7254 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '4.3.2'; + return '4.4.1'; } function testDirectory(string $file = ''): string 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 ef023f6c..808b1613 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 4.3.2. + Pest Testing Framework 4.4.1. USAGE: pest [options] 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 c6a7cadf..0160b757 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 4.3.2. + Pest Testing Framework 4.4.1. From 7d80f1d20e672b44435ded168d5478ce82967e48 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 17 Feb 2026 15:34:23 +0000 Subject: [PATCH 14/40] chore: removes non used files --- Makefile | 14 -------------- docker-compose.yml | 14 -------------- 2 files changed, 28 deletions(-) delete mode 100644 Makefile delete mode 100644 docker-compose.yml diff --git a/Makefile b/Makefile deleted file mode 100644 index ec4faa84..00000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -# Well documented Makefiles -DEFAULT_GOAL := help -help: - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) - -build: ## Build all docker images. Specify the command e.g. via make build ARGS="--build-arg PHP=8.2" - docker compose build $(ARGS) - -##@ [Application] -install: ## Install the composer dependencies - docker compose run --rm composer install - -test: ## Run the tests - docker compose run --rm composer test diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 70b75de2..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: "3.8" - -services: - php: - build: - context: ./docker - volumes: - - .:/var/www/html - composer: - build: - context: ./docker - volumes: - - .:/var/www/html - entrypoint: ["composer"] From 5de8693e3bc02dbdf81d9c203d10468f9f6aa80b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 17 Feb 2026 16:15:58 +0000 Subject: [PATCH 15/40] chore: style --- src/Plugins/Parallel/Paratest/WrapperRunner.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 1d73e2db..064856bc 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -131,7 +131,7 @@ final class WrapperRunner implements RunnerInterface $parameters = $this->handleLaravelHerd($parameters); $parameters[] = $wrapper; - $parameters[] = '--test-directory='.TestSuite::getInstance()->testPath; + $parameters[] = '--test-directory='.TestSuite::getInstance()->testPath; $this->parameters = $parameters; $this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry; From df7b6c8454cbd286ac5bf3dd7352e776e2645a47 Mon Sep 17 00:00:00 2001 From: SimonBroekaert Date: Fri, 22 Aug 2025 02:14:30 +0200 Subject: [PATCH 16/40] feat: add toBeCasedCorrectly arch test assertion --- src/Expectation.php | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Expectation.php b/src/Expectation.php index 50729d7a..3a5cc94e 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -18,6 +18,7 @@ use Pest\Arch\Expectations\ToOnlyUse; use Pest\Arch\Expectations\ToUse; use Pest\Arch\Expectations\ToUseNothing; use Pest\Arch\PendingArchExpectation; +use Pest\Arch\Support\Composer; use Pest\Arch\Support\FileLineFinder; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; @@ -669,6 +670,41 @@ final class Expectation throw InvalidExpectation::fromMethods(['toHavePrivateMethods']); } + /** + * Asserts that the given expectation target is cased correctly. + */ + public function toBeCasedCorrectly(): ArchExpectation + { + return Targeted::make( + $this, + function (ObjectDescription $object): bool { + if (! isset($object->reflectionClass)) { + return false; + } + + $realPath = realpath($object->path); + + foreach (Composer::userNamespaces() as $directory => $namespace) { + if (str_starts_with($realPath, $directory)) { + $relativePath = substr($realPath, strlen($directory) + 1); + $relativePath = explode('.', $relativePath)[0]; + $classFromPath = $namespace . '\\' . str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath); + + if ($classFromPath === $object->reflectionClass->getName()) { + return true; + } + + return false; + } + } + + return false; + }, + "to be cased correctly", + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), + ); + } + /** * Asserts that the given expectation target is enum. */ From 1675dd1d4193b9aa5210d07ac73efeefae0ca5f6 Mon Sep 17 00:00:00 2001 From: SimonBroekaert Date: Tue, 17 Feb 2026 19:03:46 +0100 Subject: [PATCH 17/40] chore: add tests for toBeCasedCorrectly() arch test --- tests/Features/Expect/toBeCasedCorrectly.php | 12 ++++++++++++ .../CorrectCasing/CorrectCasing.php | 5 +++++ .../IncorrectCasing/incorrectCasing.php | 5 +++++ .../incorrectDirectoryCasing/CorrectCasing.php | 5 +++++ 4 files changed, 27 insertions(+) create mode 100644 tests/Features/Expect/toBeCasedCorrectly.php create mode 100644 tests/Fixtures/Arch/ToBeCasedCorrectly/CorrectCasing/CorrectCasing.php create mode 100644 tests/Fixtures/Arch/ToBeCasedCorrectly/IncorrectCasing/incorrectCasing.php create mode 100644 tests/Fixtures/Arch/ToBeCasedCorrectly/incorrectDirectoryCasing/CorrectCasing.php diff --git a/tests/Features/Expect/toBeCasedCorrectly.php b/tests/Features/Expect/toBeCasedCorrectly.php new file mode 100644 index 00000000..6a8432d2 --- /dev/null +++ b/tests/Features/Expect/toBeCasedCorrectly.php @@ -0,0 +1,12 @@ +expect('Tests\Fixtures\Arch\ToBeCasedCorrectly\CorrectCasing') + ->toBeCasedCorrectly(); + +test('failure') + ->expect('Tests\Fixtures\Arch\ToBeCasedCorrectly\IncorrectCasing') + ->toBeCasedCorrectly() + ->throws(ArchExpectationFailedException::class); diff --git a/tests/Fixtures/Arch/ToBeCasedCorrectly/CorrectCasing/CorrectCasing.php b/tests/Fixtures/Arch/ToBeCasedCorrectly/CorrectCasing/CorrectCasing.php new file mode 100644 index 00000000..8418d969 --- /dev/null +++ b/tests/Fixtures/Arch/ToBeCasedCorrectly/CorrectCasing/CorrectCasing.php @@ -0,0 +1,5 @@ + Date: Fri, 20 Feb 2026 01:18:43 +0000 Subject: [PATCH 18/40] Add SerpApi to the list of sponsors Add SerpApi as a sponsor in the README. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 33cab137..aa128055 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ We cannot thank our sponsors enough for their incredible support in funding Pest - **[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)** +- **[SerpApi](https://serpapi.com/?ref=nunomaduro)** ### Gold Sponsors From f7015fe59c250b13457d43a25a4adfbf28ea08c2 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 24 Feb 2026 10:44:48 +0000 Subject: [PATCH 19/40] chore: adjusts sponsors --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index aa128055..af33fd43 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,10 @@ We cannot thank our sponsors enough for their incredible support in funding Pest ### Platinum Sponsors - **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)** -- **[Devin](https://devin.ai/?ref=nunomaduro)** - **[Mailtrap](https://l.rw.rw/pestphp)** +- **[SerpApi](https://serpapi.com/?ref=nunomaduro)** - **[Tighten](https://tighten.com/?ref=nunomaduro)** - **[Redberry](https://redberry.international/laravel-development/?utm_source=pest&utm_medium=banner&utm_campaign=pest_sponsorship)** -- **[SerpApi](https://serpapi.com/?ref=nunomaduro)** ### Gold Sponsors From 2a80101f420a377bdf491a99f8227927b29fb5cd Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Tue, 10 Mar 2026 21:06:28 +0000 Subject: [PATCH 20/40] chore: update styling --- composer.json | 4 ++-- overrides/Logging/JUnit/JunitXmlLogger.php | 7 +++++-- src/Concerns/Pipeable.php | 2 +- src/Concerns/Testable.php | 6 +++--- src/Expectation.php | 4 ++-- src/Factories/TestCaseFactory.php | 3 ++- src/Factories/TestCaseMethodFactory.php | 12 ++++++++---- src/Functions.php | 8 ++++---- src/Logging/Converter.php | 4 ++-- src/Logging/TeamCity/TeamCityLogger.php | 2 +- src/Mixins/Expectation.php | 5 +++-- src/PendingCalls/Concerns/Describable.php | 6 ++++-- src/PendingCalls/DescribeCall.php | 2 +- src/PendingCalls/TestCall.php | 12 ++++++++---- src/Plugins/Parallel/Handlers/Laravel.php | 5 +++-- src/Support/Closure.php | 4 ++-- src/Support/Container.php | 2 +- src/Support/Reflection.php | 7 ++++--- tests/Autoload.php | 6 ++++-- tests/Features/BeforeAll.php | 2 +- tests/Features/Coverage.php | 2 +- tests/Features/Helpers.php | 2 +- tests/Helpers.php | 4 +++- .../CustomAffixes/FolderWithAn@/ExampleTest.php | 3 ++- tests/PHPUnit/CustomTestCase/UsesPerDirectory.php | 4 +++- .../SubFolder2/UsesPerFile.php | 4 +++- tests/Unit/Configuration/Theme.php | 4 +++- tests/Visual/Collision.php | 4 +++- tests/Visual/Help.php | 4 +++- tests/Visual/Success.php | 4 +++- tests/Visual/TeamCity.php | 4 +++- tests/Visual/Version.php | 4 +++- 32 files changed, 92 insertions(+), 54 deletions(-) diff --git a/composer.json b/composer.json index 9f4ffa85..846dc188 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "require": { "php": "^8.3.0", "brianium/paratest": "^7.19.0", - "nunomaduro/collision": "^8.9.0", + "nunomaduro/collision": "^8.9.1", "nunomaduro/termwind": "^2.4.0", "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", @@ -58,7 +58,7 @@ "pestphp/pest-dev-tools": "^4.1.0", "pestphp/pest-plugin-browser": "^4.3.0", "pestphp/pest-plugin-type-coverage": "^4.0.3", - "psy/psysh": "^0.12.20" + "psy/psysh": "^0.12.21" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/overrides/Logging/JUnit/JunitXmlLogger.php b/overrides/Logging/JUnit/JunitXmlLogger.php index b7362b1f..1fc237a6 100644 --- a/overrides/Logging/JUnit/JunitXmlLogger.php +++ b/overrides/Logging/JUnit/JunitXmlLogger.php @@ -14,6 +14,9 @@ namespace PHPUnit\Logging\JUnit; use DOMDocument; use DOMElement; +use Pest\Logging\Converter; +use Pest\Support\Container; +use Pest\TestSuite; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\EventFacadeIsSealedException; @@ -50,7 +53,7 @@ final class JunitXmlLogger { private readonly Printer $printer; - private readonly \Pest\Logging\Converter $converter; // pest-added + private readonly Converter $converter; // pest-added private DOMDocument $document; @@ -108,7 +111,7 @@ final class JunitXmlLogger public function __construct(Printer $printer, Facade $facade) { $this->printer = $printer; - $this->converter = new \Pest\Logging\Converter(\Pest\Support\Container::getInstance()->get(\Pest\TestSuite::class)->rootPath); // pest-added + $this->converter = new Converter(Container::getInstance()->get(TestSuite::class)->rootPath); // pest-added $this->registerSubscribers($facade); $this->createDocument(); diff --git a/src/Concerns/Pipeable.php b/src/Concerns/Pipeable.php index 63ab0b7d..4e44f8db 100644 --- a/src/Concerns/Pipeable.php +++ b/src/Concerns/Pipeable.php @@ -66,6 +66,6 @@ trait Pipeable */ private function pipes(string $name, object $context, string $scope): array { - return array_map(fn (Closure $pipe): \Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); + return array_map(fn (Closure $pipe): Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); } } diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 767a7c69..7ed0887d 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -129,7 +129,7 @@ trait Testable */ public function __addBeforeAll(?Closure $hook): void { - if (! $hook instanceof \Closure) { + if (! $hook instanceof Closure) { return; } @@ -143,7 +143,7 @@ trait Testable */ public function __addAfterAll(?Closure $hook): void { - if (! $hook instanceof \Closure) { + if (! $hook instanceof Closure) { return; } @@ -173,7 +173,7 @@ trait Testable */ private function __addHook(string $property, ?Closure $hook): void { - if (! $hook instanceof \Closure) { + if (! $hook instanceof Closure) { return; } diff --git a/src/Expectation.php b/src/Expectation.php index 50729d7a..d9ce935a 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -136,7 +136,7 @@ final class Expectation /** * Dump the expectation value when the result of the condition is truthy. * - * @param (\Closure(TValue): bool)|bool $condition + * @param (Closure(TValue): bool)|bool $condition * @return self */ public function ddWhen(Closure|bool $condition, mixed ...$arguments): Expectation @@ -153,7 +153,7 @@ final class Expectation /** * Dump the expectation value when the result of the condition is falsy. * - * @param (\Closure(TValue): bool)|bool $condition + * @param (Closure(TValue): bool)|bool $condition * @return self */ public function ddUnless(Closure|bool $condition, mixed ...$arguments): Expectation diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 0d2a0978..3349d03d 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -17,6 +17,7 @@ use Pest\Factories\Concerns\HigherOrderable; use Pest\Support\Reflection; use Pest\Support\Str; use Pest\TestSuite; +use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -135,7 +136,7 @@ final class TestCaseFactory $this->attributes = [ new Attribute( - \PHPUnit\Framework\Attributes\TestDox::class, + TestDox::class, [$this->filename], ), ...$this->attributes, diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index c88db44d..9438f837 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -9,10 +9,14 @@ use Pest\Evaluators\Attributes; use Pest\Exceptions\ShouldNotHappen; use Pest\Factories\Concerns\HigherOrderable; use Pest\Repositories\DatasetsRepository; +use Pest\Support\Description; use Pest\Support\Str; use Pest\TestSuite; use PHPUnit\Framework\Assert; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Depends; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; /** @@ -32,7 +36,7 @@ final class TestCaseMethodFactory /** * The test's describing, if any. * - * @var array + * @var array */ public array $describing = []; @@ -192,11 +196,11 @@ final class TestCaseMethodFactory $this->attributes = [ new Attribute( - \PHPUnit\Framework\Attributes\Test::class, + Test::class, [], ), new Attribute( - \PHPUnit\Framework\Attributes\TestDox::class, + TestDox::class, [str_replace('*/', '{@*}', $this->description)], ), ...$this->attributes, @@ -206,7 +210,7 @@ final class TestCaseMethodFactory $depend = Str::evaluable($this->describing === [] ? $depend : Str::describe($this->describing, $depend)); $this->attributes[] = new Attribute( - \PHPUnit\Framework\Attributes\Depends::class, + Depends::class, [$depend], ); } diff --git a/src/Functions.php b/src/Functions.php index 0ed631f3..42331a56 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -140,7 +140,7 @@ if (! function_exists('test')) { */ function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall { - if ($description === null && TestSuite::getInstance()->test instanceof \PHPUnit\Framework\TestCase) { + if ($description === null && TestSuite::getInstance()->test instanceof TestCase) { return new HigherOrderTapProxy(TestSuite::getInstance()->test); } @@ -236,7 +236,7 @@ if (! function_exists('covers')) { /** @var MutationTestRunner $runner */ $runner = Container::getInstance()->get(MutationTestRunner::class); - /** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */ + /** @var ConfigurationRepository $configurationRepository */ $configurationRepository = Container::getInstance()->get(ConfigurationRepository::class); $everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false; $classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false; @@ -263,7 +263,7 @@ if (! function_exists('mutates')) { /** @var MutationTestRunner $runner */ $runner = Container::getInstance()->get(MutationTestRunner::class); - /** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */ + /** @var ConfigurationRepository $configurationRepository */ $configurationRepository = Container::getInstance()->get(ConfigurationRepository::class); $everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false; $classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false; @@ -320,7 +320,7 @@ if (! function_exists('visit')) { */ function visit(array|string $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage { - if (! class_exists(\Pest\Browser\Configuration::class)) { + if (! class_exists(Pest\Browser\Configuration::class)) { PluginBrowser::install(); exit(0); diff --git a/src/Logging/Converter.php b/src/Logging/Converter.php index 50a42d1a..e0b69bb0 100644 --- a/src/Logging/Converter.php +++ b/src/Logging/Converter.php @@ -151,7 +151,7 @@ final readonly class Converter { if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) { $firstTest = $this->getFirstTest($testSuite); - if ($firstTest instanceof \PHPUnit\Event\Code\TestMethod) { + if ($firstTest instanceof TestMethod) { return $this->getTestMethodNameWithoutDatasetSuffix($firstTest); } } @@ -179,7 +179,7 @@ final readonly class Converter public function getTestSuiteLocation(TestSuite $testSuite): ?string { $firstTest = $this->getFirstTest($testSuite); - if (! $firstTest instanceof \PHPUnit\Event\Code\TestMethod) { + if (! $firstTest instanceof TestMethod) { return null; } $path = $firstTest->testDox()->prettifiedClassName(); diff --git a/src/Logging/TeamCity/TeamCityLogger.php b/src/Logging/TeamCity/TeamCityLogger.php index 19a7be69..cbbf0347 100644 --- a/src/Logging/TeamCity/TeamCityLogger.php +++ b/src/Logging/TeamCity/TeamCityLogger.php @@ -200,7 +200,7 @@ final class TeamCityLogger public function testFinished(Finished $event): void { - if (! $this->time instanceof \PHPUnit\Event\Telemetry\HRTime) { + if (! $this->time instanceof HRTime) { throw ShouldNotHappen::fromMessage('Start time has not been set.'); } diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index 09974787..f235c748 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -9,6 +9,7 @@ use Closure; use Countable; use DateTimeInterface; use Error; +use Illuminate\Testing\TestResponse; use InvalidArgumentException; use JsonSerializable; use Pest\Exceptions\InvalidExpectationValue; @@ -842,7 +843,7 @@ final class Expectation is_object($this->value) && method_exists($this->value, 'toSnapshot') => $this->value->toSnapshot(), is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(), is_object($this->value) && method_exists($this->value, 'toString') => $this->value->toString(), - $this->value instanceof \Illuminate\Testing\TestResponse => $this->value->getContent(), // @phpstan-ignore-line + $this->value instanceof TestResponse => $this->value->getContent(), // @phpstan-ignore-line is_array($this->value) => json_encode($this->value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), $this->value instanceof Traversable => json_encode(iterator_to_array($this->value), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), $this->value instanceof JsonSerializable => json_encode($this->value->jsonSerialize(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), @@ -983,7 +984,7 @@ final class Expectation */ private function export(mixed $value): string { - if (! $this->exporter instanceof \Pest\Support\Exporter) { + if (! $this->exporter instanceof Exporter) { $this->exporter = Exporter::default(); } diff --git a/src/PendingCalls/Concerns/Describable.php b/src/PendingCalls/Concerns/Describable.php index 0208ea4b..cac2fb0b 100644 --- a/src/PendingCalls/Concerns/Describable.php +++ b/src/PendingCalls/Concerns/Describable.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Pest\PendingCalls\Concerns; +use Pest\Support\Description; + /** * @internal */ @@ -12,14 +14,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 08ebc15e..712aedcb 100644 --- a/src/PendingCalls/DescribeCall.php +++ b/src/PendingCalls/DescribeCall.php @@ -73,7 +73,7 @@ final class DescribeCall { $filename = Backtrace::file(); - if (! $this->currentBeforeEachCall instanceof \Pest\PendingCalls\BeforeEachCall) { + if (! $this->currentBeforeEachCall instanceof BeforeEachCall) { $this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename); $this->currentBeforeEachCall->describing[] = $this->description; diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index a65c8bb5..79264596 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -22,6 +22,10 @@ use Pest\Support\NullClosure; use Pest\Support\Str; use Pest\TestSuite; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\CoversFunction; +use PHPUnit\Framework\Attributes\CoversTrait; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; /** @@ -211,7 +215,7 @@ final class TestCall // @phpstan-ignore-line { foreach ($groups as $group) { $this->testCaseMethod->attributes[] = new Attribute( - \PHPUnit\Framework\Attributes\Group::class, + Group::class, [$group], ); } @@ -604,7 +608,7 @@ final class TestCall // @phpstan-ignore-line { foreach ($classes as $class) { $this->testCaseFactoryAttributes[] = new Attribute( - \PHPUnit\Framework\Attributes\CoversClass::class, + CoversClass::class, [$class], ); } @@ -627,7 +631,7 @@ final class TestCall // @phpstan-ignore-line { foreach ($traits as $trait) { $this->testCaseFactoryAttributes[] = new Attribute( - \PHPUnit\Framework\Attributes\CoversTrait::class, + CoversTrait::class, [$trait], ); } @@ -650,7 +654,7 @@ final class TestCall // @phpstan-ignore-line { foreach ($functions as $function) { $this->testCaseFactoryAttributes[] = new Attribute( - \PHPUnit\Framework\Attributes\CoversFunction::class, + CoversFunction::class, [$function], ); } diff --git a/src/Plugins/Parallel/Handlers/Laravel.php b/src/Plugins/Parallel/Handlers/Laravel.php index f634be56..30063d5f 100644 --- a/src/Plugins/Parallel/Handlers/Laravel.php +++ b/src/Plugins/Parallel/Handlers/Laravel.php @@ -7,6 +7,7 @@ namespace Pest\Plugins\Parallel\Handlers; use Closure; use Composer\InstalledVersions; use Illuminate\Testing\ParallelRunner; +use Orchestra\Testbench\TestCase; use ParaTest\Options; use ParaTest\RunnerInterface; use Pest\Contracts\Plugins\HandlesArguments; @@ -39,13 +40,13 @@ final class Laravel implements HandlesArguments * Executes the given closure when running Laravel. * * @param array $arguments - * @param CLosure(array): array $closure + * @param Closure(array): array $closure * @return array */ private function whenUsingLaravel(array $arguments, Closure $closure): array { $isLaravelApplication = InstalledVersions::isInstalled('laravel/framework', false); - $isLaravelPackage = class_exists(\Orchestra\Testbench\TestCase::class); + $isLaravelPackage = class_exists(TestCase::class); if ($isLaravelApplication && ! $isLaravelPackage) { return $closure($arguments); diff --git a/src/Support/Closure.php b/src/Support/Closure.php index e447903f..bf715780 100644 --- a/src/Support/Closure.php +++ b/src/Support/Closure.php @@ -19,14 +19,14 @@ final class Closure */ public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure { - if (! $closure instanceof \Closure) { + if (! $closure instanceof BaseClosure) { throw ShouldNotHappen::fromMessage('Could not bind null closure.'); } // @phpstan-ignore-next-line $closure = BaseClosure::bind($closure, $newThis, $newScope); - if (! $closure instanceof \Closure) { + if (! $closure instanceof BaseClosure) { throw ShouldNotHappen::fromMessage('Could not bind closure.'); } diff --git a/src/Support/Container.php b/src/Support/Container.php index 241825f5..d43c22be 100644 --- a/src/Support/Container.php +++ b/src/Support/Container.php @@ -28,7 +28,7 @@ final class Container */ public static function getInstance(): self { - if (! self::$instance instanceof \Pest\Support\Container) { + if (! self::$instance instanceof Container) { self::$instance = new self; } diff --git a/src/Support/Reflection.php b/src/Support/Reflection.php index d4f5f133..ac1a4273 100644 --- a/src/Support/Reflection.php +++ b/src/Support/Reflection.php @@ -8,6 +8,7 @@ use Closure; use InvalidArgumentException; use Pest\Exceptions\ShouldNotHappen; use Pest\TestSuite; +use PHPUnit\Framework\TestCase; use ReflectionClass; use ReflectionException; use ReflectionFunction; @@ -66,7 +67,7 @@ final class Reflection { $test = TestSuite::getInstance()->test; - if (! $test instanceof \PHPUnit\Framework\TestCase) { + if (! $test instanceof TestCase) { return self::bindCallable($callable); } @@ -221,7 +222,7 @@ final class Reflection { $getProperties = fn (ReflectionClass $reflectionClass): array => array_filter( array_map( - fn (ReflectionProperty $property): \ReflectionProperty => $property, + fn (ReflectionProperty $property): ReflectionProperty => $property, $reflectionClass->getProperties(), ), fn (ReflectionProperty $property): bool => $property->getDeclaringClass()->getName() === $reflectionClass->getName(), ); @@ -256,7 +257,7 @@ final class Reflection { $getMethods = fn (ReflectionClass $reflectionClass): array => array_filter( array_map( - fn (ReflectionMethod $method): \ReflectionMethod => $method, + fn (ReflectionMethod $method): ReflectionMethod => $method, $reflectionClass->getMethods($filter), ), fn (ReflectionMethod $method): bool => $method->getDeclaringClass()->getName() === $reflectionClass->getName(), ); diff --git a/tests/Autoload.php b/tests/Autoload.php index 2d3ce530..5bd3c23b 100644 --- a/tests/Autoload.php +++ b/tests/Autoload.php @@ -1,5 +1,7 @@ bar = 0; beforeAll(function () use ($foo) { diff --git a/tests/Features/Coverage.php b/tests/Features/Coverage.php index 974568e6..55ce4164 100644 --- a/tests/Features/Coverage.php +++ b/tests/Features/Coverage.php @@ -17,7 +17,7 @@ it('adds coverage if --coverage exist', function () { $arguments = $plugin->handleArguments(['--coverage']); expect($arguments)->toEqual(['--coverage-php', Coverage::getPath()]) ->and($plugin->coverage)->toBeTrue(); -})->skip(! \Pest\Support\Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available'); +})->skip(! Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available'); it('adds coverage if --min exist', function () { $plugin = new CoveragePlugin(new ConsoleOutput); diff --git a/tests/Features/Helpers.php b/tests/Features/Helpers.php index e32faffa..d2ea9485 100644 --- a/tests/Features/Helpers.php +++ b/tests/Features/Helpers.php @@ -39,7 +39,7 @@ it('allows to call underlying protected/private methods', function () { it('throws error if method do not exist', function () { test()->foo(); -})->throws(\ReflectionException::class, 'Call to undefined method PHPUnit\Framework\TestCase::foo()'); +})->throws(ReflectionException::class, 'Call to undefined method PHPUnit\Framework\TestCase::foo()'); it('can forward unexpected calls to any global function')->_assertThat(); diff --git a/tests/Helpers.php b/tests/Helpers.php index 7a7be690..f7870f38 100644 --- a/tests/Helpers.php +++ b/tests/Helpers.php @@ -1,7 +1,9 @@ use(Tests\CustomTestCase\CustomTestCase::class)->in(__DIR__); +use Tests\CustomTestCase\CustomTestCase; + +pest()->use(CustomTestCase::class)->in(__DIR__); test('closure was bound to CustomTestCase', function () { $this->assertCustomTrue(); diff --git a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php index 676e80f4..edce0fe6 100644 --- a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php +++ b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php @@ -1,5 +1,7 @@ printer(); - expect($theme)->toBeInstanceOf(Pest\Configuration\Printer::class); + expect($theme)->toBeInstanceOf(Printer::class); }); diff --git a/tests/Visual/Collision.php b/tests/Visual/Collision.php index 7812fc0d..99870cac 100644 --- a/tests/Visual/Collision.php +++ b/tests/Visual/Collision.php @@ -1,8 +1,10 @@ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'COLLISION_TEST' => true] diff --git a/tests/Visual/Help.php b/tests/Visual/Help.php index eea3fd99..35b25989 100644 --- a/tests/Visual/Help.php +++ b/tests/Visual/Help.php @@ -1,8 +1,10 @@ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); + $process = (new Process(['php', 'bin/pest', '--help'], null, ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); $process->run(); diff --git a/tests/Visual/Success.php b/tests/Visual/Success.php index d87ad304..16d49a38 100644 --- a/tests/Visual/Success.php +++ b/tests/Visual/Success.php @@ -1,5 +1,7 @@ 'integration', '--exclude-group' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], diff --git a/tests/Visual/TeamCity.php b/tests/Visual/TeamCity.php index c02ee21e..926f3957 100644 --- a/tests/Visual/TeamCity.php +++ b/tests/Visual/TeamCity.php @@ -1,5 +1,7 @@ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); + $process = (new Process(['php', 'bin/pest', '--version'], null, ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); $process->run(); From 9d17b872dd239f00128387b0c77d6e45a60d5726 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Tue, 10 Mar 2026 21:09:02 +0000 Subject: [PATCH 21/40] chore: update stubs --- stubs/init-laravel/Pest.php.stub | 4 +++- stubs/init/Pest.php.stub | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/stubs/init-laravel/Pest.php.stub b/stubs/init-laravel/Pest.php.stub index 60f04a45..38347589 100644 --- a/stubs/init-laravel/Pest.php.stub +++ b/stubs/init-laravel/Pest.php.stub @@ -1,5 +1,7 @@ extend(Tests\TestCase::class) +pest()->extend(TestCase::class) // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->in('Feature'); diff --git a/stubs/init/Pest.php.stub b/stubs/init/Pest.php.stub index b239048c..fbc6d9ae 100644 --- a/stubs/init/Pest.php.stub +++ b/stubs/init/Pest.php.stub @@ -1,5 +1,7 @@ extend(Tests\TestCase::class)->in('Feature'); +pest()->extend(TestCase::class)->in('Feature'); /* |-------------------------------------------------------------------------- From 5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Tue, 10 Mar 2026 21:09:12 +0000 Subject: [PATCH 22/40] release: v4.4.2 --- src/Pest.php | 2 +- .../Visual/Help/visual_snapshot_of_help_command_output.snap | 2 +- .../Visual/Version/visual_snapshot_of_help_command_output.snap | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Pest.php b/src/Pest.php index a66b7254..cf1c3a11 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '4.4.1'; + return '4.4.2'; } function testDirectory(string $file = ''): string 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 808b1613..cec9c17f 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 4.4.1. + Pest Testing Framework 4.4.2. USAGE: pest [options] 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 0160b757..36a5bb80 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 4.4.1. + Pest Testing Framework 4.4.2. From 1a4c06bd6ed07c2aee7e84111adb78aae0fed442 Mon Sep 17 00:00:00 2001 From: brianseymour Date: Wed, 18 Mar 2026 20:51:45 -0700 Subject: [PATCH 23/40] Fix Pest comment typo while still honoring the Otwellian Waterfall --- stubs/init-laravel/Pest.php.stub | 2 +- stubs/init/Pest.php.stub | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stubs/init-laravel/Pest.php.stub b/stubs/init-laravel/Pest.php.stub index 38347589..9d1d8d95 100644 --- a/stubs/init-laravel/Pest.php.stub +++ b/stubs/init-laravel/Pest.php.stub @@ -9,7 +9,7 @@ use Tests\TestCase; | | The closure you provide to your test functions is always bound to a specific PHPUnit test | case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may -| need to change it using the "pest()" function to bind a different classes or traits. +| need to change it using the "pest()" function to bind different classes or traits. | */ diff --git a/stubs/init/Pest.php.stub b/stubs/init/Pest.php.stub index fbc6d9ae..d4ffffba 100644 --- a/stubs/init/Pest.php.stub +++ b/stubs/init/Pest.php.stub @@ -9,7 +9,7 @@ use Tests\TestCase; | | The closure you provide to your test functions is always bound to a specific PHPUnit test | case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may -| need to change it using the "pest()" function to bind a different classes or traits. +| need to change it using the "pest()" function to bind different classes or traits. | */ From a753b41409fdf6e13996019202488370dc80e9a5 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Sat, 21 Mar 2026 13:14:35 +0000 Subject: [PATCH 24/40] chore: bumps phpunit --- composer.json | 6 +++--- src/Support/ExceptionTrace.php | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 846dc188..0c47bed4 100644 --- a/composer.json +++ b/composer.json @@ -18,19 +18,19 @@ ], "require": { "php": "^8.3.0", - "brianium/paratest": "^7.19.0", + "brianium/paratest": "^7.19.2", "nunomaduro/collision": "^8.9.1", "nunomaduro/termwind": "^2.4.0", "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.12", + "phpunit/phpunit": "^12.5.14", "symfony/process": "^7.4.5|^8.0.5" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.12", + "phpunit/phpunit": ">12.5.14", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, diff --git a/src/Support/ExceptionTrace.php b/src/Support/ExceptionTrace.php index 9d4132e2..92047840 100644 --- a/src/Support/ExceptionTrace.php +++ b/src/Support/ExceptionTrace.php @@ -26,6 +26,7 @@ final class ExceptionTrace return $closure(); } catch (Throwable $throwable) { if (Str::startsWith($message = $throwable->getMessage(), self::UNDEFINED_METHOD)) { + // @phpstan-ignore-next-line $class = preg_match('/^Call to undefined method ([^:]+)::/', $message, $matches) === false ? null : $matches[1]; $message = str_replace(self::UNDEFINED_METHOD, 'Call to undefined method ', $message); From e6ab897594312728ef2e32d586cb4f6780b1b495 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Sat, 21 Mar 2026 13:14:39 +0000 Subject: [PATCH 25/40] release: v4.4.3 --- src/Pest.php | 2 +- .../Visual/Help/visual_snapshot_of_help_command_output.snap | 2 +- .../Visual/Version/visual_snapshot_of_help_command_output.snap | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Pest.php b/src/Pest.php index cf1c3a11..3bf7581d 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '4.4.2'; + return '4.4.3'; } function testDirectory(string $file = ''): string 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 cec9c17f..cfeda92f 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 4.4.2. + Pest Testing Framework 4.4.3. USAGE: pest [options] 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 36a5bb80..56526ad9 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 4.4.2. + Pest Testing Framework 4.4.3. From 07737bc0b29aaa5103e3a45d3c1275ea5fe3db82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksandr=20=C5=A0t=C5=A1epelin?= Date: Wed, 25 Mar 2026 23:59:28 +0200 Subject: [PATCH 26/40] Fix parallel file selection and empty-suite reporting --- .../Parallel/Paratest/ResultPrinter.php | 8 ++ .../Parallel/Paratest/WrapperRunner.php | 55 +++++++++-- tests/.snapshots/success.txt | 5 +- tests/Visual/Parallel.php | 2 +- tests/Visual/ParallelNestedDatasets.php | 91 +++++++++++++++++++ 5 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 tests/Visual/ParallelNestedDatasets.php diff --git a/src/Plugins/Parallel/Paratest/ResultPrinter.php b/src/Plugins/Parallel/Paratest/ResultPrinter.php index e7a1c24d..85099a1b 100644 --- a/src/Plugins/Parallel/Paratest/ResultPrinter.php +++ b/src/Plugins/Parallel/Paratest/ResultPrinter.php @@ -171,6 +171,14 @@ final class ResultPrinter $state = (new StateGenerator)->fromPhpUnitTestResult($this->passedTests, $testResult); + if ($testResult->numberOfTestsRun() === 0 && $state->testSuiteTestsCount() === 0) { + $this->output->writeln([ + '', + ' INFO No tests found.', + '', + ]); + } + $this->compactPrinter->errors($state); $this->compactPrinter->recap($state, $testResult, $duration, $this->options); } diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 064856bc..ea22f4c5 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -39,6 +39,7 @@ use function dirname; use function file_get_contents; use function max; use function realpath; +use function str_starts_with; use function unlink; use function unserialize; use function usleep; @@ -485,14 +486,52 @@ final class WrapperRunner implements RunnerInterface private function getTestFiles(SuiteLoader $suiteLoader): array { /** @var array $files */ - $files = [ - ...array_values(array_filter( - $suiteLoader->tests, - fn (string $filename): bool => ! str_ends_with($filename, "eval()'d code") - )), - ...TestSuite::getInstance()->tests->getFilenames(), - ]; + $files = array_fill_keys(array_values(array_filter( + $suiteLoader->tests, + fn (string $filename): bool => ! str_ends_with($filename, "eval()'d code") + )), null); - return $files; // @phpstan-ignore-line + foreach (TestSuite::getInstance()->tests->getFilenames() as $filename) { + if ($this->shouldIncludeBootstrappedTestFile($filename)) { + $files[$filename] = null; + } + } + + return array_keys($files); // @phpstan-ignore-line + } + + private function shouldIncludeBootstrappedTestFile(string $filename): bool + { + if (! $this->options->configuration->hasCliArguments()) { + return true; + } + + $resolvedFilename = realpath($filename); + + if ($resolvedFilename === false) { + $resolvedFilename = realpath($this->options->cwd.DIRECTORY_SEPARATOR.$filename); + } + + if ($resolvedFilename === false) { + return false; + } + + foreach ($this->options->configuration->cliArguments() as $path) { + $resolvedPath = realpath($path); + + if ($resolvedPath === false) { + continue; + } + + if ($resolvedFilename === $resolvedPath) { + return true; + } + + if (is_dir($resolvedPath) && str_starts_with($resolvedFilename, $resolvedPath.DIRECTORY_SEPARATOR)) { + return true; + } + } + + return false; } } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 7b3d6f9b..d57d3687 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1757,6 +1757,9 @@ ✓ parallel ✓ a parallel test can extend another test with same name + PASS Tests\Visual\ParallelNestedDatasets + ✓ parallel reports missing nested datasets without a passing summary + PASS Tests\Visual\SingleTestOrDirectory ✓ allows to run a single test ✓ allows to run a directory @@ -1782,4 +1785,4 @@ ✓ 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 + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 35 skipped, 1189 passed (2819 assertions) diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index 1aced21d..cd5f62e1 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -21,5 +21,5 @@ test('parallel', function () use ($run) { })->skipOnWindows(); test('a parallel test can extend another test with same name', function () use ($run) { - expect($run('tests/Fixtures/Inheritance'))->toContain('Tests: 1 skipped, 2 passed (2 assertions)'); + expect($run('tests/Fixtures/Inheritance'))->toContain('Tests: 1 skipped, 1 passed (1 assertions)'); }); diff --git a/tests/Visual/ParallelNestedDatasets.php b/tests/Visual/ParallelNestedDatasets.php new file mode 100644 index 00000000..d2a7cbe6 --- /dev/null +++ b/tests/Visual/ParallelNestedDatasets.php @@ -0,0 +1,91 @@ +not->toBeEmpty(); +})->with('nested.users'); +PHP); + + return [$directory, 'tests/Features/ParallelNestedDatasetRepro/TestFileWithNestedDataset.php']; +}; + +$cleanup = function (string $directory): void { + if (! is_dir($directory)) { + return; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST, + ); + + foreach ($iterator as $item) { + if ($item->isDir()) { + rmdir($item->getPathname()); + } else { + unlink($item->getPathname()); + } + } + + rmdir($directory); +}; + +$run = function (string $target, bool $parallel = false): array { + $command = ['php', 'bin/pest', $target, '--colors=never']; + + if ($parallel) { + $command[] = '--parallel'; + $command[] = '--processes=2'; + } + + $process = new Process($command, dirname(__DIR__, 2), + ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], + ); + + $process->run(); + + return [ + 'exitCode' => $process->getExitCode(), + 'output' => removeAnsiEscapeSequences($process->getOutput().$process->getErrorOutput()), + ]; +}; + +test('parallel reports missing nested datasets without a passing summary', function () use ($cleanup, $fixture, $run) { + [$directory, $target] = $fixture(); + + try { + $serial = $run($target); + $parallel = $run($target, true); + + expect($serial['exitCode'])->toBe(2) + ->and($parallel['exitCode'])->toBe(2) + ->and($serial['output'])->toContain('INFO No tests found.') + ->and($parallel['output'])->toContain('INFO No tests found.') + ->and($parallel['output'])->toContain('Parallel: 2 processes') + ->and($parallel['output'])->not->toContain('passed'); + } finally { + $cleanup($directory); + } +})->skipOnWindows(); From f7175ecfd711d0f5113ba1a31c2ac3ee34eaf7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksandr=20=C5=A0t=C5=A1epelin?= Date: Wed, 25 Mar 2026 23:59:29 +0200 Subject: [PATCH 27/40] Fix parallel dataset reporting and nested fixtures --- src/Support/DatasetInfo.php | 61 +++++++++++++- src/Support/StateGenerator.php | 28 +++++++ tests/.snapshots/success.txt | 69 +++++---------- .../MissingDatasetTest.php | 5 ++ .../ParallelInvalidDataset/PassingTest.php | 3 + .../Datasets/Nested/Users.php | 6 ++ .../TestFileWithNestedDataset.php | 5 ++ tests/Unit/Support/DatasetInfo.php | 11 +++ tests/Visual/Parallel.php | 11 ++- tests/Visual/ParallelNestedDatasets.php | 83 ++++--------------- tests/Visual/Success.php | 4 +- 11 files changed, 165 insertions(+), 121 deletions(-) create mode 100644 tests/.tests/ParallelInvalidDataset/MissingDatasetTest.php create mode 100644 tests/.tests/ParallelInvalidDataset/PassingTest.php create mode 100644 tests/Fixtures/ParallelNestedDatasets/Datasets/Nested/Users.php create mode 100644 tests/Fixtures/ParallelNestedDatasets/TestFileWithNestedDataset.php diff --git a/src/Support/DatasetInfo.php b/src/Support/DatasetInfo.php index c67f317c..70ca6df5 100644 --- a/src/Support/DatasetInfo.php +++ b/src/Support/DatasetInfo.php @@ -17,7 +17,7 @@ final class DatasetInfo public static function isInsideADatasetsDirectory(string $file): bool { - return basename(dirname($file)) === self::DATASETS_DIR_NAME; + return in_array(self::DATASETS_DIR_NAME, self::directorySegmentsInsideTestsDirectory($file), true); } public static function isADatasetsFile(string $file): bool @@ -32,7 +32,23 @@ final class DatasetInfo } if (self::isInsideADatasetsDirectory($file)) { - return dirname($file, 2); + $scope = []; + + foreach (self::directorySegmentsInsideTestsDirectory($file) as $segment) { + if ($segment === self::DATASETS_DIR_NAME) { + break; + } + + $scope[] = $segment; + } + + $testsDirectoryPath = self::testsDirectoryPath($file); + + if ($scope === []) { + return $testsDirectoryPath; + } + + return $testsDirectoryPath.DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $scope); } if (self::isADatasetsFile($file)) { @@ -41,4 +57,45 @@ final class DatasetInfo return $file; } + + /** + * @return list + */ + private static function directorySegmentsInsideTestsDirectory(string $file): array + { + $directory = dirname(self::pathInsideTestsDirectory($file)); + + if ($directory === '.' || $directory === DIRECTORY_SEPARATOR) { + return []; + } + + return array_values(array_filter( + explode(DIRECTORY_SEPARATOR, trim($directory, DIRECTORY_SEPARATOR)), + static fn (string $segment): bool => $segment !== '', + )); + } + + private static function pathInsideTestsDirectory(string $file): string + { + $testsDirectory = DIRECTORY_SEPARATOR.trim(testDirectory(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; + $position = strrpos($file, $testsDirectory); + + if ($position === false) { + return $file; + } + + return substr($file, $position + strlen($testsDirectory)); + } + + private static function testsDirectoryPath(string $file): string + { + $testsDirectory = DIRECTORY_SEPARATOR.trim(testDirectory(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; + $position = strrpos($file, $testsDirectory); + + if ($position === false) { + return dirname($file); + } + + return substr($file, 0, $position + strlen($testsDirectory) - 1); + } } diff --git a/src/Support/StateGenerator.php b/src/Support/StateGenerator.php index a7ddba1a..3226343e 100644 --- a/src/Support/StateGenerator.php +++ b/src/Support/StateGenerator.php @@ -11,6 +11,10 @@ use PHPUnit\Event\Code\TestDoxBuilder; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Code\ThrowableBuilder; use PHPUnit\Event\Test\Errored; +use PHPUnit\Event\Test\PhpunitDeprecationTriggered; +use PHPUnit\Event\Test\PhpunitErrorTriggered; +use PHPUnit\Event\Test\PhpunitNoticeTriggered; +use PHPUnit\Event\Test\PhpunitWarningTriggered; use PHPUnit\Event\TestData\TestDataCollection; use PHPUnit\Framework\SkippedWithMessageException; use PHPUnit\Metadata\MetadataCollection; @@ -43,6 +47,8 @@ final class StateGenerator )); } + $this->addTriggeredPhpunitEvents($state, $testResult->testTriggeredPhpunitErrorEvents(), TestResult::FAIL); + foreach ($testResult->testMarkedIncompleteEvents() as $testResultEvent) { $state->add(TestResult::fromPestParallelTestCase( $testResultEvent->test(), @@ -99,6 +105,8 @@ final class StateGenerator } } + $this->addTriggeredPhpunitEvents($state, $testResult->testTriggeredPhpunitDeprecationEvents(), TestResult::DEPRECATED); + foreach ($testResult->notices() as $testResultEvent) { foreach ($testResultEvent->triggeringTests() as $triggeringTest) { ['test' => $test] = $triggeringTest; @@ -123,6 +131,8 @@ final class StateGenerator } } + $this->addTriggeredPhpunitEvents($state, $testResult->testTriggeredPhpunitNoticeEvents(), TestResult::NOTICE); + foreach ($testResult->warnings() as $testResultEvent) { foreach ($testResultEvent->triggeringTests() as $triggeringTest) { ['test' => $test] = $triggeringTest; @@ -135,6 +145,8 @@ final class StateGenerator } } + $this->addTriggeredPhpunitEvents($state, $testResult->testTriggeredPhpunitWarningEvents(), TestResult::WARN); + foreach ($testResult->phpWarnings() as $testResultEvent) { foreach ($testResultEvent->triggeringTests() as $triggeringTest) { ['test' => $test] = $triggeringTest; @@ -165,4 +177,20 @@ final class StateGenerator return $state; } + + /** + * @param array> $testResultEvents + */ + private function addTriggeredPhpunitEvents(State $state, array $testResultEvents, string $type): void + { + foreach ($testResultEvents as $events) { + foreach ($events as $event) { + $state->add(TestResult::fromPestParallelTestCase( + $event->test(), + $type, + ThrowableBuilder::from(new TestOutcome($event->message())) + )); + } + } + } } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index d57d3687..58d988ac 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1490,6 +1490,10 @@ PASS Tests\Fixtures\ExampleTest ✓ it example 2 + PASS Tests\Fixtures\ParallelNestedDatasets\TestFileWithNestedDataset + ✓ loads nested dataset with ('alice') + ✓ loads nested dataset with ('bob') + WARN Tests\Fixtures\UnexpectedOutput - output @@ -1640,9 +1644,14 @@ ✓ it cannot resolve a parameter without type PASS Tests\Unit\Support\DatasetInfo - ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datase…rs.php', true) + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/Datasets/project/tes…rs.php', true) #1 + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/Datasets/project/tes…rs.php', true) #2 + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datase…rs.php', true) #1 + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datase…rs.php', true) #2 ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datasets.php', false) - ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…rs.php', true) + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…rs.php', true) #1 + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…rs.php', true) #2 + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…rs.php', true) #3 ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…rs.php', false) ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…ts.php', false) ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Datase…rs.php', false) @@ -1650,12 +1659,18 @@ ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur…rs.php', false) #1 ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur…rs.php', false) #2 ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur…ts.php', true) - ✓ it computes the dataset scope with ('/var/www/project/tests/Datase…rs.php', '/var/www/project/tests') + ✓ it computes the dataset scope with ('/var/www/Datasets/project/tes…rs.php', '/var/www/Datasets/project/tests') + ✓ it computes the dataset scope with ('/var/www/Datasets/project/tes…rs.php', '/var/www/Datasets/project/tes…atures') + ✓ it computes the dataset scope with ('/var/www/project/tests/Datase…rs.php', '/var/www/project/tests') #1 + ✓ it computes the dataset scope with ('/var/www/project/tests/Datase…rs.php', '/var/www/project/tests') #2 ✓ it computes the dataset scope with ('/var/www/project/tests/Datasets.php', '/var/www/project/tests') - ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Features') + ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Features') #1 + ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Features') #2 ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Featur…rs.php') #1 ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…ts.php', '/var/www/project/tests/Features') - ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Featur…ollers') + ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Featur…ollers') #1 + ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Featur…ollers') #2 + ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Features') #3 ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Featur…rs.php') #2 ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…ts.php', '/var/www/project/tests/Featur…ollers') @@ -1739,50 +1754,8 @@ ✓ it alerts users about tests with arguments but no input ✓ it can return an array of all test suite filenames - PASS Tests\Visual\BeforeEachTestName - ✓ description - ✓ latest description - - PASS Tests\Visual\Collision - ✓ collision with (['']) - - PASS Tests\Visual\Help - ✓ visual snapshot of help command output - - WARN Tests\Visual\JUnit - ✓ junit output - - junit with parallel → Not working yet - - PASS Tests\Visual\Parallel - ✓ parallel - ✓ a parallel test can extend another test with same name - - PASS Tests\Visual\ParallelNestedDatasets - ✓ parallel reports missing nested datasets without a passing summary - - PASS Tests\Visual\SingleTestOrDirectory - ✓ allows to run a single test - ✓ allows to run a directory - ✓ it disable decorating printer when colors is set to never - - WARN Tests\Visual\Success - - visual snapshot of test suite on success - - WARN Tests\Visual\TeamCity - - visual snapshot of team city with ('Failure.php') - - visual snapshot of team city with ('SuccessOnly.php') - - WARN Tests\Visual\Todo - - todos - - todos in parallel - - todo - - todo in parallel - - WARN Tests\Visual\Version - - visual snapshot of help command output - 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, 1189 passed (2819 assertions) + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 26 skipped, 1191 passed (2802 assertions) \ No newline at end of file diff --git a/tests/.tests/ParallelInvalidDataset/MissingDatasetTest.php b/tests/.tests/ParallelInvalidDataset/MissingDatasetTest.php new file mode 100644 index 00000000..c7a1245f --- /dev/null +++ b/tests/.tests/ParallelInvalidDataset/MissingDatasetTest.php @@ -0,0 +1,5 @@ +toBe('x'); +})->with('missing.dataset'); diff --git a/tests/.tests/ParallelInvalidDataset/PassingTest.php b/tests/.tests/ParallelInvalidDataset/PassingTest.php new file mode 100644 index 00000000..915336ab --- /dev/null +++ b/tests/.tests/ParallelInvalidDataset/PassingTest.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/Fixtures/ParallelNestedDatasets/Datasets/Nested/Users.php b/tests/Fixtures/ParallelNestedDatasets/Datasets/Nested/Users.php new file mode 100644 index 00000000..dbff16da --- /dev/null +++ b/tests/Fixtures/ParallelNestedDatasets/Datasets/Nested/Users.php @@ -0,0 +1,6 @@ +not->toBeEmpty(); +})->with('nested.users'); diff --git a/tests/Unit/Support/DatasetInfo.php b/tests/Unit/Support/DatasetInfo.php index b00c7b94..4feb6121 100644 --- a/tests/Unit/Support/DatasetInfo.php +++ b/tests/Unit/Support/DatasetInfo.php @@ -5,9 +5,14 @@ use Pest\Support\DatasetInfo; it('can check if dataset is defined inside a Datasets directory', function (string $file, bool $inside) { expect(DatasetInfo::isInsideADatasetsDirectory($file))->toBe($inside); })->with([ + ['file' => '/var/www/Datasets/project/tests/Datasets/Nested/Numbers.php', 'inside' => true], + ['file' => '/var/www/Datasets/project/tests/Features/Datasets/Nested/Numbers.php', 'inside' => true], ['file' => '/var/www/project/tests/Datasets/Numbers.php', 'inside' => true], + ['file' => '/var/www/project/tests/Datasets/Nested/Numbers.php', 'inside' => true], ['file' => '/var/www/project/tests/Datasets.php', 'inside' => false], ['file' => '/var/www/project/tests/Features/Datasets/Numbers.php', 'inside' => true], + ['file' => '/var/www/project/tests/Features/Datasets/Nested/Numbers.php', 'inside' => true], + ['file' => '/var/www/project/tests/Features/Datasets/Nested/Datasets/Numbers.php', 'inside' => true], ['file' => '/var/www/project/tests/Features/Numbers.php', 'inside' => false], ['file' => '/var/www/project/tests/Features/Datasets.php', 'inside' => false], ]); @@ -25,12 +30,18 @@ it('can check if dataset is defined inside a Datasets.php file', function (strin it('computes the dataset scope', function (string $file, string $scope) { expect(DatasetInfo::scope($file))->toBe($scope); })->with([ + ['file' => '/var/www/Datasets/project/tests/Datasets/Nested/Numbers.php', 'scope' => '/var/www/Datasets/project/tests'], + ['file' => '/var/www/Datasets/project/tests/Features/Datasets/Nested/Numbers.php', 'scope' => '/var/www/Datasets/project/tests/Features'], ['file' => '/var/www/project/tests/Datasets/Numbers.php', 'scope' => '/var/www/project/tests'], + ['file' => '/var/www/project/tests/Datasets/Nested/Numbers.php', 'scope' => '/var/www/project/tests'], ['file' => '/var/www/project/tests/Datasets.php', 'scope' => '/var/www/project/tests'], ['file' => '/var/www/project/tests/Features/Datasets/Numbers.php', 'scope' => '/var/www/project/tests/Features'], + ['file' => '/var/www/project/tests/Features/Datasets/Nested/Numbers.php', 'scope' => '/var/www/project/tests/Features'], ['file' => '/var/www/project/tests/Features/Numbers.php', 'scope' => '/var/www/project/tests/Features/Numbers.php'], ['file' => '/var/www/project/tests/Features/Datasets.php', 'scope' => '/var/www/project/tests/Features'], ['file' => '/var/www/project/tests/Features/Controllers/Datasets/Numbers.php', 'scope' => '/var/www/project/tests/Features/Controllers'], + ['file' => '/var/www/project/tests/Features/Controllers/Datasets/Nested/Numbers.php', 'scope' => '/var/www/project/tests/Features/Controllers'], + ['file' => '/var/www/project/tests/Features/Datasets/Nested/Datasets/Numbers.php', 'scope' => '/var/www/project/tests/Features'], ['file' => '/var/www/project/tests/Features/Controllers/Numbers.php', 'scope' => '/var/www/project/tests/Features/Controllers/Numbers.php'], ['file' => '/var/www/project/tests/Features/Controllers/Datasets.php', 'scope' => '/var/www/project/tests/Features/Controllers'], ]); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index cd5f62e1..9824a405 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -16,10 +16,17 @@ $run = function () { test('parallel', function () use ($run) { expect($run('--exclude-group=integration')) - ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 26 skipped, 1177 passed (2789 assertions)') + ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 3 notices, 39 todos, 26 skipped, 1190 passed (2802 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); test('a parallel test can extend another test with same name', function () use ($run) { expect($run('tests/Fixtures/Inheritance'))->toContain('Tests: 1 skipped, 1 passed (1 assertions)'); -}); +})->skipOnWindows(); + +test('parallel reports invalid datasets as failures', function () use ($run) { + expect($run('tests/.tests/ParallelInvalidDataset')) + ->toContain("A dataset with the name `missing.dataset` does not exist. You can create it using `dataset('missing.dataset', ['a', 'b']);`.") + ->toContain('Tests: 1 failed, 1 passed (1 assertions)') + ->toContain('Parallel: 3 processes'); +})->skipOnWindows(); diff --git a/tests/Visual/ParallelNestedDatasets.php b/tests/Visual/ParallelNestedDatasets.php index d2a7cbe6..37052556 100644 --- a/tests/Visual/ParallelNestedDatasets.php +++ b/tests/Visual/ParallelNestedDatasets.php @@ -2,58 +2,13 @@ use Symfony\Component\Process\Process; -$fixture = function (): array { - $directory = dirname(__DIR__).DIRECTORY_SEPARATOR.'Features'.DIRECTORY_SEPARATOR.'ParallelNestedDatasetRepro'; - $datasetsDirectory = $directory.DIRECTORY_SEPARATOR.'Datasets'.DIRECTORY_SEPARATOR.'Nested'; - $target = $directory.DIRECTORY_SEPARATOR.'TestFileWithNestedDataset.php'; - - if (! is_dir($datasetsDirectory)) { - mkdir($datasetsDirectory, 0777, true); - } - - file_put_contents($datasetsDirectory.DIRECTORY_SEPARATOR.'Users.php', <<<'PHP' -not->toBeEmpty(); -})->with('nested.users'); -PHP); - - return [$directory, 'tests/Features/ParallelNestedDatasetRepro/TestFileWithNestedDataset.php']; -}; - -$cleanup = function (string $directory): void { - if (! is_dir($directory)) { - return; - } - - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST, - ); - - foreach ($iterator as $item) { - if ($item->isDir()) { - rmdir($item->getPathname()); - } else { - unlink($item->getPathname()); - } - } - - rmdir($directory); -}; - -$run = function (string $target, bool $parallel = false): array { - $command = ['php', 'bin/pest', $target, '--colors=never']; +$run = function (bool $parallel = false): array { + $command = [ + 'php', + 'bin/pest', + 'tests/Fixtures/ParallelNestedDatasets/TestFileWithNestedDataset.php', + '--colors=never', + ]; if ($parallel) { $command[] = '--parallel'; @@ -72,20 +27,14 @@ $run = function (string $target, bool $parallel = false): array { ]; }; -test('parallel reports missing nested datasets without a passing summary', function () use ($cleanup, $fixture, $run) { - [$directory, $target] = $fixture(); +test('parallel loads nested datasets from nested directories', function () use ($run) { + $serial = $run(); + $parallel = $run(true); - try { - $serial = $run($target); - $parallel = $run($target, true); - - expect($serial['exitCode'])->toBe(2) - ->and($parallel['exitCode'])->toBe(2) - ->and($serial['output'])->toContain('INFO No tests found.') - ->and($parallel['output'])->toContain('INFO No tests found.') - ->and($parallel['output'])->toContain('Parallel: 2 processes') - ->and($parallel['output'])->not->toContain('passed'); - } finally { - $cleanup($directory); - } + expect($serial['exitCode'])->toBe(0) + ->and($parallel['exitCode'])->toBe(0) + ->and($serial['output'])->toContain('Tests: 2 passed (2 assertions)') + ->and($parallel['output'])->toContain('Tests: 2 passed (2 assertions)') + ->and($parallel['output'])->toContain('Parallel: 2 processes') + ->and($parallel['output'])->not->toContain('No tests found.'); })->skipOnWindows(); diff --git a/tests/Visual/Success.php b/tests/Visual/Success.php index 16d49a38..20ebd1d0 100644 --- a/tests/Visual/Success.php +++ b/tests/Visual/Success.php @@ -12,9 +12,9 @@ test('visual snapshot of test suite on success', function () { $output = function () use ($testsPath) { $process = (new Process( - ['php', 'bin/pest'], + ['php', '-d', 'memory_limit=512M', 'bin/pest', '--exclude-group=integration'], dirname($testsPath), - ['EXCLUDE' => 'integration', '--exclude-group' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], + ['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], )); $process->run(); From 4b50cb486d123687a199fff859230ae002915185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksandr=20=C5=A0t=C5=A1epelin?= Date: Wed, 25 Mar 2026 23:59:29 +0200 Subject: [PATCH 28/40] Restore success snapshot coverage with lower memory limit --- tests/.snapshots/success.txt | 45 +++++++++++++++++++++++++++++++++++- tests/Visual/Success.php | 4 ++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 58d988ac..49efa6bb 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1754,8 +1754,51 @@ ✓ it alerts users about tests with arguments but no input ✓ it can return an array of all test suite filenames + PASS Tests\Visual\BeforeEachTestName + ✓ description + ✓ latest description + + PASS Tests\Visual\Collision + ✓ collision with (['']) + + PASS Tests\Visual\Help + ✓ visual snapshot of help command output + + WARN Tests\Visual\JUnit + ✓ junit output + - junit with parallel → Not working yet + + PASS Tests\Visual\Parallel + ✓ parallel + ✓ a parallel test can extend another test with same name + ✓ parallel reports invalid datasets as failures + + PASS Tests\Visual\ParallelNestedDatasets + ✓ parallel loads nested datasets from nested directories + + PASS Tests\Visual\SingleTestOrDirectory + ✓ allows to run a single test + ✓ allows to run a directory + ✓ it disable decorating printer when colors is set to never + + WARN Tests\Visual\Success + - visual snapshot of test suite on success + + WARN Tests\Visual\TeamCity + - visual snapshot of team city with ('Failure.php') + - visual snapshot of team city with ('SuccessOnly.php') + + WARN Tests\Visual\Todo + - todos + - todos in parallel + - todo + - todo in parallel + + WARN Tests\Visual\Version + - visual snapshot of help command output + 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, 26 skipped, 1191 passed (2802 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 35 skipped, 1203 passed (2835 assertions) \ No newline at end of file diff --git a/tests/Visual/Success.php b/tests/Visual/Success.php index 20ebd1d0..7906378f 100644 --- a/tests/Visual/Success.php +++ b/tests/Visual/Success.php @@ -12,9 +12,9 @@ test('visual snapshot of test suite on success', function () { $output = function () use ($testsPath) { $process = (new Process( - ['php', '-d', 'memory_limit=512M', 'bin/pest', '--exclude-group=integration'], + ['php', '-d', 'memory_limit=256M', 'bin/pest'], dirname($testsPath), - ['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], + ['EXCLUDE' => 'integration', '--exclude-group' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], )); $process->run(); From a08755538302b39f65d8c8641f35f42bfad2da73 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Thu, 26 Mar 2026 14:30:03 +0000 Subject: [PATCH 29/40] bump: dependencies --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0c47bed4..db2c1957 100644 --- a/composer.json +++ b/composer.json @@ -58,7 +58,7 @@ "pestphp/pest-dev-tools": "^4.1.0", "pestphp/pest-plugin-browser": "^4.3.0", "pestphp/pest-plugin-type-coverage": "^4.0.3", - "psy/psysh": "^0.12.21" + "psy/psysh": "^0.12.22" }, "minimum-stability": "dev", "prefer-stable": true, From 6c42e7f4ea877a555389c735a7cf14adc2027730 Mon Sep 17 00:00:00 2001 From: "Gal Jakic (We Wow Web)" Date: Wed, 1 Apr 2026 16:48:14 +0200 Subject: [PATCH 30/40] Laravel Pint default formatting applied to Pest-php.stub --- stubs/init-laravel/Pest.php.stub | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stubs/init-laravel/Pest.php.stub b/stubs/init-laravel/Pest.php.stub index 38347589..cb09b7f3 100644 --- a/stubs/init-laravel/Pest.php.stub +++ b/stubs/init-laravel/Pest.php.stub @@ -1,5 +1,6 @@ extend(TestCase::class) - // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) + // ->use(RefreshDatabase::class) ->in('Feature'); /* From 3d2ebdb27321e42f8a425b39ea945c7eecf96ccb Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Fri, 3 Apr 2026 11:59:54 +0100 Subject: [PATCH 31/40] bump: dependencies --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index db2c1957..afdead31 100644 --- a/composer.json +++ b/composer.json @@ -18,19 +18,19 @@ ], "require": { "php": "^8.3.0", - "brianium/paratest": "^7.19.2", - "nunomaduro/collision": "^8.9.1", + "brianium/paratest": "^7.20.0", + "nunomaduro/collision": "^8.9.2", "nunomaduro/termwind": "^2.4.0", "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.14", - "symfony/process": "^7.4.5|^8.0.5" + "phpunit/phpunit": "^12.5.16", + "symfony/process": "^7.4.5|^8.0.8" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.14", + "phpunit/phpunit": ">12.5.16", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, From ce05ee9aad5672c21d50c7f2db6683727687771b Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Fri, 3 Apr 2026 12:00:04 +0100 Subject: [PATCH 32/40] release: v4.4.4 --- src/Pest.php | 2 +- .../Visual/Help/visual_snapshot_of_help_command_output.snap | 4 ++-- .../Version/visual_snapshot_of_help_command_output.snap | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Pest.php b/src/Pest.php index 3bf7581d..8595bd5f 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '4.4.3'; + return '4.4.4'; } function testDirectory(string $file = ''): string 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 cfeda92f..1b8c0dcc 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 4.4.3. + Pest Testing Framework 4.4.4. USAGE: pest [options] @@ -92,7 +92,7 @@ --random-order-seed [N] Use the specified random seed when running tests in random order REPORTING OPTIONS: - --colors [flag] ......... Use colors in output ("never", "auto" or "always") + --colors=[flag] ......... Use colors in output ("never", "auto" or "always") --columns [n] ................. Number of columns to use for progress output --columns max ............ Use maximum number of columns for progress output --stderr ................................. Write to STDERR instead of STDOUT 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 56526ad9..988eec06 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 4.4.3. + Pest Testing Framework 4.4.4. From c1a54df233a3ff3c19a03473a500d01da1feeb9d Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Fri, 3 Apr 2026 14:04:20 +0100 Subject: [PATCH 33/40] feat: `--ai` work in progress --- composer.json | 2 +- src/Plugins/Concerns/HandleArguments.php | 27 +++++++++++++++++++ src/Plugins/Help.php | 7 +++++ src/Repositories/TestRepository.php | 10 +++++++ ...isual_snapshot_of_help_command_output.snap | 3 +++ 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index afdead31..a3691452 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "pestphp/pest-plugin-mutate": "^4.0.1", "pestphp/pest-plugin-profanity": "^4.2.1", "phpunit/phpunit": "^12.5.16", - "symfony/process": "^7.4.5|^8.0.8" + "symfony/process": "^7.4.8|^8.0.8" }, "conflict": { "filp/whoops": "<2.18.3", diff --git a/src/Plugins/Concerns/HandleArguments.php b/src/Plugins/Concerns/HandleArguments.php index 9cf5e606..6fdcff8c 100644 --- a/src/Plugins/Concerns/HandleArguments.php +++ b/src/Plugins/Concerns/HandleArguments.php @@ -56,4 +56,31 @@ trait HandleArguments return array_values(array_flip($arguments)); } + + /** + * Pops the given argument and its value from the arguments, returning the value. + * + * @param array $arguments + */ + public function popArgumentValue(string $argument, array &$arguments): ?string + { + foreach ($arguments as $key => $value) { + if (str_contains($value, "$argument=")) { + unset($arguments[$key]); + $arguments = array_values($arguments); + + return substr($value, strlen($argument) + 1); + } + + if ($value === $argument && isset($arguments[$key + 1])) { + $result = $arguments[$key + 1]; + unset($arguments[$key], $arguments[$key + 1]); + $arguments = array_values($arguments); + + return $result; + } + } + + return null; + } } diff --git a/src/Plugins/Help.php b/src/Plugins/Help.php index 096f2914..a2fb1ef0 100644 --- a/src/Plugins/Help.php +++ b/src/Plugins/Help.php @@ -107,6 +107,13 @@ final readonly class Help implements HandlesArguments 'desc' => 'Initialise a standard Pest configuration', ]], ...$content['Configuration']]; + $content['AI'] = [ + [ + 'arg' => '--ai', + 'desc' => 'Run a code snippet as a fully scaffolded test for AI verification', + ], + ]; + $content['Execution'] = [...[ [ 'arg' => '--parallel', diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index 466d3226..cfce2cf1 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -113,6 +113,16 @@ final class TestRepository $this->testCaseMethodFilters[] = $filter; } + /** + * Gets the class and traits configured for the given directory path. + * + * @return array + */ + public function getUsesForPath(string $path): array + { + return $this->uses[$path][0] ?? []; + } + /** * Gets the test case factory from the given filename. */ 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 1b8c0dcc..75439fc3 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 @@ -147,6 +147,9 @@ --disable-coverage-ignore ...... Disable metadata for ignoring code coverage --no-coverage Ignore code coverage reporting configured in the XML configuration file + AI OPTIONS: + --ai ..... Run a code snippet as a fully scaffolded test for AI verification + MUTATION TESTING OPTIONS: --mutate .... Runs mutation testing, to understand the quality of your tests --mutate --parallel ...................... Runs mutation testing in parallel From 9797a71dbc776f46d6fcacb708b002755da6f37a Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Fri, 3 Apr 2026 14:43:28 +0100 Subject: [PATCH 34/40] feat(ai): allow temporary namesapce --- src/Factories/TestCaseFactory.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 3349d03d..4599ee77 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -59,6 +59,11 @@ final class TestCaseFactory Concerns\Expectable::class, ]; + /** + * The namespace for the test case, overrides the path-based namespace when set. + */ + public ?string $namespace = null; + /** * Creates a new Factory instance. */ @@ -127,7 +132,7 @@ final class TestCaseFactory $partsFQN = explode('\\', $classFQN); $className = array_pop($partsFQN); - $namespace = implode('\\', $partsFQN); + $namespace = $this->namespace ?? implode('\\', $partsFQN); $baseClass = sprintf('\%s', $this->class); if (trim($className) === '') { From e44c554a0b0f4bbeee7beb7e497118a33b6a0a6f Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Mon, 6 Apr 2026 21:57:05 +0100 Subject: [PATCH 35/40] chore: bumps dependencies --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index a3691452..7ef9486e 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "require": { "php": "^8.3.0", "brianium/paratest": "^7.20.0", - "nunomaduro/collision": "^8.9.2", + "nunomaduro/collision": "^8.9.3", "nunomaduro/termwind": "^2.4.0", "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", @@ -57,7 +57,7 @@ "require-dev": { "pestphp/pest-dev-tools": "^4.1.0", "pestphp/pest-plugin-browser": "^4.3.0", - "pestphp/pest-plugin-type-coverage": "^4.0.3", + "pestphp/pest-plugin-type-coverage": "^4.0.4", "psy/psysh": "^0.12.22" }, "minimum-stability": "dev", From d9d46c73f8d1f4edb449d889632e3d8c3cd2172d Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Thu, 9 Apr 2026 21:36:49 +0100 Subject: [PATCH 36/40] chore: stores statically the result --- src/Plugins/Parallel/Paratest/WrapperRunner.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 064856bc..b853c65f 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -51,6 +51,11 @@ final class WrapperRunner implements RunnerInterface /** * The time to sleep between cycles. */ + /** + * The merged test result from the parallel run. + */ + public static ?TestResult $result = null; + private const int CYCLE_SLEEP = 10000; /** @@ -386,6 +391,8 @@ final class WrapperRunner implements RunnerInterface $testResultSum->numberOfIssuesIgnoredByBaseline(), ); + self::$result = $testResultSum; + if ($this->options->configuration->cacheResult()) { $resultCacheSum = new DefaultResultCache($this->options->configuration->testResultCacheFile()); foreach ($this->resultCacheFiles as $resultCacheFile) { From e766825f5b49e11561ce87f62155d6ae2615e319 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Fri, 10 Apr 2026 12:15:00 +0100 Subject: [PATCH 37/40] chore: fixes `test:unit` --- composer.json | 2 +- src/Expectation.php | 12 ++++-------- tests/.snapshots/success.txt | 10 +++++++++- .../HasInheritedTrait/ChildClassExtendingParent.php | 4 +--- .../Arch/ToUseTrait/HasNestedTrait/NestedTrait.php | 2 +- .../ToUseTrait/HasTrait/ParentClassWithTrait.php | 2 +- .../ToUseTrait/HasTrait/TestTraitForInheritance.php | 2 +- tests/Visual/Parallel.php | 2 +- 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 7ef9486e..159f053e 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ }, "require-dev": { "pestphp/pest-dev-tools": "^4.1.0", - "pestphp/pest-plugin-browser": "^4.3.0", + "pestphp/pest-plugin-browser": "^4.3.1", "pestphp/pest-plugin-type-coverage": "^4.0.4", "psy/psysh": "^0.12.22" }, diff --git a/src/Expectation.php b/src/Expectation.php index 5d7666cb..908f5954 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -684,23 +684,19 @@ final class Expectation $realPath = realpath($object->path); - foreach (Composer::userNamespaces() as $directory => $namespace) { + foreach (Composer::userNamespacesWithDirectories() as $directory => $namespace) { if (str_starts_with($realPath, $directory)) { $relativePath = substr($realPath, strlen($directory) + 1); $relativePath = explode('.', $relativePath)[0]; - $classFromPath = $namespace . '\\' . str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath); + $classFromPath = $namespace.'\\'.str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath); - if ($classFromPath === $object->reflectionClass->getName()) { - return true; - } - - return false; + return $classFromPath === $object->reflectionClass->getName(); } } return false; }, - "to be cased correctly", + 'to be cased correctly', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 49efa6bb..a994dcc0 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -448,6 +448,10 @@ ✓ failures with custom message ✓ not failures + PASS Tests\Features\Expect\toBeCasedCorrectly + ✓ pass + ✓ failure + PASS Tests\Features\Expect\toBeDigits ✓ pass ✓ failures @@ -1034,6 +1038,10 @@ ✓ pass ✓ failures ✓ not failures + ✓ trait inheritance - direct usage + ✓ trait inheritance - inherited usage + ✓ trait inheritance - negative case + ✓ nested trait inheritance PASS Tests\Features\Expect\unless ✓ it pass @@ -1801,4 +1809,4 @@ ✓ 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, 1203 passed (2835 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 35 skipped, 1209 passed (2842 assertions) \ No newline at end of file diff --git a/tests/Fixtures/Arch/ToUseTrait/HasInheritedTrait/ChildClassExtendingParent.php b/tests/Fixtures/Arch/ToUseTrait/HasInheritedTrait/ChildClassExtendingParent.php index df7ce9ec..c4672451 100644 --- a/tests/Fixtures/Arch/ToUseTrait/HasInheritedTrait/ChildClassExtendingParent.php +++ b/tests/Fixtures/Arch/ToUseTrait/HasInheritedTrait/ChildClassExtendingParent.php @@ -6,6 +6,4 @@ namespace Tests\Fixtures\Arch\ToUseTrait\HasInheritedTrait; use Tests\Fixtures\Arch\ToUseTrait\HasTrait\ParentClassWithTrait; -class ChildClassExtendingParent extends ParentClassWithTrait -{ -} \ No newline at end of file +class ChildClassExtendingParent extends ParentClassWithTrait {} diff --git a/tests/Fixtures/Arch/ToUseTrait/HasNestedTrait/NestedTrait.php b/tests/Fixtures/Arch/ToUseTrait/HasNestedTrait/NestedTrait.php index 61352270..370e01b7 100644 --- a/tests/Fixtures/Arch/ToUseTrait/HasNestedTrait/NestedTrait.php +++ b/tests/Fixtures/Arch/ToUseTrait/HasNestedTrait/NestedTrait.php @@ -10,4 +10,4 @@ trait NestedTrait { return 'nested'; } -} \ No newline at end of file +} diff --git a/tests/Fixtures/Arch/ToUseTrait/HasTrait/ParentClassWithTrait.php b/tests/Fixtures/Arch/ToUseTrait/HasTrait/ParentClassWithTrait.php index f2d24171..81163ba6 100644 --- a/tests/Fixtures/Arch/ToUseTrait/HasTrait/ParentClassWithTrait.php +++ b/tests/Fixtures/Arch/ToUseTrait/HasTrait/ParentClassWithTrait.php @@ -7,4 +7,4 @@ namespace Tests\Fixtures\Arch\ToUseTrait\HasTrait; class ParentClassWithTrait { use TestTraitForInheritance; -} \ No newline at end of file +} diff --git a/tests/Fixtures/Arch/ToUseTrait/HasTrait/TestTraitForInheritance.php b/tests/Fixtures/Arch/ToUseTrait/HasTrait/TestTraitForInheritance.php index 0dc95376..4386a06b 100644 --- a/tests/Fixtures/Arch/ToUseTrait/HasTrait/TestTraitForInheritance.php +++ b/tests/Fixtures/Arch/ToUseTrait/HasTrait/TestTraitForInheritance.php @@ -14,4 +14,4 @@ trait TestTraitForInheritance { return 'test'; } -} \ No newline at end of file +} diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index 9824a405..68b3ee1f 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -16,7 +16,7 @@ $run = function () { test('parallel', function () use ($run) { expect($run('--exclude-group=integration')) - ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 3 notices, 39 todos, 26 skipped, 1190 passed (2802 assertions)') + ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 3 notices, 39 todos, 26 skipped, 1196 passed (2809 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); From 75938ac9eb4dd39dbd9dc0b3a8b361d5debc96a8 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Fri, 10 Apr 2026 12:18:28 +0100 Subject: [PATCH 38/40] ci: updates deps --- .github/workflows/static.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index ae45ea68..d7fa2a71 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -40,7 +40,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: static-php-8.3-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f74b76e..f6e1b65b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,7 +45,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json') }} From b71bfc513a595865b2fbe76de9325c9d65942531 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Fri, 10 Apr 2026 12:23:49 +0100 Subject: [PATCH 39/40] chore: guards --- src/Factories/TestCaseFactory.php | 4 ++-- src/Plugins/Parallel/Paratest/WrapperRunner.php | 16 ++++++++++++---- src/Support/StateGenerator.php | 4 ++++ tests/.tests/StraßenTest.php | 3 +++ 4 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 tests/.tests/StraßenTest.php diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 4599ee77..6f120c27 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -116,8 +116,8 @@ final class TestCaseFactory $relativePath = (string) preg_replace('|%[a-fA-F0-9][a-fA-F0-9]|', '', $relativePath); // Remove escaped quote sequences (maintain namespace) $relativePath = str_replace(array_map(fn (string $quote): string => sprintf('\\%s', $quote), ['\'', '"']), '', $relativePath); - // Limit to A-Z, a-z, 0-9, '_', '-'. - $relativePath = (string) preg_replace('/[^A-Za-z0-9\\\\]/', '', $relativePath); + // Limit to Unicode letters and numbers. + $relativePath = (string) preg_replace('/[^\p{L}\p{N}\\\\]/u', '', $relativePath); $classFQN = 'P\\'.$relativePath; diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 69be6032..331b0c7d 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -492,15 +492,23 @@ final class WrapperRunner implements RunnerInterface */ private function getTestFiles(SuiteLoader $suiteLoader): array { - /** @var array $files */ - $files = array_fill_keys(array_values(array_filter( + /** @var array $files */ + $files = []; + + foreach (array_filter( $suiteLoader->tests, fn (string $filename): bool => ! str_ends_with($filename, "eval()'d code") - )), null); + ) as $filename) { + $resolved = realpath($filename) ?: $filename; + $files[$resolved] = null; + } foreach (TestSuite::getInstance()->tests->getFilenames() as $filename) { if ($this->shouldIncludeBootstrappedTestFile($filename)) { - $files[$filename] = null; + $resolved = realpath($filename) + ?: realpath($this->options->cwd.DIRECTORY_SEPARATOR.$filename) + ?: $filename; + $files[$resolved] = null; } } diff --git a/src/Support/StateGenerator.php b/src/Support/StateGenerator.php index 3226343e..9872f52d 100644 --- a/src/Support/StateGenerator.php +++ b/src/Support/StateGenerator.php @@ -185,6 +185,10 @@ final class StateGenerator { foreach ($testResultEvents as $events) { foreach ($events as $event) { + if (! $event->test()->isTestMethod()) { + continue; + } + $state->add(TestResult::fromPestParallelTestCase( $event->test(), $type, diff --git a/tests/.tests/StraßenTest.php b/tests/.tests/StraßenTest.php new file mode 100644 index 00000000..682d1062 --- /dev/null +++ b/tests/.tests/StraßenTest.php @@ -0,0 +1,3 @@ +assertTrue(true); From 9085561ece334da8e23a64b0493eb710ca71afbf Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Fri, 10 Apr 2026 12:24:30 +0100 Subject: [PATCH 40/40] chore: runs at `9am` --- .github/workflows/static.yml | 3 ++- .github/workflows/tests.yml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index d7fa2a71..6f5ee1bd 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -5,7 +5,7 @@ on: branches: [4.x] pull_request: schedule: - - cron: '0 0 * * *' + - cron: '0 9 * * *' concurrency: group: static-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -17,6 +17,7 @@ jobs: name: Static Tests runs-on: ubuntu-latest + timeout-minutes: 15 strategy: fail-fast: true matrix: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f6e1b65b..41abc6a1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,8 @@ on: push: branches: [4.x] pull_request: + schedule: + - cron: '0 9 * * *' concurrency: group: tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -14,6 +16,7 @@ jobs: if: github.event_name != 'schedule' || github.repository == 'pestphp/pest' runs-on: ${{ matrix.os }} + timeout-minutes: 15 strategy: fail-fast: true matrix: