Compare commits

..

27 Commits
v4.3.1 ... 5.x

Author SHA1 Message Date
fa27c8daef chore: version 2026-02-17 17:52:40 +00:00
f0a08f0503 chore: missing types 2026-02-17 17:52:00 +00:00
2c040c5b1f chore: style 2026-02-17 17:45:50 +00:00
a9ce1fd739 chore: code refactor 2026-02-17 17:45:34 +00:00
3533356262 chore: updates snapshots 2026-02-17 17:44:56 +00:00
4aa41d0b14 chore: bumps dependencies 2026-02-17 17:41:38 +00:00
e4ed60085c chore: bumps dependencies 2026-02-17 17:18:45 +00:00
e2b119655d chore: point pestphp dependencies to ^5.0.0 2026-02-17 17:13:36 +00:00
fcf5baf0a9 chore: start preparing for pest 5.x 2026-02-17 16:55:03 +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
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
b7b16096db Specify closure this for extend 2025-10-29 11:20:08 +01:00
29 changed files with 109 additions and 112 deletions

View File

@ -2,10 +2,15 @@ name: Static Analysis
on:
push:
branches: [5.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'
@ -24,13 +29,27 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
php-version: 8.4
tools: composer:v2
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.4-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: |
static-php-8.4-${{ matrix.dependency-version }}-composer-
static-php-8.4-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: [5.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,8 +18,8 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest] # windows-latest
symfony: ['7.3']
php: ['8.3', '8.4', '8.5']
symfony: ['8.0']
php: ['8.4', '8.5']
dependency_version: [prefer-stable]
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
@ -31,6 +36,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

@ -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)**.

View File

@ -17,20 +17,20 @@
}
],
"require": {
"php": "^8.3.0",
"brianium/paratest": "^7.16.0",
"nunomaduro/collision": "^8.8.3",
"nunomaduro/termwind": "^2.3.3",
"pestphp/pest-plugin": "^4.0.0",
"pestphp/pest-plugin-arch": "^4.0.0",
"pestphp/pest-plugin-mutate": "^4.0.1",
"pestphp/pest-plugin-profanity": "^4.2.1",
"phpunit/phpunit": "^12.5.4",
"symfony/process": "^7.4.3|^8.0.0"
"php": "^8.4",
"brianium/paratest": "^7.19.0",
"nunomaduro/collision": "^8.9.1",
"nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^5.0.0",
"pestphp/pest-plugin-arch": "^5.0.0",
"pestphp/pest-plugin-mutate": "^5.0.0",
"pestphp/pest-plugin-profanity": "^5.0.0",
"phpunit/phpunit": "^13.0.3",
"symfony/process": "^8.0.5"
},
"conflict": {
"filp/whoops": "<2.18.3",
"phpunit/phpunit": ">12.5.4",
"phpunit/phpunit": ">13.0.3",
"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.1.1",
"pestphp/pest-plugin-type-coverage": "^4.0.3",
"psy/psysh": "^0.12.18"
"pestphp/pest-dev-tools": "^5.0.0",
"pestphp/pest-plugin-browser": "^5.0.0",
"pestphp/pest-plugin-type-coverage": "^5.0.0",
"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

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

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

@ -33,7 +33,7 @@ final readonly class Configuration
*/
public function in(string ...$targets): UsesCall
{
return (new UsesCall($this->filename, []))->in(...$targets);
return new UsesCall($this->filename, [])->in(...$targets);
}
/**
@ -60,7 +60,7 @@ final readonly class Configuration
*/
public function group(string ...$groups): UsesCall
{
return (new UsesCall($this->filename, []))->group(...$groups);
return new UsesCall($this->filename, [])->group(...$groups);
}
/**
@ -68,7 +68,7 @@ final readonly class Configuration
*/
public function only(): void
{
(new BeforeEachCall(TestSuite::getInstance(), $this->filename))->only();
new BeforeEachCall(TestSuite::getInstance(), $this->filename)->only();
}
/**

View File

@ -52,7 +52,9 @@ use ReflectionProperty;
*/
final class Expectation
{
/** @use Extendable<self<TValue>> */
use Extendable;
use Pipeable;
use Retrievable;
@ -235,7 +237,7 @@ final class Expectation
if ($callbacks[$index] instanceof Closure) {
$callbacks[$index](new self($value), new self($key));
} else {
(new self($value))->toEqual($callbacks[$index]);
new self($value)->toEqual($callbacks[$index]);
}
$index = isset($callbacks[$index + 1]) ? $index + 1 : 0;
@ -862,15 +864,7 @@ final class Expectation
return Targeted::make(
$this,
function (ObjectDescription $object) use ($interfaces): bool {
foreach ($interfaces as $interface) {
if (! isset($object->reflectionClass) || ! $object->reflectionClass->implementsInterface($interface)) {
return false;
}
}
return true;
},
fn (ObjectDescription $object): bool => array_all($interfaces, fn (string $interface): bool => isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface)),
"to implement '".implode("', '", $interfaces)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
@ -1085,8 +1079,8 @@ final class Expectation
$this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass)
&& $object->reflectionClass->isEnum()
&& (new ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
&& (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line
&& new ReflectionEnum($object->name)->isBacked() // @phpstan-ignore-line
&& (string) new ReflectionEnum($object->name)->getBackingType() === $backingType, // @phpstan-ignore-line
'to be '.$backingType.' backed enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);

View File

@ -576,15 +576,7 @@ final readonly class OppositeExpectation
return Targeted::make(
$original,
function (ObjectDescription $object) use ($traits): bool {
foreach ($traits as $trait) {
if (isset($object->reflectionClass) && in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
return false;
}
}
return true;
},
fn (ObjectDescription $object): bool => array_all($traits, fn (string $trait): bool => ! (isset($object->reflectionClass) && in_array($trait, $object->reflectionClass->getTraitNames(), true))),
"not to use traits '".implode("', '", $traits)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
@ -604,15 +596,7 @@ final readonly class OppositeExpectation
return Targeted::make(
$original,
function (ObjectDescription $object) use ($interfaces): bool {
foreach ($interfaces as $interface) {
if (isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface)) {
return false;
}
}
return true;
},
fn (ObjectDescription $object): bool => array_all($interfaces, fn (string $interface): bool => ! (isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface))),
"not to implement '".implode("', '", $interfaces)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
@ -852,8 +836,8 @@ final readonly class OppositeExpectation
$original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| ! $object->reflectionClass->isEnum()
|| ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
|| (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line
|| ! new \ReflectionEnum($object->name)->isBacked() // @phpstan-ignore-line
|| (string) new \ReflectionEnum($object->name)->getBackingType() !== $backingType, // @phpstan-ignore-line
'not to be '.$backingType.' backed enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);

View File

@ -191,7 +191,7 @@ final class TestCaseFactory
if (
$method->closure instanceof \Closure &&
(new \ReflectionFunction($method->closure))->isStatic()
new \ReflectionFunction($method->closure)->isStatic()
) {
throw new TestClosureMustNotBeStatic($method);

View File

@ -921,7 +921,7 @@ final class Expectation
if ($exception instanceof Closure) {
$callback = $exception;
$parameters = (new ReflectionFunction($exception))->getParameters();
$parameters = new ReflectionFunction($exception)->getParameters();
if (count($parameters) !== 1) {
throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.');

View File

@ -53,9 +53,7 @@ final class UsesCall
$this->targets = [$filename];
}
/**
* @deprecated Use `pest()->printer()->compact()` instead.
*/
#[\Deprecated(message: 'Use `pest()->printer()->compact()` instead.')]
public function compact(): self
{
DefaultPrinter::compact(true);

View File

@ -6,7 +6,7 @@ namespace Pest;
function version(): string
{
return '4.3.1';
return '5.0.0-rc.1';
}
function testDirectory(string $file = ''): string

View File

@ -176,13 +176,7 @@ final class Parallel implements HandlesArguments
{
$arguments = new ArgvInput;
foreach (self::UNSUPPORTED_ARGUMENTS as $unsupportedArgument) {
if ($arguments->hasParameterOption($unsupportedArgument)) {
return true;
}
}
return false;
return array_any(self::UNSUPPORTED_ARGUMENTS, fn (string|array $unsupportedArgument): bool => $arguments->hasParameterOption($unsupportedArgument));
}
/**

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

@ -83,11 +83,11 @@ final class Shard implements AddsOutput, HandlesArguments
*/
private function allTests(array $arguments): array
{
$output = (new Process([
$output = new Process([
'php',
...$this->removeParallelArguments($arguments),
'--list-tests',
]))->mustRun()->getOutput();
])->mustRun()->getOutput();
preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches);

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

@ -50,7 +50,7 @@ final class HigherOrderMessage
}
if ($this->hasHigherOrderCallable()) {
return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments);
return new HigherOrderCallables($target)->{$this->name}(...$this->arguments);
}
try {

View File

@ -31,7 +31,7 @@ final class HigherOrderMessageCollection
*/
public function addWhen(callable $condition, string $filename, int $line, string $name, ?array $arguments): void
{
$this->messages[] = (new HigherOrderMessage($filename, $line, $name, $arguments))->when($condition);
$this->messages[] = new HigherOrderMessage($filename, $line, $name, $arguments)->when($condition);
}
/**

View File

@ -38,7 +38,7 @@ final class HigherOrderTapProxy
return $this->target->{$property};
}
$className = (new ReflectionClass($this->target))->getName();
$className = new ReflectionClass($this->target)->getName();
if (str_starts_with($className, 'P\\')) {
$className = substr($className, 2);
@ -60,7 +60,7 @@ final class HigherOrderTapProxy
$filename = Backtrace::file();
$line = Backtrace::line();
return (new HigherOrderMessage($filename, $line, $methodName, $arguments))
return new HigherOrderMessage($filename, $line, $methodName, $arguments)
->call($this->target);
}
}

View File

@ -180,7 +180,7 @@ final class Reflection
*/
public static function getFunctionArguments(Closure $function): array
{
$parameters = (new ReflectionFunction($function))->getParameters();
$parameters = new ReflectionFunction($function)->getParameters();
$arguments = [];
foreach ($parameters as $parameter) {
@ -206,7 +206,7 @@ final class Reflection
public static function getFunctionVariable(Closure $function, string $key): mixed
{
return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null;
return new ReflectionFunction($function)->getStaticVariables()[$key] ?? null;
}
/**

View File

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

View File

@ -1,5 +1,5 @@
Pest Testing Framework 4.3.1.
Pest Testing Framework 5.0.0-rc.1.
USAGE: pest <file> [options]
@ -44,6 +44,7 @@
--filter [pattern] ............................... Filter which tests to run
--exclude-filter [pattern] .. Exclude tests for the specified filter pattern
--test-suffix [suffixes] Only search for test in files with specified suffix(es). Default: Test.php,.phpt
--test-files-file [file] Only run test files listed in file (one file by line)
EXECUTION OPTIONS:
--parallel ........................................... Run tests in parallel

View File

@ -1,3 +1,3 @@
Pest Testing Framework 4.3.1.
Pest Testing Framework 5.0.0-rc.1.

View File

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

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

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