Compare commits

...

62 Commits
v4.0.1 ... 4.x

Author SHA1 Message Date
f7015fe59c chore: adjusts sponsors 2026-02-24 10:44:48 +00:00
7281e0ded7 Add SerpApi to the list of sponsors
Add SerpApi as a sponsor in the README.
2026-02-20 01:18:43 +00:00
5de8693e3b chore: style 2026-02-17 16:15:58 +00:00
7d80f1d20e chore: removes non used files 2026-02-17 15:34:32 +00:00
b3119cc120 Merge pull request #1562 from michaelw85/patch-1
[Fix] Pass test dir to worker
2026-02-17 15:33:16 +00:00
4e294edf76 Merge pull request #1639 from imliam/patch-1
[Laravel Preset] Allow App\Http to be used in providers
2026-02-17 15:31:01 +00:00
f96a1b2786 release: v4.4.1 2026-02-17 15:27:18 +00:00
a49cf7edc5 ci: speed up ci 2026-02-17 15:21:20 +00:00
b0f6a74cb6 ci: makes jobs faster 2026-02-17 15:18:33 +00:00
aaa226f6a6 chore: tests against symfony 8 2026-02-17 15:14:45 +00:00
69cb752d02 chore: bumps dependencies 2026-02-17 15:01:37 +00:00
cf00e58b7d chore: bumps dependencies 2026-02-17 11:22:04 +00:00
1f39b28e2c Allow App\Http to be used in providers 2026-02-16 00:25:47 +01:00
9fcbca69d4 Update README.md 2026-02-13 10:41:22 +00:00
3a4329ddc7 release: 4.3.2 2026-01-28 01:01:19 +00:00
dd01229d7b Merge pull request #1606 from smirok/teamcity-fix-for-describe-tests-with-dataset
fix: replace `substr` with `mb_substr` in Str::beforeLast to ensure multibyte string compatibility and correct TeamCity test names for datasets in "describe" blocks
2026-01-15 01:52:01 +00:00
c7e4efcea4 fix: replace substr with mb_substr in Str::beforeLast to ensure multibyte string compatibility and correct TeamCity test names for datasets in "describe" blocks 2026-01-14 00:55:35 +01:00
df3205e814 Merge pull request #1554 from pindab0ter/feature/extend-closure-this
Specify closure this for extend
2026-01-13 01:19:47 +00:00
bc57a84e77 release: 4.3.1 2026-01-04 11:29:59 -05:00
bc39830d8a chore: removes toHaveSuspiciousCharacters from php preset 2026-01-04 11:25:57 -05:00
3a566b100e docs: why php 2026-01-04 11:04:03 -05:00
9fe61e0e56 docs: update sponsors
Removed and updated sponsor links in the README.
2026-01-03 18:07:02 -05:00
e86bec3e68 release: 4.3.0 2025-12-30 14:48:33 -05:00
58b8f3cc5d Merge pull request #1598 from Willem-Jaap/willem-jaap/pest-only-file-level
feat: add pest only function to mark each test in a file as only
2025-12-30 14:40:04 -05:00
c157b661f2 style: format 2025-12-30 09:26:35 +01:00
be90610f17 feat: add pest only function to mark each test in a file as only 2025-12-30 09:24:05 +01:00
1701a306c3 Merge pull request #1590 from pestphp/feature/intl-exception
feat: show more useful exception when `intl` extension not found
2025-12-29 22:09:16 -05:00
064ab3fc2e Merge pull request #1595 from bilboque/dirty-flag-not-found-in-help
feat: add --dirty documentation in --help
2025-12-29 22:08:12 -05:00
leo
44e315df98 feat: add --dirty documentation in --help 2025-12-22 11:02:28 +01:00
62694c14b9 chore: style 2025-12-15 11:54:24 +00:00
7c43c1c583 Merge pull request #1586 from jackbayliss/bump-checkout-action
[4.x] Bump checkout version from 5 to 6
2025-12-15 11:49:28 +00:00
6a96aed654 feat: adds phpunit@12.5 support 2025-12-15 11:48:43 +00:00
b1c997a869 feat: show more useful exception when intl extension not found 2025-12-12 12:02:00 +00:00
b4172e2c2e bump checkout version from 5 to 6 2025-12-10 14:08:06 +00:00
ae419afd36 chore: support for symfony 8.0.0 components 2025-11-28 12:04:48 +00:00
27aa305897 Merge pull request #1576 from Chris53897/feature/ci
ci: bump actions/checkout 4 => 5
2025-11-25 11:18:28 +00:00
f5820bd670 release: 4.1.5 2025-11-24 12:46:38 +00:00
41fd831153 release: 4.1.4 2025-11-24 10:25:45 +00:00
51340439e8 ci: bump actions/checkout 4 => 5 2025-11-22 12:12:15 +01:00
1a39826935 ci: tests against php 8.5 2025-11-20 02:59:27 +00:00
ae1da79ac1 Pass test dir to worker
#1444 
Test directory argument is lost when spawning workers, add it again.
2025-11-05 17:15:51 +01:00
00990efc97 Merge pull request #1544 from Se7en-RU/fix-testdox-columns-warning
Fix Undefined array key "testdox-columns" warning
2025-11-04 07:42:57 +00:00
477d20a54f release: 4.1.3 2025-10-29 22:45:27 +00:00
b7b16096db Specify closure this for extend 2025-10-29 11:20:08 +01:00
4105e33c39 Fix Undefined array key "testdox-columns" warning
Fix Undefined array key "testdox-columns" warning
2025-10-21 11:44:55 +03:00
08b09f2e98 release: 4.1.2 2025-10-05 20:09:49 +01:00
b0fab7e437 chore: uses phpunit@12.4 2025-10-05 20:09:42 +01:00
8e3444e1db chore: bumps requirements 2025-10-01 14:30:25 +01:00
fc7a4182b5 adjusts sponsors 2025-09-18 20:13:01 +01:00
b7406938ac release: v4.1.0 2025-09-10 14:41:09 +01:00
314caabd1d chore: improves types 2025-09-10 14:41:02 +01:00
65cabf91b1 chore: bumps dependencies 2025-09-10 14:40:52 +01:00
f91c6c1e1e Update social media links in Thanks.php 2025-09-01 00:09:31 +01:00
843dbbf18a Update README.md 2025-08-31 23:50:16 +01:00
47fb1d7763 release: v4.0.4 2025-08-28 19:19:42 +01:00
639df4cb43 chore: uses phpunit 12.3.7 2025-08-28 19:19:32 +01:00
e54e4a0178 release: 4.0.3 2025-08-24 15:17:23 +01:00
7749775f50 chore: uses phpunit 12.3.6 2025-08-24 15:17:19 +01:00
f11f3aa0a4 Merge pull request #1464 from gehrisandro/sandro/prevent-duplicate-attributes
Prevent duplicate attributes
2025-08-23 22:43:15 +01:00
33817013fe Prevent duplicate attributes 2025-08-23 12:35:32 +02:00
7f11ace329 release: 4.0.2 2025-08-22 11:34:24 +01:00
3d776f1f20 fix: revert reading coverage by chunks 2025-08-22 11:12:55 +01:00
71 changed files with 166 additions and 204 deletions

View File

@ -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'
@ -19,7 +24,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
@ -29,8 +34,22 @@ 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
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
- name: Profanity Check
run: composer test:profanity

View File

@ -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'
@ -13,15 +18,18 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest] # windows-latest
symfony: ['7.3']
php: ['8.3', '8.4']
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 }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
@ -31,6 +39,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"

View File

@ -1,14 +0,0 @@
# Well documented Makefiles
DEFAULT_GOAL := help
help:
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\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

View File

@ -5,6 +5,7 @@
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
<a href="https://whyphp.dev"><img src="https://img.shields.io/badge/Why_PHP-in_2026-7A86E8?style=flat-square&labelColor=18181b" alt="Why PHP in 2026"></a>
</p>
</p>
@ -16,8 +17,8 @@
- Explore our docs at **[pestphp.com »](https://pestphp.com)**
- Follow the creator Nuno Maduro:
- YouTube: **[youtube.com/@nunomaduro](https://www.youtube.com/@nunomaduro)** — Videos every weekday
- Twitch: **[twitch.tv/enunomaduro](https://www.twitch.tv/enunomaduro)** — Streams (almost) every weekday
- YouTube: **[youtube.com/@nunomaduro](https://youtube.com/@nunomaduro)** — Videos every week
- Twitch: **[twitch.tv/nunomaduro](https://twitch.tv/nunomaduro)** — Live coding on Mondays, Wednesdays, and Fridays at 9PM UTC
- Twitter / X: **[x.com/enunomaduro](https://x.com/enunomaduro)**
- LinkedIn: **[linkedin.com/in/nunomaduro](https://www.linkedin.com/in/nunomaduro)**
- Instagram: **[instagram.com/enunomaduro](https://www.instagram.com/enunomaduro)**
@ -30,23 +31,23 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
### Platinum Sponsors
- **[Laracasts](https://laracasts.com/?ref=pestphp)**
- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
- **[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)**
### Gold Sponsors
- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
- **[NativePHP](https://nativephp.com/mobile?ref=pestphp.com)**
- **[CMS Max](https://cmsmax.com/?ref=pestphp)**
### Premium Sponsors
- [Zapiet](https://zapiet.com/?ref=pestphp)
- [Load Forge](https://loadforge.com/?ref=pestphp)
- [Route4Me](https://route4me.com/pt?ref=pestphp)
- [Nerdify](https://getnerdify.com/?ref=pestphp)
- [Akaunting](https://akaunting.com/?ref=pestphp)
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
- [Localazy](https://localazy.com/?ref=pestphp)
- [Forge](https://forge.laravel.com/?ref=pestphp)
- [Route4Me](https://www.route4me.com/?ref=pestphp)
- [Spatie](https://spatie.be/?ref=pestphp)
- [Worksome](https://www.worksome.com/?ref=pestphp)
- [Zapiet](https://www.zapiet.com/?ref=pestphp)
- [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)**.

View File

@ -86,7 +86,7 @@ $bootPest = (static function (): void {
$getopt['teamcity-file'] ?? null,
$getopt['testdox-file'] ?? null,
isset($getopt['testdox-color']),
(int) $getopt['testdox-columns'] ?? null,
(int) ($getopt['testdox-columns'] ?? null),
);
while (true) {

View File

@ -18,19 +18,19 @@
],
"require": {
"php": "^8.3.0",
"brianium/paratest": "^7.11.2",
"nunomaduro/collision": "^8.8.2",
"nunomaduro/termwind": "^2.3.1",
"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.0.1",
"phpunit/phpunit": "^12.3.5",
"symfony/process": "^7.3.0"
"pestphp/pest-plugin-profanity": "^4.2.1",
"phpunit/phpunit": "^12.5.12",
"symfony/process": "^7.4.5|^8.0.5"
},
"conflict": {
"filp/whoops": "<2.18.3",
"phpunit/phpunit": ">12.3.5",
"phpunit/phpunit": ">12.5.12",
"sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0"
},
@ -55,10 +55,10 @@
]
},
"require-dev": {
"pestphp/pest-dev-tools": "^4.0.0",
"pestphp/pest-plugin-browser": "^4.0.2",
"pestphp/pest-plugin-type-coverage": "^4.0.2",
"psy/psysh": "^0.12.10"
"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"
},
"minimum-stability": "dev",
"prefer-stable": true,

View File

@ -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"]

View File

@ -1,98 +0,0 @@
<?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use const PHP_EOL;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
use function dirname;
use function serialize;
use function str_contains;
final class PHP
{
public function process(CodeCoverage $coverage, ?string $target = null): string
{
$coverage->clearCache();
$buffer = "<?php return \unserialize(<<<'END_OF_COVERAGE_SERIALIZATION'".PHP_EOL.serialize($coverage).PHP_EOL.'END_OF_COVERAGE_SERIALIZATION'.PHP_EOL.');';
if ($target !== null) {
if (! str_contains($target, '://')) {
Filesystem::createDirectory(dirname($target));
}
if (! is_writable(dirname($target))) {
throw new WriteOperationFailedException($target);
}
$fp = @fopen($target, 'wb');
if (! $fp) {
throw new WriteOperationFailedException($target);
}
$chunkSize = 1024 * 1024 * 8;
$offset = 0;
$total = strlen($buffer);
while ($offset < $total) {
$written = @fwrite($fp, substr($buffer, $offset, $chunkSize));
if ($written === false) {
fclose($fp);
throw new WriteOperationFailedException($target);
}
$offset += $written;
}
fclose($fp);
}
return $buffer;
}
}

View File

@ -2,7 +2,10 @@
declare(strict_types=1);
use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector;
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector;
use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
return RectorConfig::configure()
@ -12,6 +15,9 @@ return RectorConfig::configure()
->withSkip([
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
ReturnNeverTypeRector::class,
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
NarrowObjectReturnTypeRector::class,
RemoveParentDelegatingConstructorRector::class,
])
->withPreparedSets(
deadCode: true,

View File

@ -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']);

View File

@ -4,9 +4,6 @@ declare(strict_types=1);
namespace Pest\ArchPresets;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/**
* @internal
*/
@ -92,9 +89,5 @@ final class Php extends AbstractPreset
'xdebug_var_dump',
'trap',
])->not->toBeUsed();
$this->eachUserNamespace(
fn (Expectation $namespace): ArchExpectation => $namespace->not->toHaveSuspiciousCharacters(),
);
}
}

View File

@ -83,7 +83,7 @@ final class BootFiles implements Bootstrapper
private function bootDatasets(string $testsPath): void
{
assert(strlen($testsPath) > 0);
assert($testsPath !== '');
$files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');

View File

@ -26,7 +26,6 @@ final class BootOverrides implements Bootstrapper
'TextUI/TestSuiteFilterProcessor.php',
'Event/Value/ThrowableBuilder.php',
'Logging/JUnit/JunitXmlLogger.php',
'Report/PHP.php',
];
/**

View File

@ -8,6 +8,8 @@ use Closure;
/**
* @internal
*
* @template T of object
*/
trait Extendable
{
@ -20,6 +22,8 @@ trait Extendable
/**
* Register a new extend.
*
* @param-closure-this T $extend
*/
public function extend(string $name, Closure $extend): void
{

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest;
use Pest\PendingCalls\BeforeEachCall;
use Pest\PendingCalls\UsesCall;
/**
@ -62,6 +63,14 @@ final readonly class Configuration
return (new UsesCall($this->filename, []))->group(...$groups);
}
/**
* Marks all tests in the current file to be run exclusively.
*/
public function only(): void
{
(new BeforeEachCall(TestSuite::getInstance(), $this->filename))->only();
}
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/

View File

@ -25,8 +25,8 @@ final readonly class Thanks
private const array FUNDING_MESSAGES = [
'Star' => 'https://github.com/pestphp/pest',
'YouTube' => 'https://youtube.com/@nunomaduro',
'TikTok' => 'https://tiktok.com/@nunomaduro',
'Twitch' => 'https://twitch.tv/enunomaduro',
'TikTok' => 'https://tiktok.com/@enunomaduro',
'Twitch' => 'https://twitch.tv/nunomaduro',
'LinkedIn' => 'https://linkedin.com/in/nunomaduro',
'Instagram' => 'https://instagram.com/enunomaduro',
'X' => 'https://x.com/enunomaduro',

View File

@ -52,7 +52,9 @@ use ReflectionProperty;
*/
final class Expectation
{
/** @use Extendable<self<TValue>> */
use Extendable;
use Pipeable;
use Retrievable;
@ -397,7 +399,7 @@ final class Expectation
*
* @return Expectation<TValue>|OppositeExpectation<TValue>|EachExpectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue|null>|TValue
*/
public function __get(string $name)
public function __get(string $name): mixed
{
if (! self::hasMethod($name)) {
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $name)) {

View File

@ -15,6 +15,7 @@ use Pest\Arch\PendingArchExpectation;
use Pest\Arch\SingleArchExpectation;
use Pest\Arch\Support\FileLineFinder;
use Pest\Exceptions\InvalidExpectation;
use Pest\Exceptions\MissingDependency;
use Pest\Expectation;
use Pest\Support\Arr;
use Pest\Support\Exporter;
@ -284,6 +285,10 @@ final readonly class OppositeExpectation
*/
public function toHaveSuspiciousCharacters(): ArchExpectation
{
if (! class_exists(Spoofchecker::class)) {
throw new MissingDependency(__FUNCTION__, 'ext-intl >= 2.0');
}
$checker = new Spoofchecker;
/** @var Expectation<array<int, string>|string> $original */

View File

@ -71,7 +71,7 @@ final readonly class Kernel
$output,
);
register_shutdown_function(fn () => $kernel->shutdown());
register_shutdown_function($kernel->shutdown(...));
foreach (self::BOOTSTRAPPERS as $bootstrapper) {
$bootstrapper = Container::getInstance()->get($bootstrapper);

View File

@ -131,7 +131,7 @@ final readonly class Converter
// clean the paths of each frame.
$frames = array_map(
fn (string $frame): string => $this->toRelativePath($frame),
$this->toRelativePath(...),
$frames
);

View File

@ -759,7 +759,12 @@ final class TestCall // @phpstan-ignore-line
$this->testSuite->tests->set($this->testCaseMethod);
if (! is_null($testCase = $this->testSuite->tests->get($this->filename))) {
$testCase->attributes = array_merge($testCase->attributes, $this->testCaseFactoryAttributes);
$attributesToMerge = array_filter(
$this->testCaseFactoryAttributes,
fn (Attribute $attributeToMerge): bool => array_filter($testCase->attributes, fn (Attribute $attribute): bool => serialize($attributeToMerge) === serialize($attribute)) === []
);
$testCase->attributes = array_merge($testCase->attributes, $attributesToMerge);
}
}
}

View File

@ -6,7 +6,7 @@ namespace Pest;
function version(): string
{
return '4.0.1';
return '4.4.1';
}
function testDirectory(string $file = ''): string

View File

@ -99,6 +99,7 @@ final readonly class Help implements HandlesArguments
{
$helpReflection = new PHPUnitHelp;
// @phpstan-ignore-next-line
$content = (fn (): array => $this->elements())->call($helpReflection);
$content['Configuration'] = [...[[
@ -141,6 +142,9 @@ final readonly class Help implements HandlesArguments
], [
'arg' => '--retry',
'desc' => 'Run non-passing tests first and stop execution upon first error or failure',
], [
'arg' => '--dirty',
'desc' => 'Only run tests that have uncommitted changes according to Git',
], ...$content['Selection']];
$content['Reporting'] = [...$content['Reporting'], ...[

View File

@ -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;

View File

@ -67,11 +67,11 @@ final class DatasetsRepository
}
/**
* @return Closure|array<int|string, mixed>
* @return array<int|string, mixed>
*
* @throws ShouldNotHappen
*/
public static function get(string $filename, string $description): Closure|array // @phpstan-ignore-line
public static function get(string $filename, string $description): array // @phpstan-ignore-line
{
$dataset = self::$withs[$filename.self::SEPARATOR.$description];
@ -191,6 +191,7 @@ final class DatasetsRepository
return str_starts_with($currentTestFile, $datasetScope);
}, ARRAY_FILTER_USE_KEY);
/** @var string|null $closestScopeDatasetKey */
$closestScopeDatasetKey = array_reduce(
array_keys($matchingDatasets),
fn (string|int|null $keyA, string|int|null $keyB): string|int|null => $keyA !== null && strlen((string) $keyA) > strlen((string) $keyB) ? $keyA : $keyB

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Pest\Support;
use Pest\Exceptions\ShouldNotHappen;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\Directory;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\Environment\Runtime;
@ -87,20 +88,10 @@ final class Coverage
throw ShouldNotHappen::fromMessage(sprintf('Coverage not found in path: %s.', $reportPath));
}
$handle = fopen($reportPath, 'r');
$code = '';
while (is_resource($handle) && ! feof($handle)) {
$code .= fread($handle, 8192);
}
if (is_resource($handle)) {
fclose($handle);
}
/** @var CodeCoverage $codeCoverage */
$codeCoverage = require $reportPath;
unlink($reportPath);
$codeCoverage = eval(substr($code, 5));
$totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines();
/** @var Directory<File|Directory> $report */

View File

@ -46,6 +46,7 @@ final readonly class HigherOrderCallables
*/
public function and(mixed $value): Expectation
{
// @phpstan-ignore-next-line
return $this->expect($value);
}

View File

@ -31,10 +31,8 @@ final class HigherOrderTapProxy
/**
* Dynamically pass properties gets to the target.
*
* @return mixed
*/
public function __get(string $property)
public function __get(string $property): mixed
{
if (property_exists($this->target, $property)) {
return $this->target->{$property};

View File

@ -79,7 +79,7 @@ final class Str
return $subject;
}
return substr($subject, 0, $pos);
return mb_substr($subject, 0, $pos);
}
/**

View File

@ -19,7 +19,7 @@ test('pass with dataset', function ($data) {
[$filename] = TestSuite::getInstance()->snapshots->get();
expect($filename)->toStartWith('tests/.pest/snapshots-external/')
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap')
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot();
})->with(['my-datas-set-value']);
@ -29,7 +29,7 @@ describe('within describe', function () {
[$filename] = TestSuite::getInstance()->snapshots->get();
expect($filename)->toStartWith('tests/.pest/snapshots-external/')
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap')
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot();
});
})->with(['my-datas-set-value']);

View File

@ -0,0 +1,7 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Snapshot</h1>
</div>
</div>
</div>

View File

@ -0,0 +1,7 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Snapshot</h1>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
Pest Testing Framework 4.0.1.
Pest Testing Framework 4.4.1.
USAGE: pest <file> [options]
@ -27,6 +27,8 @@
--pr .... Output to standard output tests with the given pull request number
--pull-request Output to standard output tests with the given pull request number (alias for --pr)
--retry Run non-passing tests first and stop execution upon first error or failure
--dirty ...... Only run tests that have uncommitted changes according to Git
--all .................... Ignore test selection from XML configuration file
--list-suites ................................... List available test suites
--testsuite [name] ......... Only run tests from the specified test suite(s)
--exclude-testsuite [name] .. Exclude tests from the specified test suite(s)
@ -138,6 +140,7 @@
--only-summary-for-coverage-text Option for code coverage report in text format: only show summary
--show-uncovered-for-coverage-text Option for code coverage report in text format: show uncovered files
--coverage-xml [dir] . Write code coverage report in XML format to directory
--exclude-source-from-xml-coverage Exclude [source] element from code coverage report in XML format
--warm-coverage-cache ........................... Warm static analysis cache
--coverage-filter [dir] ........... Include [dir] in code coverage reporting
--path-coverage .......... Report path coverage in addition to line coverage

View File

@ -1,3 +1,3 @@
Pest Testing Framework 4.0.1.
Pest Testing Framework 4.4.1.

View File

@ -1,15 +1,19 @@
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234']
##teamcity[testCount count='3' flowId='1234']
##teamcity[testCount count='4' flowId='1234']
##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234']
##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234']
##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234']
##teamcity[testFinished name='can also pass' duration='100000' flowId='1234']
##teamcity[testSuiteStarted name='can pass with dataset' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset' flowId='1234']
##teamcity[testStarted name='can pass with dataset with data set "@(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "@(true)"' flowId='1234']
##teamcity[testFinished name='can pass with dataset with data set "@(true)"' duration='100000' flowId='1234']
##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234']
##teamcity[testFinished name='can pass with dataset with data set "(true)"' duration='100000' flowId='1234']
##teamcity[testSuiteFinished name='can pass with dataset' flowId='1234']
##teamcity[testSuiteStarted name='`block` → can pass with dataset in describe block' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block' flowId='1234']
##teamcity[testStarted name='`block` → can pass with dataset in describe block with data set "(1)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block with data set "(1)"' flowId='1234']
##teamcity[testFinished name='`block` → can pass with dataset in describe block with data set "(1)"' duration='100000' flowId='1234']
##teamcity[testSuiteFinished name='`block` → can pass with dataset in describe block' flowId='1234']
##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234']
Tests: 3 passed (3 assertions)
Tests: 4 passed (4 assertions)
Duration: 1.00s

View File

@ -1782,4 +1782,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 (2814 assertions)
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 35 skipped, 1188 passed (2813 assertions)

View File

@ -13,3 +13,9 @@ test('can also pass', function () {
test('can pass with dataset', function ($value) {
expect($value)->toEqual(true);
})->with([true]);
describe('block', function () {
test('can pass with dataset in describe block', function ($number) {
expect($number)->toBeInt();
})->with([1]);
});

View File

@ -75,7 +75,7 @@ test('pass with dataset', function ($data) {
[$filename] = TestSuite::getInstance()->snapshots->get();
expect($filename)->toStartWith('tests/.pest/snapshots/')
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap')
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot();
})->with(['my-datas-set-value']);
@ -85,7 +85,7 @@ describe('within describe', function () {
[$filename] = TestSuite::getInstance()->snapshots->get();
expect($filename)->toStartWith('tests/.pest/snapshots/')
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap')
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot();
});
})->with(['my-datas-set-value']);

View File

@ -36,8 +36,8 @@ test('junit output', function () use ($normalizedPath, $run) {
expect($result['testsuite']['@attributes'])
->name->toBe('Tests\tests\SuccessOnly')
->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php'))
->tests->toBe('3')
->assertions->toBe('3')
->tests->toBe('4')
->assertions->toBe('4')
->errors->toBe('0')
->failures->toBe('0')
->skipped->toBe('0');

View File

@ -16,7 +16,7 @@ $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, 1178 passed (2790 assertions)')
->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 26 skipped, 1177 passed (2789 assertions)')
->toContain('Parallel: 3 processes');
})->skipOnWindows();