Compare commits

..

9 Commits
v3.7.3 ... 2.x

Author SHA1 Message Date
d66361b272 release: 2.36.1 2026-01-28 02:02:41 +00:00
93b5611059 chore: deprecates php 8.1 2026-01-28 01:55:42 +00:00
2ded999adf chore: bumps dependencies 2026-01-28 01:51:26 +00:00
cde074cfd4 chore: removes static jobs 2026-01-28 01:36:09 +00:00
499480f28a chore: only runs CI against stable 2026-01-28 01:34:18 +00:00
f8c88bd14d chore: requires latest versions of collision and termwind 2024-10-15 16:30:56 +01:00
d454a36a48 removes non reported error 2024-10-15 15:34:49 +01:00
61b6b8c7d9 release: v2.36.0 2024-10-15 15:31:46 +01:00
e8aaa586cb feat: php 8.4 support 2024-10-15 15:31:29 +01:00
186 changed files with 1049 additions and 5531 deletions

View File

@ -1,44 +0,0 @@
name: Static Analysis
on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'
jobs:
static:
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
name: Static Tests
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
dependency-version: [prefer-lowest, prefer-stable]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
tools: composer:v2
coverage: none
- name: Install Dependencies
run: composer update --prefer-stable --no-interaction --no-progress --ansi
# - name: Type Check
# run: composer test:type:check
- name: Type Coverage
run: composer test:type:coverage
- name: Refacto
run: composer test:refacto
- name: Style
run: composer test:lint

View File

@ -13,9 +13,9 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
symfony: ['7.1']
symfony: ['6.4', '7.0']
php: ['8.2', '8.3', '8.4']
dependency_version: [prefer-lowest, prefer-stable]
dependency_version: [prefer-stable]
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
@ -36,8 +36,7 @@ jobs:
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install PHP dependencies
shell: bash
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:~${{ matrix.symfony }}"
- name: Unit Tests
run: composer test:unit

2
.gitignore vendored
View File

@ -12,5 +12,3 @@ coverage.xml
*.swp
*.swo
.vscode/
.STREAM.md

View File

@ -42,7 +42,7 @@ composer test
Check types:
```bash
composer test:type:check
composer test:types
```
Unit tests:
@ -69,7 +69,7 @@ If you want to check things work against a specific version of PHP, you may incl
the `PHP` build argument when building the image:
```bash
make build ARGS="--build-arg PHP=8.3"
make build ARGS="--build-arg PHP=8.2"
```
The default PHP version will always be the lowest version of PHP supported by Pest.

View File

@ -1,7 +1,7 @@
<p align="center">
<img src="https://raw.githubusercontent.com/pestphp/art/master/v3/banner.png" width="600" alt="PEST">
<img src="https://raw.githubusercontent.com/pestphp/art/master/v2/banner.png" width="600" alt="PEST">
<p align="center">
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (master)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=3.x&label=Tests%203.x"></a>
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (master)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=2.x&label=Tests%202.x"></a>
<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>
@ -9,9 +9,6 @@
</p>
------
> Pest v3 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest3-now-available)**.
**Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP.
- Explore our docs at **[pestphp.com »](https://pestphp.com)**
@ -22,27 +19,22 @@
We cannot thank our sponsors enough for their incredible support in funding Pest's development. Their contributions have been instrumental in making Pest the best it can be. For those who are interested in becoming a sponsor, please visit Nuno Maduro's Sponsor page at **[github.com/sponsors/nunomaduro](https://github.com/sponsors/nunomaduro)**.
### Platinum Sponsors
- **[Laracasts](https://laracasts.com/?ref=pestphp)**
### Gold Sponsors
- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
- **[LaraJobs](https://larajobs.com/?ref=pestphp)**
- **[Brokerchooser](https://brokerchooser.com/?ref=pestphp)**
- **[Forge](https://forge.laravel.com/?ref=pestphp)**
- **[LaraJobs](https://larajobs.com)**
- **[Brokerchooser](https://brokerchooser.com)**
- **[Forge](https://forge.laravel.com)**
- **[Spatie](https://spatie.be)**
- **[Worksome](https://www.worksome.com/)**
### Premium Sponsors
- [Akaunting](https://akaunting.com/?ref=pestphp)
- [Codecourse](https://codecourse.com/?ref=pestphp)
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
- [Laracasts](https://laracasts.com/?ref=pestphp)
- [Localazy](https://localazy.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)
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.

View File

@ -2,10 +2,10 @@
When releasing a new version of Pest there are some checks and updates that need to be done:
> **For Pest v2 you should use the `2.x` branch instead.**
> **For Pest v1 you should use the `1.x` branch instead.**
- Clear your local repository with: `git add . && git reset --hard && git checkout 3.x`
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...3.x](https://github.com/pestphp/pest/compare/{latest_version}...3.x)
- Clear your local repository with: `git add . && git reset --hard && git checkout 2.x`
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...2.x](https://github.com/pestphp/pest/compare/{latest_version}...2.x)
- Update the version number in [src/Pest.php](src/Pest.php)
- Run the tests locally using: `composer test`
- Commit the Pest file with the message: `git commit -m "release: vX.X.X"`

101
bin/pest
View File

@ -1,15 +1,9 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
<?php declare(strict_types=1);
use Pest\Kernel;
use Pest\Panic;
use Pest\TestCaseFilters\GitDirtyTestCaseFilter;
use Pest\TestCaseMethodFilters\AssigneeTestCaseFilter;
use Pest\TestCaseMethodFilters\IssueTestCaseFilter;
use Pest\TestCaseMethodFilters\NotesTestCaseFilter;
use Pest\TestCaseMethodFilters\PrTestCaseFilter;
use Pest\TestCaseMethodFilters\TodoTestCaseFilter;
use Pest\TestSuite;
use Symfony\Component\Console\Input\ArgvInput;
@ -23,10 +17,8 @@ use Symfony\Component\Console\Output\ConsoleOutput;
$dirty = false;
$todo = false;
$notes = false;
foreach ($arguments as $key => $value) {
if ($value === '--compact') {
$_SERVER['COLLISION_PRINTER_COMPACT'] = 'true';
unset($arguments[$key]);
@ -37,14 +29,8 @@ use Symfony\Component\Console\Output\ConsoleOutput;
unset($arguments[$key]);
}
if (str_contains($value, '--test-directory=')) {
if (str_contains($value, '--test-directory')) {
unset($arguments[$key]);
} elseif ($value === '--test-directory') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if ($value === '--dirty') {
@ -57,61 +43,6 @@ use Symfony\Component\Console\Output\ConsoleOutput;
unset($arguments[$key]);
}
if ($value === '--notes') {
$notes = true;
unset($arguments[$key]);
}
if (str_contains($value, '--assignee=')) {
unset($arguments[$key]);
} elseif ($value === '--assignee') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--issue=')) {
unset($arguments[$key]);
} elseif ($value === '--issue') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--ticket=')) {
unset($arguments[$key]);
} elseif ($value === '--ticket') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--pr=')) {
unset($arguments[$key]);
} elseif ($value === '--pr') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--pull-request=')) {
unset($arguments[$key]);
} elseif ($value === '--pull-request') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--teamcity')) {
unset($arguments[$key]);
$arguments[] = '--no-output';
@ -135,7 +66,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
// Get $rootPath based on $autoloadPath
$rootPath = dirname($autoloadPath, 2);
$input = new ArgvInput;
$input = new ArgvInput();
$testSuite = TestSuite::getInstance(
$rootPath,
@ -147,31 +78,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
}
if ($todo) {
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter);
}
if ($notes) {
$testSuite->tests->addTestCaseMethodFilter(new NotesTestCaseFilter);
}
if ($assignee = $input->getParameterOption('--assignee')) {
$testSuite->tests->addTestCaseMethodFilter(new AssigneeTestCaseFilter((string) $assignee));
}
if ($issue = $input->getParameterOption('--issue')) {
$testSuite->tests->addTestCaseMethodFilter(new IssueTestCaseFilter((int) $issue));
}
if ($issue = $input->getParameterOption('--ticket')) {
$testSuite->tests->addTestCaseMethodFilter(new IssueTestCaseFilter((int) $issue));
}
if ($pr = $input->getParameterOption('--pr')) {
$testSuite->tests->addTestCaseMethodFilter(new PrTestCaseFilter((int) $pr));
}
if ($pr = $input->getParameterOption('--pull-request')) {
$testSuite->tests->addTestCaseMethodFilter(new PrTestCaseFilter((int) $pr));
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter());
}
$isDecorated = $input->getParameterOption('--colors', 'always') !== 'never';

View File

@ -32,13 +32,10 @@ $bootPest = (static function (): void {
'status-file:',
'progress-file:',
'unexpected-output-file:',
'test-result-file:',
'result-cache-file:',
'testresult-file:',
'teamcity-file:',
'testdox-file:',
'testdox-color',
'testdox-columns:',
'testdox-summary',
'phpunit-argv:',
]);
@ -64,8 +61,7 @@ $bootPest = (static function (): void {
assert(isset($getopt['progress-file']) && is_string($getopt['progress-file']));
assert(isset($getopt['unexpected-output-file']) && is_string($getopt['unexpected-output-file']));
assert(isset($getopt['test-result-file']) && is_string($getopt['test-result-file']));
assert(! isset($getopt['result-cache-file']) || is_string($getopt['result-cache-file']));
assert(isset($getopt['testresult-file']) && is_string($getopt['testresult-file']));
assert(! isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file']));
assert(! isset($getopt['testdox-file']) || is_string($getopt['testdox-file']));
@ -81,8 +77,7 @@ $bootPest = (static function (): void {
$phpunitArgv,
$getopt['progress-file'],
$getopt['unexpected-output-file'],
$getopt['test-result-file'],
$getopt['result-cache-file'] ?? null,
$getopt['testresult-file'],
$getopt['teamcity-file'] ?? null,
$getopt['testdox-file'] ?? null,
isset($getopt['testdox-color']),

View File

@ -18,18 +18,17 @@
],
"require": {
"php": "^8.2.0",
"brianium/paratest": "^7.7.0",
"nunomaduro/collision": "^8.6.0",
"nunomaduro/termwind": "^2.3.0",
"pestphp/pest-plugin": "^3.0.0",
"pestphp/pest-plugin-arch": "^3.0.0",
"pestphp/pest-plugin-mutate": "^3.0.5",
"phpunit/phpunit": "^11.5.3"
"brianium/paratest": "^7.4.9",
"nunomaduro/collision": "^7.11.0|^8.5.0",
"nunomaduro/termwind": "^1.16.0|^2.3.3",
"pestphp/pest-plugin": "^2.1.1",
"pestphp/pest-plugin-arch": "^2.7.0",
"phpunit/phpunit": "^10.5.63"
},
"conflict": {
"filp/whoops": "<2.16.0",
"phpunit/phpunit": ">11.5.3",
"sebastian/exporter": "<6.0.0",
"phpunit/phpunit": ">10.5.63",
"sebastian/exporter": "<5.1.0",
"webmozart/assert": "<1.11.0"
},
"autoload": {
@ -53,9 +52,9 @@
]
},
"require-dev": {
"pestphp/pest-dev-tools": "^3.3.0",
"pestphp/pest-plugin-type-coverage": "^3.2.3",
"symfony/process": "^7.2.0"
"pestphp/pest-dev-tools": "^2.17.0",
"pestphp/pest-plugin-type-coverage": "^2.8.7",
"symfony/process": "^6.4.0|^7.4.4"
},
"minimum-stability": "dev",
"prefer-stable": true,
@ -70,22 +69,12 @@
"bin/pest"
],
"scripts": {
"refacto": "rector",
"lint": "pint",
"test:refacto": "rector --dry-run",
"test:lint": "pint --test",
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
"test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100",
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
"test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3",
"test:integration": "php bin/pest --colors=always --group=integration -v",
"test:integration": "php bin/pest --colors=always --group=integration",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
"test": [
"@test:refacto",
"@test:lint",
"@test:type:check",
"@test:type:coverage",
"@test:unit",
"@test:parallel",
"@test:integration"
@ -94,8 +83,6 @@
"extra": {
"pest": {
"plugins": [
"Pest\\Mutate\\Plugins\\Mutate",
"Pest\\Plugins\\Configuration",
"Pest\\Plugins\\Bail",
"Pest\\Plugins\\Cache",
"Pest\\Plugins\\Coverage",

View File

@ -1,37 +1,5 @@
<?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);
/*
@ -54,7 +22,7 @@ use PHPUnit\Util\ThrowableToStringMapper;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final readonly class ThrowableBuilder
final class ThrowableBuilder
{
/**
* @throws Exception
@ -68,7 +36,7 @@ final readonly class ThrowableBuilder
$previous = self::from($previous);
}
$trace = Filter::stackTraceFromThrowableAsString($t);
$trace = Filter::getFilteredStacktrace($t);
if ($t instanceof RenderableOnCollisionEditor && $frame = $t->toCollisionEditor()) {
$file = $frame->getFile();

View File

@ -308,6 +308,7 @@ final class JunitXmlLogger
new TestFinishedSubscriber($this),
new TestErroredSubscriber($this),
new TestFailedSubscriber($this),
new TestMarkedIncompleteSubscriber($this),
new TestSkippedSubscriber($this),
new TestRunnerExecutionFinishedSubscriber($this),
);
@ -446,7 +447,7 @@ final class JunitXmlLogger
if ($test->isTestMethod()) {
assert($test instanceof TestMethod);
// $testCase->setAttribute('line', (string) $test->line()); // pest-removed
//$testCase->setAttribute('line', (string) $test->line()); // pest-removed
$className = $this->converter->getTrimmedTestClassName($test); // pest-added
$testCase->setAttribute('class', $className); // pest-changed
$testCase->setAttribute('classname', str_replace('\\', '.', $className)); // pest-changed

View File

@ -32,27 +32,19 @@
* 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 PHPUnit\Runner\Filter;
use Exception;
use Pest\Contracts\HasPrintableTestCaseName;
use PHPUnit\Framework\SelfDescribing;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\PhptTestCase;
use RecursiveFilterIterator;
use RecursiveIterator;
use function end;
use function implode;
use function preg_match;
use function sprintf;
use function str_replace;
@ -60,30 +52,22 @@ use function str_replace;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
abstract class NameFilterIterator extends RecursiveFilterIterator
final class NameFilterIterator extends RecursiveFilterIterator
{
/**
* @psalm-var non-empty-string
*/
private readonly string $regularExpression;
private ?string $filter = null;
private readonly ?int $dataSetMinimum;
private ?int $filterMin = null;
private readonly ?int $dataSetMaximum;
private ?int $filterMax = null;
/**
* @psalm-param RecursiveIterator<int, Test> $iterator
* @psalm-param non-empty-string $filter
* @throws Exception
*/
public function __construct(RecursiveIterator $iterator, string $filter)
{
parent::__construct($iterator);
$preparedFilter = $this->prepareFilter($filter);
$this->regularExpression = $preparedFilter['regularExpression'];
$this->dataSetMinimum = $preparedFilter['dataSetMinimum'];
$this->dataSetMaximum = $preparedFilter['dataSetMaximum'];
$this->setFilter($filter);
}
public function accept(): bool
@ -94,38 +78,29 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
return true;
}
if ($test instanceof PhptTestCase) {
return false;
}
$tmp = $this->describe($test);
if ($test instanceof HasPrintableTestCaseName) {
$name = $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName();
if ($tmp[0] !== '') {
$name = implode('::', $tmp);
} else {
$name = $test::class.'::'.$test->nameWithDataSet();
$name = $tmp[1];
}
$accepted = @preg_match($this->regularExpression, $name, $matches) === 1;
$accepted = @preg_match($this->filter, $name, $matches);
if ($accepted && isset($this->dataSetMaximum)) {
if ($accepted && isset($this->filterMax)) {
$set = end($matches);
$accepted = $set >= $this->dataSetMinimum && $set <= $this->dataSetMaximum;
$accepted = $set >= $this->filterMin && $set <= $this->filterMax;
}
return $this->doAccept($accepted);
return (bool) $accepted;
}
abstract protected function doAccept(bool $result): bool;
/**
* @psalm-param non-empty-string $filter
*
* @psalm-return array{regularExpression: non-empty-string, dataSetMinimum: ?int, dataSetMaximum: ?int}
* @throws Exception
*/
private function prepareFilter(string $filter): array
private function setFilter(string $filter): void
{
$dataSetMinimum = null;
$dataSetMaximum = null;
if (@preg_match($filter, '') === false) {
// Handles:
// * testAssertEqualsSucceeds#4
@ -133,17 +108,17 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) {
if (isset($matches[3]) && $matches[2] < $matches[3]) {
$filter = sprintf(
'%s.*with data set #(\d+)$',
$matches[1],
'%s.*with dataset #(\d+)$',
$matches[1]
);
$dataSetMinimum = (int) $matches[2];
$dataSetMaximum = (int) $matches[3];
$this->filterMin = (int) $matches[2];
$this->filterMax = (int) $matches[3];
} else {
$filter = sprintf(
'%s.*with data set #%s$',
'%s.*with dataset #%s$',
$matches[1],
$matches[2],
$matches[2]
);
}
} // Handles:
@ -151,9 +126,9 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
// * testDetermineJsonError@JSON.*
elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) {
$filter = sprintf(
'%s.*with data set "%s"$',
'%s.*with dataset "%s"$',
$matches[1],
$matches[2],
$matches[2]
);
}
@ -164,15 +139,34 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
str_replace(
'/',
'\\/',
$filter,
),
$filter
)
);
}
return [
'regularExpression' => $filter,
'dataSetMinimum' => $dataSetMinimum,
'dataSetMaximum' => $dataSetMaximum,
];
$this->filter = $filter;
}
/**
* @psalm-return array{0: string, 1: string}
*/
private function describe(Test $test): array
{
if ($test instanceof HasPrintableTestCaseName) {
return [
$test::getPrintableTestCaseName(),
$test->getPrintableTestCaseMethodName(),
];
}
if ($test instanceof TestCase) {
return [$test::class, $test->nameWithDataSet()];
}
if ($test instanceof SelfDescribing) {
return ['', $test->toString()];
}
return ['', $test::class];
}
}

View File

@ -59,7 +59,6 @@ use function file_get_contents;
use function file_put_contents;
use function is_array;
use function is_dir;
use function is_file;
use function json_decode;
use function json_encode;
use function Pest\version;
@ -81,6 +80,11 @@ final class DefaultResultCache implements ResultCache
*/
private array $defects = [];
/**
* @psalm-var array<string, TestStatus>
*/
private array $currentDefects = [];
/**
* @psalm-var array<string, float>
*/
@ -97,11 +101,10 @@ final class DefaultResultCache implements ResultCache
public function setStatus(string $id, TestStatus $status): void
{
if ($status->isSuccess()) {
return;
if ($status->isFailure() || $status->isError()) {
$this->currentDefects[$id] = $status;
$this->defects[$id] = $status;
}
$this->defects[$id] = $status;
}
public function status(string $id): TestStatus
@ -111,6 +114,10 @@ final class DefaultResultCache implements ResultCache
public function setTime(string $id, float $time): void
{
if (! isset($this->currentDefects[$id])) {
unset($this->defects[$id]);
}
$this->times[$id] = $time;
}
@ -121,11 +128,7 @@ final class DefaultResultCache implements ResultCache
public function load(): void
{
if (! is_file($this->cacheFilename)) {
return;
}
$contents = file_get_contents($this->cacheFilename);
$contents = @file_get_contents($this->cacheFilename);
if ($contents === false) {
return;
@ -181,7 +184,7 @@ final class DefaultResultCache implements ResultCache
file_put_contents(
$this->cacheFilename,
json_encode($data),
LOCK_EX,
LOCK_EX
);
}

View File

@ -38,13 +38,11 @@ namespace PHPUnit\Runner;
use Exception;
use Pest\Contracts\HasPrintableTestCaseName;
use Pest\Panic;
use Pest\TestCases\IgnorableTestCase;
use Pest\TestSuite;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionException;
use Throwable;
use function array_diff;
use function array_values;
@ -88,11 +86,7 @@ final class TestSuiteLoader
$suiteClassName = $this->classNameFromFileName($suiteClassFile);
(static function () use ($suiteClassFile) {
try {
include_once $suiteClassFile;
} catch (Throwable $e) {
Panic::with($e);
}
include_once $suiteClassFile;
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
})();

View File

@ -45,8 +45,6 @@ declare(strict_types=1);
namespace PHPUnit\TextUI\Command;
use const PHP_EOL;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
use PHPUnit\TextUI\Configuration\Configuration;
use PHPUnit\TextUI\Configuration\NoCoverageCacheDirectoryException;
@ -57,11 +55,11 @@ use SebastianBergmann\Timer\Timer;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final readonly class WarmCodeCoverageCacheCommand implements Command
final class WarmCodeCoverageCacheCommand implements Command
{
private Configuration $configuration;
private readonly Configuration $configuration;
private CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
private readonly CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
public function __construct(Configuration $configuration, CodeCoverageFilterRegistry $codeCoverageFilterRegistry)
{
@ -78,16 +76,16 @@ final readonly class WarmCodeCoverageCacheCommand implements Command
if (! $this->configuration->hasCoverageCacheDirectory()) {
return Result::from(
'Cache for static analysis has not been configured'.PHP_EOL,
Result::FAILURE,
Result::FAILURE
);
}
$this->codeCoverageFilterRegistry->init($this->configuration, true);
$this->codeCoverageFilterRegistry->init($this->configuration);
if (! $this->codeCoverageFilterRegistry->configured()) {
return Result::from(
'Filter for code coverage has not been configured'.PHP_EOL,
Result::FAILURE,
Result::FAILURE
);
}
@ -98,7 +96,7 @@ final readonly class WarmCodeCoverageCacheCommand implements Command
$this->configuration->coverageCacheDirectory(),
! $this->configuration->disableCodeCoverageIgnore(),
$this->configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage(),
$this->codeCoverageFilterRegistry->get(),
$this->codeCoverageFilterRegistry->get()
);
return Result::from();

View File

@ -43,7 +43,7 @@ declare(strict_types=1);
* file that was distributed with this source code.
*/
namespace Pest\Logging\TeamCity\Subscriber;
namespace PHPUnit\TextUI\Output\Default\ProgressPrinter;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\Test\SkippedSubscriber;
@ -51,16 +51,21 @@ use ReflectionClass;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*
* This file is overridden to allow Pest Parallel to show todo items in the progress output.
*/
final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
{
/**
* Notifies the printer that a test was skipped.
*/
public function notify(Skipped $event): void
{
if (str_contains($event->message(), '__TODO__')) {
$this->printTodoItem();
}
$this->logger()->testSkipped($event);
$this->printer()->testSkipped();
}
/**

View File

@ -33,6 +33,7 @@
*/
declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
@ -56,7 +57,7 @@ use function array_map;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final readonly class TestSuiteFilterProcessor
final class TestSuiteFilterProcessor
{
/**
* @throws Event\RuntimeException
@ -69,24 +70,24 @@ final readonly class TestSuiteFilterProcessor
if (! $configuration->hasFilter() &&
! $configuration->hasGroups() &&
! $configuration->hasExcludeGroups() &&
! $configuration->hasExcludeFilter() &&
! $configuration->hasTestsCovering() &&
! $configuration->hasTestsUsing() &&
! Only::isEnabled()) {
! Only::isEnabled()
) {
return;
}
if ($configuration->hasExcludeGroups()) {
$factory->addExcludeGroupFilter(
$configuration->excludeGroups(),
$configuration->excludeGroups()
);
}
if (Only::isEnabled()) {
$factory->addIncludeGroupFilter([Only::group()]);
$factory->addIncludeGroupFilter(['__pest_only']);
} elseif ($configuration->hasGroups()) {
$factory->addIncludeGroupFilter(
$configuration->groups(),
$configuration->groups()
);
}
@ -94,8 +95,8 @@ final readonly class TestSuiteFilterProcessor
$factory->addIncludeGroupFilter(
array_map(
static fn (string $name): string => '__phpunit_covers_'.$name,
$configuration->testsCovering(),
),
$configuration->testsCovering()
)
);
}
@ -103,27 +104,21 @@ final readonly class TestSuiteFilterProcessor
$factory->addIncludeGroupFilter(
array_map(
static fn (string $name): string => '__phpunit_uses_'.$name,
$configuration->testsUsing(),
),
);
}
if ($configuration->hasExcludeFilter()) {
$factory->addExcludeNameFilter(
$configuration->excludeFilter(),
$configuration->testsUsing()
)
);
}
if ($configuration->hasFilter()) {
$factory->addIncludeNameFilter(
$configuration->filter(),
$factory->addNameFilter(
$configuration->filter()
);
}
$suite->injectFilter($factory);
Event\Facade::emitter()->testSuiteFiltered(
Event\TestSuite\TestSuiteBuilder::from($suite),
Event\TestSuite\TestSuiteBuilder::from($suite)
);
}
}

View File

@ -2,22 +2,30 @@
declare(strict_types=1);
use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
use Rector\Config\RectorConfig;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
return RectorConfig::configure()
->withPaths([
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([
__DIR__.'/src',
])
->withSkip([
]);
$rectorConfig->rules([
InlineConstructorDefaultToPropertyRector::class,
]);
$rectorConfig->skip([
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
ReturnNeverTypeRector::class,
])
->withPreparedSets(
deadCode: true,
codeQuality: true,
typeDeclarations: true,
privatization: true,
earlyReturn: true,
)
->withPhpSets();
]);
$rectorConfig->sets([
LevelSetList::UP_TO_PHP_81,
SetList::CODE_QUALITY,
SetList::DEAD_CODE,
SetList::EARLY_RETURN,
SetList::TYPE_DECLARATION,
SetList::PRIVATIZATION,
]);
};

View File

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Default">
<directory>tests/</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
<directory>src</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>

View File

@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/**
* @internal
*/
abstract class AbstractPreset // @pest-arch-ignore-line
{
/**
* The expectations.
*
* @var array<int, Expectation<mixed>|ArchExpectation>
*/
protected array $expectations = [];
/**
* Creates a new preset instance.
*
* @param array<int, string> $userNamespaces
*/
public function __construct(
private readonly array $userNamespaces,
) {
//
}
/**
* Executes the arch preset.
*
* @internal
*/
abstract public function execute(): void;
/**
* Ignores the given "targets" or "dependencies".
*
* @param array<int, string>|string $targetsOrDependencies
*/
final public function ignoring(array|string $targetsOrDependencies): void
{
$this->expectations = array_map(
fn (ArchExpectation|Expectation $expectation): Expectation|ArchExpectation => $expectation instanceof ArchExpectation ? $expectation->ignoring($targetsOrDependencies) : $expectation,
$this->expectations,
);
}
/**
* Runs the given callback for each namespace.
*
* @param callable(Expectation<string|null>): ArchExpectation ...$callbacks
*/
final public function eachUserNamespace(callable ...$callbacks): void
{
foreach ($this->userNamespaces as $namespace) {
foreach ($callbacks as $callback) {
$this->expectations[] = $callback(expect($namespace));
}
}
}
/**
* Flushes the expectations.
*/
final public function flush(): void
{
$this->expectations = [];
}
}

View File

@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
use Closure;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/**
* @internal
*/
final class Custom extends AbstractPreset
{
/**
* Creates a new preset instance.
*
* @param array<int, string> $userNamespaces
* @param Closure(array<int, string>): array<Expectation<mixed>|ArchExpectation> $execute
*/
public function __construct(
private readonly array $userNamespaces,
private readonly string $name,
private readonly Closure $execute,
) {
parent::__construct($userNamespaces);
}
/**
* Returns the name of the preset.
*/
public function name(): string
{
return $this->name;
}
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->expectations = ($this->execute)($this->userNamespaces);
}
}

View File

@ -1,170 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
use Throwable;
/**
* @internal
*/
final class Laravel extends AbstractPreset
{
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->expectations[] = expect('App\Traits')
->toBeTraits();
$this->expectations[] = expect('App\Concerns')
->toBeTraits();
$this->expectations[] = expect('App')
->not->toBeEnums()
->ignoring('App\Enums');
$this->expectations[] = expect('App\Enums')
->toBeEnums()
->ignoring('App\Enums\Concerns');
$this->expectations[] = expect('App\Features')
->toBeClasses()
->ignoring('App\Features\Concerns');
$this->expectations[] = expect('App\Features')
->toHaveMethod('resolve');
$this->expectations[] = expect('App\Exceptions')
->classes()
->toImplement('Throwable')
->ignoring('App\Exceptions\Handler');
$this->expectations[] = expect('App')
->not->toImplement(Throwable::class)
->ignoring('App\Exceptions');
$this->expectations[] = expect('App\Http\Middleware')
->classes()
->toHaveMethod('handle');
$this->expectations[] = expect('App\Models')
->classes()
->toExtend('Illuminate\Database\Eloquent\Model')
->ignoring('App\Models\Scopes');
$this->expectations[] = expect('App\Models')
->classes()
->not->toHaveSuffix('Model');
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Database\Eloquent\Model')
->ignoring('App\Models');
$this->expectations[] = expect('App\Http\Requests')
->classes()
->toHaveSuffix('Request');
$this->expectations[] = expect('App\Http\Requests')
->toExtend('Illuminate\Foundation\Http\FormRequest');
$this->expectations[] = expect('App\Http\Requests')
->toHaveMethod('rules');
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Foundation\Http\FormRequest')
->ignoring('App\Http\Requests');
$this->expectations[] = expect('App\Console\Commands')
->classes()
->toHaveSuffix('Command');
$this->expectations[] = expect('App\Console\Commands')
->classes()
->toExtend('Illuminate\Console\Command');
$this->expectations[] = expect('App\Console\Commands')
->classes()
->toHaveMethod('handle');
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Console\Command')
->ignoring('App\Console\Commands');
$this->expectations[] = expect('App\Mail')
->classes()
->toExtend('Illuminate\Mail\Mailable');
$this->expectations[] = expect('App\Mail')
->classes()
->toImplement('Illuminate\Contracts\Queue\ShouldQueue');
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Mail\Mailable')
->ignoring('App\Mail');
$this->expectations[] = expect('App\Jobs')
->classes()
->toImplement('Illuminate\Contracts\Queue\ShouldQueue');
$this->expectations[] = expect('App\Jobs')
->classes()
->toHaveMethod('handle');
$this->expectations[] = expect('App\Listeners')
->toHaveMethod('handle');
$this->expectations[] = expect('App\Notifications')
->toExtend('Illuminate\Notifications\Notification');
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Notifications\Notification')
->ignoring('App\Notifications');
$this->expectations[] = expect('App\Providers')
->toHaveSuffix('ServiceProvider');
$this->expectations[] = expect('App\Providers')
->toExtend('Illuminate\Support\ServiceProvider');
$this->expectations[] = expect('App\Providers')
->not->toBeUsed();
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Support\ServiceProvider')
->ignoring('App\Providers');
$this->expectations[] = expect('App')
->not->toHaveSuffix('ServiceProvider')
->ignoring('App\Providers');
$this->expectations[] = expect('App')
->not->toHaveSuffix('Controller')
->ignoring('App\Http\Controllers');
$this->expectations[] = expect('App\Http\Controllers')
->classes()
->toHaveSuffix('Controller');
$this->expectations[] = expect('App\Http')
->toOnlyBeUsedIn('App\Http');
$this->expectations[] = expect('App\Http\Controllers')
->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy', 'middleware']);
$this->expectations[] = expect([
'dd',
'ddd',
'dump',
'env',
'exit',
'ray',
])->not->toBeUsed();
$this->expectations[] = expect('App\Policies')
->classes()
->toHaveSuffix('Policy');
}
}

View File

@ -1,93 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
/**
* @internal
*/
final class Php extends AbstractPreset
{
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->expectations[] = expect([
'debug_zval_dump',
'debug_backtrace',
'debug_print_backtrace',
'dump',
'ray',
'ds',
'die',
'goto',
'global',
'var_dump',
'phpinfo',
'echo',
'ereg',
'eregi',
'mysql_connect',
'mysql_pconnect',
'mysql_query',
'mysql_select_db',
'mysql_fetch_array',
'mysql_fetch_assoc',
'mysql_fetch_object',
'mysql_fetch_row',
'mysql_num_rows',
'mysql_affected_rows',
'mysql_free_result',
'mysql_insert_id',
'mysql_error',
'mysql_real_escape_string',
'print',
'print_r',
'var_export',
'xdebug_break',
'xdebug_call_class',
'xdebug_call_file',
'xdebug_call_int',
'xdebug_call_line',
'xdebug_code_coverage_started',
'xdebug_connect_to_client',
'xdebug_debug_zval',
'xdebug_debug_zval_stdout',
'xdebug_dump_superglobals',
'xdebug_get_code_coverage',
'xdebug_get_collected_errors',
'xdebug_get_function_count',
'xdebug_get_function_stack',
'xdebug_get_gc_run_count',
'xdebug_get_gc_total_collected_roots',
'xdebug_get_gcstats_filename',
'xdebug_get_headers',
'xdebug_get_monitored_functions',
'xdebug_get_profiler_filename',
'xdebug_get_stack_depth',
'xdebug_get_tracefile_name',
'xdebug_info',
'xdebug_is_debugger_active',
'xdebug_memory_usage',
'xdebug_notify',
'xdebug_peak_memory_usage',
'xdebug_print_function_stack',
'xdebug_set_filter',
'xdebug_start_code_coverage',
'xdebug_start_error_collection',
'xdebug_start_function_monitor',
'xdebug_start_gcstats',
'xdebug_start_trace',
'xdebug_stop_code_coverage',
'xdebug_stop_error_collection',
'xdebug_stop_function_monitor',
'xdebug_stop_gcstats',
'xdebug_stop_trace',
'xdebug_time_index',
'xdebug_var_dump',
'trap',
])->not->toBeUsed();
}
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/**
* @internal
*/
final class Relaxed extends AbstractPreset
{
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->eachUserNamespace(
fn (Expectation $namespace): ArchExpectation => $namespace->not->toUseStrictTypes(),
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeFinal(),
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHavePrivateMethods(),
);
}
}

View File

@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
/**
* @internal
*/
final class Security extends AbstractPreset
{
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->expectations[] = expect([
'md5',
'sha1',
'uniqid',
'rand',
'mt_rand',
'tempnam',
'str_shuffle',
'shuffle',
'array_rand',
'eval',
'exec',
'shell_exec',
'system',
'passthru',
'create_function',
'unserialize',
'extract',
'parse_str',
'mb_parse_str',
'dl',
'assert',
])->not->toBeUsed();
}
}

View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/**
* @internal
*/
final class Strict extends AbstractPreset
{
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->eachUserNamespace(
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHaveProtectedMethods(),
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeAbstract(),
fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictTypes(),
fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictEquality(),
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->toBeFinal(),
);
$this->expectations[] = expect([
'sleep',
'usleep',
])->not->toBeUsed();
}
}

View File

@ -12,13 +12,13 @@ use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final readonly class BootKernelDump implements Bootstrapper
final class BootKernelDump implements Bootstrapper
{
/**
* Creates a new Boot Kernel Dump instance.
*/
public function __construct(
private OutputInterface $output,
private readonly OutputInterface $output,
) {
// ...
}

View File

@ -18,14 +18,14 @@ final class BootOverrides implements Bootstrapper
* @var array<string, string>
*/
public const FILES = [
'53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php',
'77ffb7647b583bd82e37962c6fbdc4b04d3344d8a2c1ed103e625ed1ff7cb5c2' => 'Runner/ResultCache/DefaultResultCache.php',
'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php',
'3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
'8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php',
'8cfcb4999af79463eca51a42058e502ea4ddc776cba5677bf2f8eb6093e21a5c' => 'Event/Value/ThrowableBuilder.php',
'86cd9bcaa53cdd59c5b13e58f30064a015c549501e7629d93b96893d4dee1eb1' => 'Logging/JUnit/JunitXmlLogger.php',
'c7b9c8a96006dea314204a8f09a8764e51ce0b9b79aadd58da52e8c328db4870' => 'Runner/Filter/NameFilterIterator.php',
'c7c09ab7c9378710b27f761a4b2948196cbbdf2a73e4389bcdca1e7c94fa9c21' => 'Runner/ResultCache/DefaultResultCache.php',
'bc8718c89264f65800beabc23e51c6d3bcff87dfc764a12179ef5dbfde272c8b' => 'Runner/TestSuiteLoader.php',
'f41e48d6cb546772a7de4f8e66b6b7ce894a5318d063eb52e354d206e96c701c' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
'cb7519f2d82893640b694492cf7ec9528da80773cc1d259634181b5d393528b5' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
'2f06e4b1a9f3a24145bfc7ea25df4f124117f940a2cde30a04d04d5678006bff' => 'TextUI/TestSuiteFilterProcessor.php',
'ef64a657ed9c0067791483784944107827bf227c7e3200f212b6751876b99e25' => 'Event/Value/ThrowableBuilder.php',
'c78f96e34b98ed01dd8106539d59b8aa8d67f733274118b827c01c5c4111c033' => 'Logging/JUnit/JunitXmlLogger.php',
];
/**

View File

@ -13,7 +13,7 @@ use PHPUnit\Event\Subscriber;
/**
* @internal
*/
final readonly class BootSubscribers implements Bootstrapper
final class BootSubscribers implements Bootstrapper
{
/**
* The list of Subscribers.
@ -31,7 +31,7 @@ final readonly class BootSubscribers implements Bootstrapper
* Creates a new instance of the Boot Subscribers.
*/
public function __construct(
private Container $container,
private readonly Container $container,
) {}
/**

View File

@ -11,13 +11,13 @@ use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final readonly class BootView implements Bootstrapper
final class BootView implements Bootstrapper
{
/**
* Creates a new instance of the Boot View.
*/
public function __construct(
private OutputInterface $output
private readonly OutputInterface $output
) {
// ..
}

View File

@ -1,100 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Collision;
use NunoMaduro\Collision\Adapters\Phpunit\TestResult;
use Pest\Configuration\Project;
use Symfony\Component\Console\Output\OutputInterface;
use function Termwind\render;
use function Termwind\renderUsing;
/**
* @internal
*/
final class Events
{
/**
* Sets the output.
*/
private static ?OutputInterface $output = null;
/**
* Sets the output.
*/
public static function setOutput(OutputInterface $output): void
{
self::$output = $output;
}
/**
* Fires before the test method description is printed.
*/
public static function beforeTestMethodDescription(TestResult $result, string $description): string
{
if (($context = $result->context) === []) {
return $description;
}
renderUsing(self::$output);
[
'assignees' => $assignees,
'issues' => $issues,
'prs' => $prs,
] = $context;
if (($link = Project::getInstance()->issues) !== '') {
$issuesDescription = array_map(fn (int $issue): string => sprintf('<a href="%s">#%s</a>', sprintf($link, $issue), $issue), $issues);
}
if (($link = Project::getInstance()->prs) !== '') {
$prsDescription = array_map(fn (int $pr): string => sprintf('<a href="%s">#%s</a>', sprintf($link, $pr), $pr), $prs);
}
if (($link = Project::getInstance()->assignees) !== '' && count($assignees) > 0) {
$assigneesDescription = array_map(fn (string $assignee): string => sprintf(
'<a href="%s">@%s</a>',
sprintf($link, $assignee),
$assignee,
), $assignees);
}
if (count($assignees) > 0 || count($issues) > 0 || count($prs) > 0) {
$description .= ' '.implode(', ', array_merge(
$issuesDescription ?? [],
$prsDescription ?? [],
isset($assigneesDescription) ? ['['.implode(', ', $assigneesDescription).']'] : [],
));
}
return $description;
}
/**
* Fires after the test method description is printed.
*/
public static function afterTestMethodDescription(TestResult $result): void
{
if (($context = $result->context) === []) {
return;
}
renderUsing(self::$output);
[
'notes' => $notes,
] = $context;
foreach ($notes as $note) {
render(sprintf(<<<'HTML'
<div class="ml-2">
<span class="text-gray"> // %s</span>
</div>
HTML, $note,
));
}
}
}

View File

@ -5,17 +5,14 @@ declare(strict_types=1);
namespace Pest\Concerns;
use Closure;
use Pest\Exceptions\DatasetArgumentsMismatch;
use Pest\Preset;
use Pest\Exceptions\DatasetArgsCountMismatch;
use Pest\Support\ChainableClosure;
use Pest\Support\ExceptionTrace;
use Pest\Support\Reflection;
use Pest\TestSuite;
use PHPUnit\Framework\Attributes\PostCondition;
use PHPUnit\Framework\TestCase;
use ReflectionException;
use ReflectionFunction;
use ReflectionParameter;
use Throwable;
/**
@ -35,41 +32,10 @@ trait Testable
*/
private static string $__latestDescription;
/**
* The test's assignees.
*/
private static array $__latestAssignees = [];
/**
* The test's notes.
*/
private static array $__latestNotes = [];
/**
* The test's issues.
*
* @var array<int, int>
*/
private static array $__latestIssues = [];
/**
* The test's PRs.
*
* @var array<int, int>
*/
private static array $__latestPrs = [];
/**
* The test's describing, if any.
*
* @var array<int, string>
*/
public array $__describing = [];
/**
* Whether the test has ran or not.
*/
public bool $__ran = false;
public ?string $__describing = null;
/**
* The test's test closure.
@ -101,6 +67,15 @@ trait Testable
*/
private array $__snapshotChanges = [];
/**
* Resets the test case static properties.
*/
public static function flush(): void
{
self::$__beforeAll = null;
self::$__afterAll = null;
}
/**
* Creates a new Test Case instance.
*/
@ -113,36 +88,11 @@ trait Testable
if ($test->hasMethod($name)) {
$method = $test->getMethod($name);
$this->__description = self::$__latestDescription = $method->description;
self::$__latestAssignees = $method->assignees;
self::$__latestNotes = $method->notes;
self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs;
$this->__describing = $method->describing;
$this->__test = $method->getClosure();
$this->__test = $method->getClosure($this);
}
}
/**
* Resets the test case static properties.
*/
public static function flush(): void
{
self::$__beforeAll = null;
self::$__afterAll = null;
}
/**
* Adds a new "note" to the Test Case.
*/
public function note(array|string $note): self
{
$note = is_array($note) ? $note : [$note];
self::$__latestNotes = array_merge(self::$__latestNotes, $note);
return $this;
}
/**
* Adds a new "setUpBeforeClass" to the Test Case.
*/
@ -236,22 +186,14 @@ trait Testable
/**
* Gets executed before the Test Case.
*/
protected function setUp(...$arguments): void
protected function setUp(): void
{
TestSuite::getInstance()->test = $this;
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$method->setUp($this);
$description = $method->description;
if ($this->dataName()) {
$description = str_contains((string) $description, ':dataset')
? str_replace(':dataset', str_replace('dataset ', '', $this->dataName()), (string) $description)
: $description.' with '.$this->dataName();
}
$description = htmlspecialchars(html_entity_decode((string) $description), ENT_NOQUOTES);
$description = $this->dataName() ? $method->description.' with '.$this->dataName() : $method->description;
$description = htmlspecialchars(html_entity_decode($description), ENT_NOQUOTES);
if ($method->repetitions > 1) {
$matches = [];
@ -269,10 +211,6 @@ trait Testable
}
$this->__description = self::$__latestDescription = $description;
self::$__latestAssignees = $method->assignees;
self::$__latestNotes = $method->notes;
self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs;
parent::setUp();
@ -282,13 +220,13 @@ trait Testable
$beforeEach = ChainableClosure::bound($this->__beforeEach, $beforeEach);
}
$this->__callClosure($beforeEach, $arguments);
$this->__callClosure($beforeEach, func_get_args());
}
/**
* Gets executed after the Test Case.
*/
protected function tearDown(...$arguments): void
protected function tearDown(): void
{
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
@ -302,9 +240,6 @@ trait Testable
parent::tearDown();
TestSuite::getInstance()->test = null;
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$method->tearDown($this);
}
}
@ -316,7 +251,7 @@ trait Testable
private function __runTest(Closure $closure, ...$args): mixed
{
$arguments = $this->__resolveTestArguments($args);
$this->__ensureDatasetArgumentNameAndNumberMatches($arguments);
$this->__ensureDatasetArgumentNumberMatches($arguments);
return $this->__callClosure($closure, $arguments);
}
@ -331,12 +266,7 @@ trait Testable
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
if ($method->repetitions > 1) {
// If the test is repeated, the first argument is the iteration number
// we need to move it to the end of the arguments list
// so that the datasets are the first n arguments
// and the iteration number is the last argument
$firstArgument = array_shift($arguments);
$arguments[] = $firstArgument;
array_shift($arguments);
}
$underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure');
@ -358,7 +288,7 @@ trait Testable
return $arguments;
}
if (! isset($arguments[0]) || ! $arguments[0] instanceof Closure) {
if (! $arguments[0] instanceof Closure) {
return $arguments;
}
@ -381,9 +311,9 @@ trait Testable
* Ensures dataset items count matches underlying test case required parameters
*
* @throws ReflectionException
* @throws DatasetArgumentsMismatch
* @throws DatasetArgsCountMismatch
*/
private function __ensureDatasetArgumentNameAndNumberMatches(array $arguments): void
private function __ensureDatasetArgumentNumberMatches(array $arguments): void
{
if ($arguments === []) {
return;
@ -394,21 +324,11 @@ trait Testable
$requiredParametersCount = $testReflection->getNumberOfRequiredParameters();
$suppliedParametersCount = count($arguments);
$datasetParameterNames = array_keys($arguments);
$testParameterNames = array_map(
fn (ReflectionParameter $reflectionParameter): string => $reflectionParameter->getName(),
array_filter($testReflection->getParameters(), fn (ReflectionParameter $reflectionParameter): bool => ! $reflectionParameter->isOptional()),
);
if (array_diff($testParameterNames, $datasetParameterNames) === []) {
if ($suppliedParametersCount >= $requiredParametersCount) {
return;
}
if (isset($testParameterNames[0]) && $suppliedParametersCount >= $requiredParametersCount) {
return;
}
throw new DatasetArgumentsMismatch($requiredParametersCount, $suppliedParametersCount);
throw new DatasetArgsCountMismatch($requiredParametersCount, $suppliedParametersCount);
}
/**
@ -419,15 +339,7 @@ trait Testable
return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
}
/**
* Uses the given preset on the test.
*/
public function preset(): Preset
{
return new Preset;
}
#[PostCondition]
/** @postCondition */
protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void
{
if (count($this->__snapshotChanges) === 0) {
@ -468,17 +380,4 @@ trait Testable
{
return self::$__latestDescription;
}
/**
* The printable test case method context.
*/
public static function getPrintableContext(): array
{
return [
'assignees' => self::$__latestAssignees,
'issues' => self::$__latestIssues,
'prs' => self::$__latestPrs,
'notes' => self::$__latestNotes,
];
}
}

View File

@ -1,114 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest;
use Pest\PendingCalls\UsesCall;
/**
* @internal
*
* @mixin UsesCall
*/
final readonly class Configuration
{
/**
* The filename of the configuration.
*/
private string $filename;
/**
* Creates a new configuration instance.
*/
public function __construct(
string $filename,
) {
$this->filename = str_ends_with($filename, DIRECTORY_SEPARATOR.'Pest.php') ? dirname($filename) : $filename;
}
/**
* Use the given classes and traits in the given targets.
*/
public function in(string ...$targets): UsesCall
{
return (new UsesCall($this->filename, []))->in(...$targets);
}
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function extend(string ...$classAndTraits): UsesCall
{
return new UsesCall(
$this->filename,
array_values($classAndTraits)
);
}
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function extends(string ...$classAndTraits): UsesCall
{
return $this->extend(...$classAndTraits);
}
/**
* Depending on where is called, it will add the given groups globally or locally.
*/
public function group(string ...$groups): UsesCall
{
return (new UsesCall($this->filename, []))->group(...$groups);
}
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function use(string ...$classAndTraits): UsesCall
{
return $this->extend(...$classAndTraits);
}
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function uses(string ...$classAndTraits): UsesCall
{
return $this->extends(...$classAndTraits);
}
/**
* Gets the printer configuration.
*/
public function printer(): Configuration\Printer
{
return new Configuration\Printer;
}
/**
* Gets the presets configuration.
*/
public function presets(): Configuration\Presets
{
return new Configuration\Presets;
}
/**
* Gets the project configuration.
*/
public function project(): Configuration\Project
{
return Configuration\Project::getInstance();
}
/**
* Proxies calls to the uses method.
*
* @param array<array-key, mixed> $arguments
*/
public function __call(string $name, array $arguments): mixed
{
return $this->uses()->$name(...$arguments); // @phpstan-ignore-line
}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Configuration;
use Closure;
use Pest\Preset;
final class Presets
{
/**
* Creates a custom preset instance, and adds it to the list of presets.
*/
public function custom(string $name, Closure $execute): void
{
Preset::custom($name, $execute);
}
}

View File

@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Configuration;
use NunoMaduro\Collision\Adapters\Phpunit\Printers\DefaultPrinter;
/**
* @internal
*/
final readonly class Printer
{
/**
* Sets the theme to compact.
*/
public function compact(): self
{
DefaultPrinter::compact(true);
return $this;
}
}

View File

@ -1,109 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Configuration;
/**
* @internal
*/
final class Project
{
/**
* The assignees link.
*
* @internal
*/
public string $assignees = '';
/**
* The issues link.
*
* @internal
*/
public string $issues = '';
/**
* The PRs link.
*
* @internal
*/
public string $prs = '';
/**
* The singleton instance.
*/
private static ?self $instance = null;
/**
* Creates a new instance of the project.
*/
public static function getInstance(): self
{
return self::$instance ??= new self;
}
/**
* Sets the test project to GitHub.
*/
public function github(string $project): self
{
$this->issues = "https://github.com/{$project}/issues/%s";
$this->prs = "https://github.com/{$project}/pull/%s";
$this->assignees = 'https://github.com/%s';
return $this;
}
/**
* Sets the test project to GitLab.
*/
public function gitlab(string $project): self
{
$this->issues = "https://gitlab.com/{$project}/issues/%s";
$this->prs = "https://gitlab.com/{$project}/merge_requests/%s";
$this->assignees = 'https://gitlab.com/%s';
return $this;
}
/**
* Sets the test project to Bitbucket.
*/
public function bitbucket(string $project): self
{
$this->issues = "https://bitbucket.org/{$project}/issues/%s";
$this->prs = "https://bitbucket.org/{$project}/pull-requests/%s";
$this->assignees = 'https://bitbucket.org/%s';
return $this;
}
/**
* Sets the test project to Jira.
*/
public function jira(string $namespace, string $project): self
{
$this->issues = "https://{$namespace}.atlassian.net/browse/{$project}-%s";
$this->assignees = "https://{$namespace}.atlassian.net/secure/ViewProfile.jspa?name=%s";
return $this;
}
/**
* Sets the test project to custom.
*/
public function custom(string $issues, string $prs, string $assignees): self
{
$this->issues = $issues;
$this->prs = $prs;
$this->assignees = $assignees;
return $this;
}
}

View File

@ -9,7 +9,7 @@ use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final readonly class Help
final class Help
{
/**
* The Command messages.
@ -27,7 +27,7 @@ final readonly class Help
/**
* Creates a new Console Command instance.
*/
public function __construct(private OutputInterface $output)
public function __construct(private readonly OutputInterface $output)
{
// ..
}

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Question\ConfirmationQuestion;
/**
* @internal
*/
final readonly class Thanks
final class Thanks
{
/**
* The support options.
@ -33,8 +33,8 @@ final readonly class Thanks
* Creates a new Console Command instance.
*/
public function __construct(
private InputInterface $input,
private OutputInterface $output
private readonly InputInterface $input,
private readonly OutputInterface $output
) {
// ..
}
@ -72,13 +72,13 @@ final readonly class Thanks
}
if ($wantsToSupport === true) {
if (PHP_OS_FAMILY === 'Darwin') {
if (PHP_OS_FAMILY == 'Darwin') {
exec('open https://github.com/pestphp/pest');
}
if (PHP_OS_FAMILY === 'Windows') {
if (PHP_OS_FAMILY == 'Windows') {
exec('start https://github.com/pestphp/pest');
}
if (PHP_OS_FAMILY === 'Linux') {
if (PHP_OS_FAMILY == 'Linux') {
exec('xdg-open https://github.com/pestphp/pest');
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Pest\Contracts;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
interface AddsAnnotations
{
/**
* Adds annotations to the given test case method.
*
* @param array<int, string> $annotations
* @return array<int, string>
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array;
}

View File

@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Contracts;
/**
* @internal
*/
interface ArchPreset {}

View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Evaluators;
use Pest\Factories\Attribute;
/**
* @internal
*/
final class Attributes
{
/**
* Evaluates the given attributes and returns the code.
*
* @param iterable<int, Attribute> $attributes
*/
public static function code(iterable $attributes): string
{
return implode(PHP_EOL, array_map(function (Attribute $attribute): string {
$name = $attribute->name;
if ($attribute->arguments === []) {
return " #[\\{$name}]";
}
$arguments = array_map(fn (string $argument): string => var_export($argument, true), iterator_to_array($attribute->arguments));
return sprintf(' #[\\%s(%s)]', $name, implode(', ', $arguments));
}, iterator_to_array($attributes)));
}
}

View File

@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use InvalidArgumentException;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Symfony\Component\Console\Exception\ExceptionInterface;
/**
* @internal
*/
final class AfterBeforeTestFunction extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename)
{
parent::__construct('After method cannot be used with before the [test|it] functions in the filename `['.$filename.']`.');
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use Exception;
final class DatasetArgsCountMismatch extends Exception
{
public function __construct(int $requiredCount, int $suppliedCount)
{
parent::__construct(sprintf('Test expects %d arguments but dataset only provides %d', $requiredCount, $suppliedCount));
}
}

View File

@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use Exception;
final class DatasetArgumentsMismatch extends Exception
{
public function __construct(int $requiredCount, int $suppliedCount)
{
if ($requiredCount <= $suppliedCount) {
parent::__construct('Test argument names and dataset keys do not match');
} else {
parent::__construct(sprintf('Test expects %d arguments but dataset only provides %d', $requiredCount, $suppliedCount));
}
}
//
}

View File

@ -30,12 +30,9 @@ use Pest\Expectations\HigherOrderExpectation;
use Pest\Expectations\OppositeExpectation;
use Pest\Matchers\Any;
use Pest\Support\ExpectationPipeline;
use Pest\Support\Reflection;
use PHPUnit\Architecture\Elements\ObjectDescription;
use PHPUnit\Framework\ExpectationFailedException;
use ReflectionEnum;
use ReflectionMethod;
use ReflectionProperty;
/**
* @template TValue
@ -223,7 +220,7 @@ final class Expectation
throw new BadMethodCallException('Expectation value is not iterable.');
}
if ($callbacks === []) {
if (count($callbacks) == 0) {
throw new InvalidArgumentException('No sequence expectations defined.');
}
@ -264,7 +261,7 @@ final class Expectation
$matched = false;
foreach ($expressions as $key => $callback) {
if ($subject != $key) { // @pest-arch-ignore-line
if ($subject != $key) {
continue;
}
@ -380,7 +377,7 @@ final class Expectation
if (self::hasExtend($name)) {
$extend = self::$extends[$name]->bindTo($this, Expectation::class);
if ($extend != false) { // @pest-arch-ignore-line
if ($extend != false) {
return $extend;
}
}
@ -437,71 +434,6 @@ final class Expectation
return ToUse::make($this, $targets);
}
/**
* Asserts that the given expectation target does have the given permissions
*/
public function toHaveFileSystemPermissions(string $permissions): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) === $permissions,
sprintf('permissions to be [%s]', $permissions),
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
);
}
/**
* Asserts that the given expectation target to have line count less than the given number.
*/
public function toHaveLineCountLessThan(int $lines): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => count(file($object->path)) < $lines, // @phpstan-ignore-line
sprintf('to have less than %d lines of code', $lines),
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
);
}
/**
* Asserts that the given expectation target have all methods documented.
*/
public function toHaveMethodsDocumented(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| array_filter(
Reflection::getMethodsFromReflectionClass($object->reflectionClass),
fn (ReflectionMethod $method): bool => (enum_exists($object->name) === false || in_array($method->name, ['from', 'tryFrom', 'cases'], true) === false)
&& realpath($method->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
&& $method->getDocComment() === false,
) === [],
'to have methods with documentation / annotations',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
);
}
/**
* Asserts that the given expectation target have all properties documented.
*/
public function toHavePropertiesDocumented(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| array_filter(
Reflection::getPropertiesFromReflectionClass($object->reflectionClass),
fn (ReflectionProperty $property): bool => (enum_exists($object->name) === false || in_array($property->name, ['value', 'name'], true) === false)
&& realpath($property->getDeclaringClass()->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
&& $property->isPromoted() === false
&& $property->getDocComment() === false,
) === [],
'to have properties with documentation / annotations',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
);
}
/**
* Asserts that the given expectation target use the "declare(strict_types=1)" declaration.
*/
@ -509,25 +441,12 @@ final class Expectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => (bool) preg_match('/^<\?php\s*(\/\*[\s\S]*?\*\/|\/\/[^\r\n]*(?:\r?\n|$)|\s)*declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;/m', (string) file_get_contents($object->path)),
fn (ObjectDescription $object): bool => (bool) preg_match('/^<\?php\s+declare\(.*?strict_types\s?=\s?1.*?\);/', (string) file_get_contents($object->path)),
'to use strict types',
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
);
}
/**
* Asserts that the given expectation target uses strict equality.
*/
public function toUseStrictEquality(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' == ') && ! str_contains((string) file_get_contents($object->path), ' != '),
'to use strict equality',
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' == ') || str_contains($line, ' != ')),
);
}
/**
* Asserts that the given expectation target is final.
*/
@ -590,79 +509,17 @@ final class Expectation
/**
* Asserts that the given expectation target has a specific method.
*
* @param array<int, string>|string $method
*/
public function toHaveMethod(array|string $method): ArchExpectation
public function toHaveMethod(string $method): ArchExpectation
{
$methods = is_array($method) ? $method : [$method];
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => $object->reflectionClass->hasMethod($method))) === count($methods),
sprintf("to have method '%s'", implode("', '", $methods)),
fn (ObjectDescription $object): bool => $object->reflectionClass->hasMethod($method),
sprintf("to have method '%s'", $method),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target has a specific methods.
*
* @param array<int, string> $methods
*/
public function toHaveMethods(array $methods): ArchExpectation
{
return $this->toHaveMethod($methods);
}
/**
* Not supported.
*/
public function toHavePublicMethodsBesides(): void
{
throw InvalidExpectation::fromMethods(['toHavePublicMethodsBesides']);
}
/**
* Not supported.
*/
public function toHavePublicMethods(): void
{
throw InvalidExpectation::fromMethods(['toHavePublicMethods']);
}
/**
* Not supported.
*/
public function toHaveProtectedMethodsBesides(): void
{
throw InvalidExpectation::fromMethods(['toHaveProtectedMethodsBesides']);
}
/**
* Not supported.
*/
public function toHaveProtectedMethods(): void
{
throw InvalidExpectation::fromMethods(['toHaveProtectedMethods']);
}
/**
* Not supported.
*/
public function toHavePrivateMethodsBesides(): void
{
throw InvalidExpectation::fromMethods(['toHavePrivateMethodsBesides']);
}
/**
* Not supported.
*/
public function toHavePrivateMethods(): void
{
throw InvalidExpectation::fromMethods(['toHavePrivateMethods']);
}
/**
* Asserts that the given expectation target is enum.
*/
@ -728,6 +585,8 @@ final class Expectation
/**
* Asserts that the given expectation target to be subclass of the given class.
*
* @param class-string $class
*/
public function toExtend(string $class): ArchExpectation
{
@ -752,39 +611,6 @@ final class Expectation
);
}
/**
* Asserts that the given expectation target to use the given trait.
*/
public function toUseTrait(string $trait): ArchExpectation
{
return $this->toUseTraits($trait);
}
/**
* Asserts that the given expectation target to use the given traits.
*
* @param array<int, string>|string $traits
*/
public function toUseTraits(array|string $traits): ArchExpectation
{
$traits = is_array($traits) ? $traits : [$traits];
return Targeted::make(
$this,
function (ObjectDescription $object) use ($traits): bool {
foreach ($traits as $trait) {
if (! in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
return false;
}
}
return true;
},
"to use traits '".implode("', '", $traits)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target to not implement any interfaces.
*/
@ -801,7 +627,7 @@ final class Expectation
/**
* Asserts that the given expectation target to only implement the given interfaces.
*
* @param array<int, string>|string $interfaces
* @param array<int, class-string>|class-string $interfaces
*/
public function toOnlyImplement(array|string $interfaces): ArchExpectation
{
@ -845,7 +671,7 @@ final class Expectation
/**
* Asserts that the given expectation target to implement the given interfaces.
*
* @param array<int, string>|string $interfaces
* @param array<int, class-string>|class-string $interfaces
*/
public function toImplement(array|string $interfaces): ArchExpectation
{
@ -885,10 +711,7 @@ final class Expectation
return ToUseNothing::make($this);
}
/**
* Not supported.
*/
public function toBeUsed(): void
public function toBeUsed(): never
{
throw InvalidExpectation::fromMethods(['toBeUsed']);
}
@ -1032,6 +855,8 @@ final class Expectation
/**
* Asserts that the given expectation target to have the given attribute.
*
* @param class-string<Attribute> $attribute
*/
public function toHaveAttribute(string $attribute): ArchExpectation
{

View File

@ -17,9 +17,6 @@ use function expect;
*/
final class EachExpectation
{
/**
* Indicates if the expectation is the opposite.
*/
private bool $opposite = false;
/**

View File

@ -25,14 +25,8 @@ final class HigherOrderExpectation
*/
private Expectation|EachExpectation $expectation;
/**
* Indicates if the expectation is the opposite.
*/
private bool $opposite = false;
/**
* Indicates if the expectation should reset the value.
*/
private bool $shouldReset = false;
/**

View File

@ -18,13 +18,9 @@ use Pest\Exceptions\InvalidExpectation;
use Pest\Expectation;
use Pest\Support\Arr;
use Pest\Support\Exporter;
use Pest\Support\Reflection;
use PHPUnit\Architecture\Elements\ObjectDescription;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExpectationFailedException;
use ReflectionMethod;
use ReflectionProperty;
use stdClass;
/**
* @internal
@ -33,14 +29,14 @@ use stdClass;
*
* @mixin Expectation<TValue>
*/
final readonly class OppositeExpectation
final class OppositeExpectation
{
/**
* Creates a new opposite expectation.
*
* @param Expectation<TValue> $original
*/
public function __construct(private Expectation $original) {}
public function __construct(private readonly Expectation $original) {}
/**
* Asserts that the value array not has the provided $keys.
@ -79,66 +75,6 @@ final readonly class OppositeExpectation
), is_string($targets) ? [$targets] : $targets));
}
/**
* Asserts that the given expectation target does not have the given permissions
*/
public function toHaveFileSystemPermissions(string $permissions): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) !== $permissions,
sprintf('permissions not to be [%s]', $permissions),
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
);
}
/**
* Not supported.
*/
public function toHaveLineCountLessThan(): ArchExpectation
{
throw InvalidExpectation::fromMethods(['not', 'toHaveLineCountLessThan']);
}
/**
* Not supported.
*/
public function toHaveMethodsDocumented(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| array_filter(
Reflection::getMethodsFromReflectionClass($object->reflectionClass),
fn (ReflectionMethod $method): bool => (enum_exists($object->name) === false || in_array($method->name, ['from', 'tryFrom', 'cases'], true) === false)
&& realpath($method->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
&& $method->getDocComment() !== false,
) === [],
'to have methods without documentation / annotations',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
);
}
/**
* Not supported.
*/
public function toHavePropertiesDocumented(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| array_filter(
Reflection::getPropertiesFromReflectionClass($object->reflectionClass),
fn (ReflectionProperty $property): bool => (enum_exists($object->name) === false || in_array($property->name, ['value', 'name'], true) === false)
&& realpath($property->getDeclaringClass()->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
&& $property->isPromoted() === false
&& $property->getDocComment() !== false,
) === [],
'to have properties without documentation / annotations',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
);
}
/**
* Asserts that the given expectation target does not use the "declare(strict_types=1)" declaration.
*/
@ -152,19 +88,6 @@ final readonly class OppositeExpectation
);
}
/**
* Asserts that the given expectation target does not use the strict equality operator.
*/
public function toUseStrictEquality(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' === ') && ! str_contains((string) file_get_contents($object->path), ' !== '),
'to use strict equality',
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' === ') || str_contains($line, ' !== ')),
);
}
/**
* Asserts that the given expectation target is not final.
*/
@ -227,163 +150,17 @@ final readonly class OppositeExpectation
/**
* Asserts that the given expectation target does not have a specific method.
*
* @param array<int, string>|string $method
*/
public function toHaveMethod(array|string $method): ArchExpectation
public function toHaveMethod(string $method): ArchExpectation
{
$methods = is_array($method) ? $method : [$method];
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => array_filter(
$methods,
fn (string $method): bool => $object->reflectionClass->hasMethod($method),
) === [],
'to not have methods: '.implode(', ', $methods),
fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod($method),
'to not have method',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target does not have the given methods.
*
* @param array<int, string> $methods
*/
public function toHaveMethods(array $methods): ArchExpectation
{
return $this->toHaveMethod($methods);
}
/**
* Asserts that the given expectation target not to have the public methods besides the given methods.
*
* @param array<int, string>|string $methods
*/
public function toHavePublicMethodsBesides(array|string $methods): ArchExpectation
{
$methods = is_array($methods) ? $methods : [$methods];
$state = new stdClass;
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PUBLIC)
: [];
foreach ($reflectionMethods as $reflectionMethod) {
if (! in_array($reflectionMethod->name, $methods, true)) {
$state->contains = 'public function '.$reflectionMethod->name;
return false;
}
}
return true;
},
$methods === []
? 'not to have public methods'
: sprintf("not to have public methods besides '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
);
}
/**
* Asserts that the given expectation target not to have the public methods.
*/
public function toHavePublicMethods(): ArchExpectation
{
return $this->toHavePublicMethodsBesides([]);
}
/**
* Asserts that the given expectation target not to have the protected methods besides the given methods.
*
* @param array<int, string>|string $methods
*/
public function toHaveProtectedMethodsBesides(array|string $methods): ArchExpectation
{
$methods = is_array($methods) ? $methods : [$methods];
$state = new stdClass;
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PROTECTED)
: [];
foreach ($reflectionMethods as $reflectionMethod) {
if (! in_array($reflectionMethod->name, $methods, true)) {
$state->contains = 'protected function '.$reflectionMethod->name;
return false;
}
}
return true;
},
$methods === []
? 'not to have protected methods'
: sprintf("not to have protected methods besides '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
);
}
/**
* Asserts that the given expectation target not to have the protected methods.
*/
public function toHaveProtectedMethods(): ArchExpectation
{
return $this->toHaveProtectedMethodsBesides([]);
}
/**
* Asserts that the given expectation target not to have the private methods besides the given methods.
*
* @param array<int, string>|string $methods
*/
public function toHavePrivateMethodsBesides(array|string $methods): ArchExpectation
{
$methods = is_array($methods) ? $methods : [$methods];
$state = new stdClass;
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PRIVATE)
: [];
foreach ($reflectionMethods as $reflectionMethod) {
if (! in_array($reflectionMethod->name, $methods, true)) {
$state->contains = 'private function '.$reflectionMethod->name;
return false;
}
}
return true;
},
$methods === []
? 'not to have private methods'
: sprintf("not to have private methods besides '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
);
}
/**
* Asserts that the given expectation target not to have the private methods.
*/
public function toHavePrivateMethods(): ArchExpectation
{
return $this->toHavePrivateMethodsBesides([]);
}
/**
* Asserts that the given expectation target is not enum.
*/
@ -449,6 +226,8 @@ final readonly class OppositeExpectation
/**
* Asserts that the given expectation target to be not subclass of the given class.
*
* @param class-string $class
*/
public function toExtend(string $class): ArchExpectation
{
@ -473,43 +252,10 @@ final readonly class OppositeExpectation
);
}
/**
* Asserts that the given expectation target not to use the given trait.
*/
public function toUseTrait(string $trait): ArchExpectation
{
return $this->toUseTraits($trait);
}
/**
* Asserts that the given expectation target not to use the given traits.
*
* @param array<int, string>|string $traits
*/
public function toUseTraits(array|string $traits): ArchExpectation
{
$traits = is_array($traits) ? $traits : [$traits];
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($traits): bool {
foreach ($traits as $trait) {
if (in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
return false;
}
}
return true;
},
"not to use traits '".implode("', '", $traits)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target not to implement the given interfaces.
*
* @param array<int, string>|string $interfaces
* @param array<int, class-string>|string $interfaces
*/
public function toImplement(array|string $interfaces): ArchExpectation
{
@ -546,8 +292,10 @@ final readonly class OppositeExpectation
/**
* Not supported.
*
* @param array<int, class-string>|string $interfaces
*/
public function toOnlyImplement(): void
public function toOnlyImplement(array|string $interfaces): never
{
throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']);
}
@ -580,8 +328,10 @@ final readonly class OppositeExpectation
/**
* Not supported.
*
* @param array<int, string>|string $targets
*/
public function toOnlyUse(): void
public function toOnlyUse(array|string $targets): never
{
throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']);
}
@ -589,7 +339,7 @@ final readonly class OppositeExpectation
/**
* Not supported.
*/
public function toUseNothing(): void
public function toUseNothing(): never
{
throw InvalidExpectation::fromMethods(['not', 'toUseNothing']);
}
@ -614,7 +364,7 @@ final readonly class OppositeExpectation
), is_string($targets) ? [$targets] : $targets));
}
public function toOnlyBeUsedIn(): void
public function toOnlyBeUsedIn(): never
{
throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedIn']);
}
@ -622,7 +372,7 @@ final readonly class OppositeExpectation
/**
* Asserts that the given expectation dependency is not used.
*/
public function toBeUsedInNothing(): void
public function toBeUsedInNothing(): never
{
throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']);
}
@ -642,6 +392,8 @@ final readonly class OppositeExpectation
/**
* Asserts that the given expectation target not to have the given attribute.
*
* @param class-string<Attribute> $attribute
*/
public function toHaveAttribute(string $attribute): ArchExpectation
{

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Contracts\AddsAnnotations;
use Pest\Factories\Covers\CoversNothing as CoversNothingFactory;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
final class CoversNothing implements AddsAnnotations
{
/**
* {@inheritdoc}
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{
if (($method->covers[0] ?? null) instanceof CoversNothingFactory) {
$annotations[] = '@coversNothing';
}
return $annotations;
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Contracts\AddsAnnotations;
use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Str;
/**
* @internal
*/
final class Depends implements AddsAnnotations
{
/**
* {@inheritdoc}
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{
foreach ($method->depends as $depend) {
$depend = Str::evaluable($method->describing !== null ? Str::describe($method->describing, $depend) : $depend);
$annotations[] = "@depends $depend";
}
return $annotations;
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Contracts\AddsAnnotations;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
final class Groups implements AddsAnnotations
{
/**
* {@inheritdoc}
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{
foreach ($method->groups as $group) {
$annotations[] = "@group $group";
}
return $annotations;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Contracts\AddsAnnotations;
use Pest\Factories\TestCaseMethodFactory;
final class TestDox implements AddsAnnotations
{
/**
* {@inheritdoc}
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{
/*
* Escapes docblock according to
* https://manual.phpdoc.org/HTMLframesConverter/default/phpDocumentor/tutorial_phpDocumentor.howto.pkg.html#basics.desc
*
* Note: '@' escaping is not needed as it cannot be the first character of the line (it always starts with @testdox).
*/
assert($method->description !== null);
$methodDescription = str_replace('*/', '{@*}', $method->description);
$annotations[] = "@testdox $methodDescription";
return $annotations;
}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Factories;
/**
* @internal
*/
final class Attribute
{
/**
* @param iterable<int, string> $arguments
*/
public function __construct(public string $name, public iterable $arguments)
{
//
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Attributes;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
abstract class Attribute
{
/**
* Determine if the attribute should be placed above the class instead of above the method.
*/
public static bool $above = false;
/**
* @param array<int, string> $attributes
* @return array<int, string>
*/
public function __invoke(TestCaseMethodFactory $method, array $attributes): array
{
return $attributes;
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Attributes;
use Pest\Factories\Covers\CoversClass;
use Pest\Factories\Covers\CoversFunction;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
final class Covers extends Attribute
{
/**
* Determine if the attribute should be placed above the class instead of above the method.
*/
public static bool $above = true;
/**
* Adds attributes regarding the "covers" feature.
*
* @param array<int, string> $attributes
* @return array<int, string>
*/
public function __invoke(TestCaseMethodFactory $method, array $attributes): array
{
foreach ($method->covers as $covering) {
if ($covering instanceof CoversClass) {
// Prepend a backslash for FQN classes
if (str_contains($covering->class, '\\')) {
$covering->class = '\\'.$covering->class;
}
$attributes[] = "#[\PHPUnit\Framework\Attributes\CoversClass({$covering->class}::class)]";
} elseif ($covering instanceof CoversFunction) {
$attributes[] = "#[\PHPUnit\Framework\Attributes\CoversFunction('{$covering->function}')]";
}
}
return $attributes;
}
}

View File

@ -6,8 +6,8 @@ namespace Pest\Factories;
use ParseError;
use Pest\Concerns;
use Pest\Contracts\AddsAnnotations;
use Pest\Contracts\HasPrintableTestCaseName;
use Pest\Evaluators\Attributes;
use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Exceptions\TestAlreadyExist;
@ -27,12 +27,26 @@ final class TestCaseFactory
{
use HigherOrderable;
/**
* The list of annotations.
*
* @var array<int, class-string<AddsAnnotations>>
*/
private const ANNOTATIONS = [
Annotations\Depends::class,
Annotations\Groups::class,
Annotations\CoversNothing::class,
Annotations\TestDox::class,
];
/**
* The list of attributes.
*
* @var array<int, Attribute>
* @var array<int, class-string<\Pest\Factories\Attributes\Attribute>>
*/
public array $attributes = [];
private const ATTRIBUTES = [
Attributes\Covers::class,
];
/**
* The FQN of the Test Case class.
@ -133,21 +147,32 @@ final class TestCaseFactory
$className = 'InvalidTestName'.Str::random();
}
$this->attributes = [
new Attribute(
\PHPUnit\Framework\Attributes\TestDox::class,
[$this->filename],
),
...$this->attributes,
];
$classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => $attribute::$above);
$methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => ! $attribute::$above);
$attributesCode = Attributes::code($this->attributes);
$classAttributes = [];
foreach ($classAvailableAttributes as $attribute) {
$classAttributes = array_reduce(
$methods,
fn (array $carry, TestCaseMethodFactory $methodFactory): array => (new $attribute)->__invoke($methodFactory, $carry),
$classAttributes
);
}
$methodsCode = implode('', array_map(
fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(),
fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(
self::ANNOTATIONS,
$methodAvailableAttributes
),
$methods
));
$classAttributesCode = implode('', array_map(
static fn (string $attribute): string => sprintf("\n%s", $attribute),
array_unique($classAttributes),
));
try {
$classCode = <<<PHP
namespace $namespace;
@ -155,7 +180,10 @@ final class TestCaseFactory
use Pest\Repositories\DatasetsRepository as __PestDatasets;
use Pest\TestSuite as __PestTestSuite;
$attributesCode
/**
* @testdox $filename
*/
$classAttributesCode
#[\AllowDynamicProperties]
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode

View File

@ -5,14 +5,13 @@ declare(strict_types=1);
namespace Pest\Factories;
use Closure;
use Pest\Evaluators\Attributes;
use Pest\Contracts\AddsAnnotations;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Factories\Concerns\HigherOrderable;
use Pest\Repositories\DatasetsRepository;
use Pest\Support\Str;
use Pest\TestSuite;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
/**
@ -22,24 +21,10 @@ final class TestCaseMethodFactory
{
use HigherOrderable;
/**
* The list of attributes.
*
* @var array<int, Attribute>
*/
public array $attributes = [];
/**
* The test's describing, if any.
*
* @var array<int, string>
*/
public array $describing = [];
/**
* The test's description, if any.
*/
public ?string $description = null;
public ?string $describing = null;
/**
* The test's number of repetitions.
@ -51,34 +36,6 @@ final class TestCaseMethodFactory
*/
public bool $todo = false;
/**
* The associated issue numbers.
*
* @var array<int, int>
*/
public array $issues = [];
/**
* The test assignees.
*
* @var array<int, string>
*/
public array $assignees = [];
/**
* The associated PRs numbers.
*
* @var array<int, int>
*/
public array $prs = [];
/**
* The test's notes.
*
* @var array<int, string>
*/
public array $notes = [];
/**
* The test's datasets.
*
@ -101,15 +58,18 @@ final class TestCaseMethodFactory
public array $groups = [];
/**
* @see This property is not actually used in the codebase, it's only here to make Rector happy.
* The covered classes and functions.
*
* @var array<int, \Pest\Factories\Covers\CoversClass|\Pest\Factories\Covers\CoversFunction|\Pest\Factories\Covers\CoversNothing>
*/
public bool $__ran = false;
public array $covers = [];
/**
* Creates a new test case method factory instance.
*/
public function __construct(
public string $filename,
public ?string $description,
public ?Closure $closure,
) {
$this->closure ??= function (): void {
@ -120,9 +80,9 @@ final class TestCaseMethodFactory
}
/**
* Sets the test's hooks, and runs any proxy to the test case.
* Creates the test's closure.
*/
public function setUp(TestCase $concrete): void
public function getClosure(TestCase $concrete): Closure
{
$concrete::flush(); // @phpstan-ignore-line
@ -130,32 +90,16 @@ final class TestCaseMethodFactory
throw ShouldNotHappen::fromMessage('Description can not be empty.');
}
$closure = $this->closure;
$testCase = TestSuite::getInstance()->tests->get($this->filename);
assert($testCase instanceof TestCaseFactory);
$testCase->factoryProxies->proxy($concrete);
$this->factoryProxies->proxy($concrete);
}
/**
* Flushes the test case.
*/
public function tearDown(TestCase $concrete): void
{
$concrete::flush(); // @phpstan-ignore-line
}
/**
* Creates the test's closure.
*/
public function getClosure(): Closure
{
$closure = $this->closure;
$testCase = TestSuite::getInstance()->tests->get($this->filename);
assert($testCase instanceof TestCaseFactory);
$method = $this;
return function (...$arguments) use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line
return function () use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line
/* @var TestCase $this */
$testCase->proxies->proxy($this);
$method->proxies->proxy($this);
@ -163,9 +107,7 @@ final class TestCaseMethodFactory
$testCase->chains->chain($this);
$method->chains->chain($this);
$this->__ran = true;
return \Pest\Support\Closure::bind($closure, $this, self::class)(...$arguments);
return \Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args());
};
}
@ -174,13 +116,16 @@ final class TestCaseMethodFactory
*/
public function receivesArguments(): bool
{
return $this->datasets !== [] || $this->depends !== [] || $this->repetitions > 1;
return $this->datasets !== [] || $this->depends !== [];
}
/**
* Creates a PHPUnit method as a string ready for evaluation.
*
* @param array<int, class-string<AddsAnnotations>> $annotationsToUse
* @param array<int, class-string<\Pest\Factories\Attributes\Attribute>> $attributesToUse
*/
public function buildForEvaluation(): string
public function buildForEvaluation(array $annotationsToUse, array $attributesToUse): string
{
if ($this->description === null) {
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
@ -189,49 +134,46 @@ final class TestCaseMethodFactory
$methodName = Str::evaluable($this->description);
$datasetsCode = '';
$annotations = ['@test'];
$attributes = [];
$this->attributes = [
new Attribute(
\PHPUnit\Framework\Attributes\Test::class,
[],
),
new Attribute(
\PHPUnit\Framework\Attributes\TestDox::class,
[str_replace('*/', '{@*}', $this->description)],
),
...$this->attributes,
];
foreach ($annotationsToUse as $annotation) {
$annotations = (new $annotation)->__invoke($this, $annotations);
}
foreach ($this->depends as $depend) {
$depend = Str::evaluable($this->describing === [] ? $depend : Str::describe($this->describing, $depend));
$this->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\Depends::class,
[$depend],
);
foreach ($attributesToUse as $attribute) {
$attributes = (new $attribute)->__invoke($this, $attributes);
}
if ($this->datasets !== [] || $this->repetitions > 1) {
$dataProviderName = $methodName.'_dataset';
$this->attributes[] = new Attribute(
DataProvider::class,
[$dataProviderName],
);
$annotations[] = "@dataProvider $dataProviderName";
$datasetsCode = $this->buildDatasetForEvaluation($methodName, $dataProviderName);
}
$attributesCode = Attributes::code($this->attributes);
$annotations = implode('', array_map(
static fn (string $annotation): string => sprintf("\n * %s", $annotation), $annotations,
));
$attributes = implode('', array_map(
static fn (string $attribute): string => sprintf("\n %s", $attribute), $attributes,
));
return <<<PHP
$attributesCode
public function $methodName(...\$arguments)
/**$annotations
*/
$attributes
public function $methodName()
{
\$test = \Pest\TestSuite::getInstance()->tests->get(self::\$__filename)->getMethod(\$this->name())->getClosure(\$this);
return \$this->__runTest(
\$this->__test,
...\$arguments,
\$test,
...func_get_args(),
);
}
$datasetsCode
$datasetsCode
PHP;
}

View File

@ -3,12 +3,9 @@
declare(strict_types=1);
use Pest\Concerns\Expectable;
use Pest\Configuration;
use Pest\Exceptions\AfterAllWithinDescribe;
use Pest\Exceptions\BeforeAllWithinDescribe;
use Pest\Expectation;
use Pest\Mutate\Contracts\MutationTestRunner;
use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\AfterEachCall;
use Pest\PendingCalls\BeforeEachCall;
use Pest\PendingCalls\DescribeCall;
@ -16,7 +13,6 @@ use Pest\PendingCalls\TestCall;
use Pest\PendingCalls\UsesCall;
use Pest\Repositories\DatasetsRepository;
use Pest\Support\Backtrace;
use Pest\Support\Container;
use Pest\Support\DatasetInfo;
use Pest\Support\HigherOrderTapProxy;
use Pest\TestSuite;
@ -43,7 +39,7 @@ if (! function_exists('beforeAll')) {
*/
function beforeAll(Closure $closure): void
{
if (DescribeCall::describing() !== []) {
if (! is_null(DescribeCall::describing())) {
$filename = Backtrace::file();
throw new BeforeAllWithinDescribe($filename);
@ -57,8 +53,6 @@ if (! function_exists('beforeEach')) {
/**
* Runs the given closure before each test in the current file.
*
* @param-closure-this TestCase $closure
*
* @return HigherOrderTapProxy<Expectable|TestCall|TestCase>|Expectable|TestCall|TestCase|mixed
*/
function beforeEach(?Closure $closure = null): BeforeEachCall
@ -114,24 +108,12 @@ if (! function_exists('uses')) {
}
}
if (! function_exists('pest')) {
/**
* Creates a new Pest configuration instance.
*/
function pest(): Configuration
{
return new Configuration(Backtrace::file());
}
}
if (! function_exists('test')) {
/**
* Adds the given closure as a test. The first argument
* is the test description; the second argument is
* a closure that contains the test expectations.
*
* @param-closure-this TestCase $closure
*
* @return Expectable|TestCall|TestCase|mixed
*/
function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall
@ -152,8 +134,6 @@ if (! function_exists('it')) {
* is the test description; the second argument is
* a closure that contains the test expectations.
*
* @param-closure-this TestCase $closure
*
* @return Expectable|TestCall|TestCase|mixed
*/
function it(string $description, ?Closure $closure = null): TestCall
@ -169,7 +149,9 @@ if (! function_exists('it')) {
if (! function_exists('todo')) {
/**
* Creates a new test that is marked as "todo".
* Adds the given todo test. Internally, this test
* is marked as incomplete. Yet, Collision, Pest's
* printer, will display it as a "todo" test.
*
* @return Expectable|TestCall|TestCase|mixed
*/
@ -187,8 +169,6 @@ if (! function_exists('afterEach')) {
/**
* Runs the given closure after each test in the current file.
*
* @param-closure-this TestCase $closure
*
* @return Expectable|HigherOrderTapProxy<Expectable|TestCall|TestCase>|TestCall|mixed
*/
function afterEach(?Closure $closure = null): AfterEachCall
@ -205,7 +185,7 @@ if (! function_exists('afterAll')) {
*/
function afterAll(Closure $closure): void
{
if (DescribeCall::describing() !== []) {
if (! is_null(DescribeCall::describing())) {
$filename = Backtrace::file();
throw new AfterAllWithinDescribe($filename);
@ -214,67 +194,3 @@ if (! function_exists('afterAll')) {
TestSuite::getInstance()->afterAll->set($closure);
}
}
if (! function_exists('covers')) {
/**
* Specifies which classes, or functions, a test case covers.
*
* @param array<int, string>|string $classesOrFunctions
*/
function covers(array|string ...$classesOrFunctions): void
{
$filename = Backtrace::file();
$beforeEachCall = (new BeforeEachCall(TestSuite::getInstance(), $filename));
$beforeEachCall->covers(...$classesOrFunctions);
$beforeEachCall->group('__pest_mutate_only');
/** @var MutationTestRunner $runner */
$runner = Container::getInstance()->get(MutationTestRunner::class);
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if ($runner->isEnabled() && ! $everything && ! is_array($classes) && ! is_array($paths)) {
$beforeEachCall->only('__pest_mutate_only');
}
}
}
if (! function_exists('mutates')) {
/**
* Specifies which classes, enums, or traits a test case mutates.
*
* @param array<int, string>|string $targets
*/
function mutates(array|string ...$targets): void
{
$filename = Backtrace::file();
$beforeEachCall = (new BeforeEachCall(TestSuite::getInstance(), $filename));
$beforeEachCall->group('__pest_mutate_only');
/** @var MutationTestRunner $runner */
$runner = Container::getInstance()->get(MutationTestRunner::class);
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if ($runner->isEnabled() && ! $everything && ! is_array($classes) && ! is_array($paths)) {
$beforeEachCall->only('__pest_mutate_only');
}
/** @var ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if (! is_array($paths)) {
$configurationRepository->globalConfiguration('default')->class(...$targets); // @phpstan-ignore-line
}
}
}

View File

@ -27,7 +27,7 @@ use Whoops\Exception\Inspector;
/**
* @internal
*/
final readonly class Kernel
final class Kernel
{
/**
* The Kernel bootstrappers.
@ -47,8 +47,8 @@ final readonly class Kernel
* Creates a new Kernel instance.
*/
public function __construct(
private Application $application,
private OutputInterface $output,
private readonly Application $application,
private readonly OutputInterface $output,
) {
//
}

View File

@ -11,7 +11,6 @@ use Pest\Support\Str;
use PHPUnit\Event\Code\Test;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Code\Throwable;
use PHPUnit\Event\Test\AfterLastTestMethodErrored;
use PHPUnit\Event\Test\BeforeFirstTestMethodErrored;
use PHPUnit\Event\Test\ConsideredRisky;
use PHPUnit\Event\Test\Errored;
@ -19,30 +18,23 @@ use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestSuite\TestSuite;
use PHPUnit\Event\TestSuite\TestSuiteForTestMethodWithDataProvider;
use PHPUnit\Framework\Exception as FrameworkException;
use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult;
/**
* @internal
*/
final readonly class Converter
final class Converter
{
/**
* The prefix for the test suite name.
*/
private const PREFIX = 'P\\';
/**
* The state generator.
*/
private StateGenerator $stateGenerator;
private readonly StateGenerator $stateGenerator;
/**
* Creates a new instance of the Converter.
*/
public function __construct(
private string $rootPath,
private readonly string $rootPath,
) {
$this->stateGenerator = new StateGenerator;
}
@ -137,7 +129,7 @@ final readonly class Converter
// Format stacktrace as `at <path>`
$frames = array_map(
fn (string $frame): string => "at $frame",
fn (string $frame) => "at $frame",
$frames
);
@ -149,13 +141,6 @@ final readonly class Converter
*/
public function getTestSuiteName(TestSuite $testSuite): string
{
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
$firstTest = $this->getFirstTest($testSuite);
if ($firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
return $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
}
}
$name = $testSuite->name();
if (! str_starts_with($name, self::PREFIX)) {
@ -177,35 +162,6 @@ final readonly class Converter
* Gets the test suite location.
*/
public function getTestSuiteLocation(TestSuite $testSuite): ?string
{
$firstTest = $this->getFirstTest($testSuite);
if (! $firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
return null;
}
$path = $firstTest->testDox()->prettifiedClassName();
$classRelativePath = $this->toRelativePath($path);
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
$methodName = $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
return "$classRelativePath::$methodName";
}
return $classRelativePath;
}
/**
* Gets the prettified test method name without dataset-related suffix.
*/
private function getTestMethodNameWithoutDatasetSuffix(TestMethod $testMethod): string
{
return Str::beforeLast($testMethod->testDox()->prettifiedMethodName(), ' with data set ');
}
/**
* Gets the first test from the test suite.
*/
private function getFirstTest(TestSuite $testSuite): ?TestMethod
{
$tests = $testSuite->tests()->asArray();
@ -219,7 +175,9 @@ final readonly class Converter
throw ShouldNotHappen::fromMessage('Not an instance of TestMethod');
}
return $firstTest;
$path = $firstTest->testDox()->prettifiedClassName();
return $this->toRelativePath($path);
}
/**
@ -255,9 +213,8 @@ final readonly class Converter
$numberOfNotPassedTests = count(
array_unique(
array_map(
function (AfterLastTestMethodErrored|BeforeFirstTestMethodErrored|Errored|Failed|Skipped|ConsideredRisky|MarkedIncomplete $event): string {
if ($event instanceof BeforeFirstTestMethodErrored
|| $event instanceof AfterLastTestMethodErrored) {
function (BeforeFirstTestMethodErrored|Errored|Failed|Skipped|ConsideredRisky|MarkedIncomplete $event): string {
if ($event instanceof BeforeFirstTestMethodErrored) {
return $event->testClassName();
}

View File

@ -9,9 +9,6 @@ namespace Pest\Logging\TeamCity;
*/
final class ServiceMessage
{
/**
* The flow ID.
*/
private static ?int $flowId = null;
/**
@ -38,7 +35,7 @@ final class ServiceMessage
{
return new self('testSuiteStarted', [
'name' => $name,
'locationHint' => $location === null ? null : "pest_qn://$location",
'locationHint' => $location === null ? null : "file://$location",
]);
}

View File

@ -9,7 +9,7 @@ use Pest\Logging\TeamCity\TeamCityLogger;
/**
* @internal
*/
abstract class Subscriber // @pest-arch-ignore-line
abstract class Subscriber
{
/**
* Creates a new Subscriber instance.
@ -19,7 +19,7 @@ abstract class Subscriber // @pest-arch-ignore-line
/**
* Creates a new TeamCityLogger instance.
*/
final protected function logger(): TeamCityLogger // @pest-arch-ignore-line
final protected function logger(): TeamCityLogger
{
return $this->logger;
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\MarkedIncompleteSubscriber;
/**
* @internal
*/
final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber
{
public function notify(MarkedIncomplete $event): void
{
$this->logger()->testMarkedIncomplete($event);
}
}

View File

@ -12,6 +12,7 @@ use Pest\Logging\TeamCity\Subscriber\TestErroredSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestExecutionFinishedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestFailedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestFinishedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestMarkedIncompleteSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestPreparedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestSkippedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestSuiteFinishedSubscriber;
@ -27,6 +28,7 @@ use PHPUnit\Event\Test\ConsideredRisky;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestRunner\ExecutionFinished;
@ -43,14 +45,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
final class TeamCityLogger
{
/**
* The current time.
*/
private ?HRTime $time = null;
/**
* Indicates if the summary test count has been printed.
*/
private bool $isSummaryTestCountPrinted = false;
/**
@ -112,7 +108,7 @@ final class TeamCityLogger
$this->time = $event->telemetryInfo()->time();
}
public function testMarkedIncomplete(): never
public function testMarkedIncomplete(MarkedIncomplete $event): never
{
throw ShouldNotHappen::fromMessage('testMarkedIncomplete not implemented.');
}
@ -266,6 +262,7 @@ final class TeamCityLogger
new TestFinishedSubscriber($this),
new TestErroredSubscriber($this),
new TestFailedSubscriber($this),
new TestMarkedIncompleteSubscriber($this),
new TestSkippedSubscriber($this),
new TestConsideredRiskySubscriber($this),
new TestExecutionFinishedSubscriber($this),

View File

@ -344,6 +344,36 @@ final class Expectation
return $this;
}
/**
* Asserts that the value has the method $name.
*
* @return self<TValue>
*/
public function toHaveMethod(string $name, string $message = ''): self
{
$this->toBeObject();
// @phpstan-ignore-next-line
Assert::assertTrue(method_exists($this->value, $name), $message);
return $this;
}
/**
* Asserts that the value has the provided methods $names.
*
* @param iterable<array-key, string> $names
* @return self<TValue>
*/
public function toHaveMethods(iterable $names, string $message = ''): self
{
foreach ($names as $name) {
$this->toHaveMethod($name, message: $message);
}
return $this;
}
/**
* Asserts that two variables have the same value.
*

View File

@ -5,20 +5,19 @@ declare(strict_types=1);
namespace Pest;
use NunoMaduro\Collision\Writer;
use Pest\Exceptions\TestDescriptionMissing;
use Pest\Support\Container;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use Whoops\Exception\Inspector;
final readonly class Panic
final class Panic
{
/**
* Creates a new Panic instance.
*/
private function __construct(
private Throwable $throwable
private readonly Throwable $throwable
) {
// ...
}
@ -28,10 +27,6 @@ final readonly class Panic
*/
public static function with(Throwable $throwable): never
{
if ($throwable instanceof TestDescriptionMissing && ! is_null($previous = $throwable->getPrevious())) {
$throwable = $previous;
}
$panic = new self($throwable);
$panic->handle();

View File

@ -6,7 +6,6 @@ namespace Pest\PendingCalls;
use Closure;
use Pest\PendingCalls\Concerns\Describable;
use Pest\Support\Arr;
use Pest\Support\Backtrace;
use Pest\Support\ChainableClosure;
use Pest\Support\HigherOrderMessageCollection;
@ -55,8 +54,8 @@ final class AfterEachCall
$proxies = $this->proxies;
$afterEachTestCase = ChainableClosure::boundWhen(
fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true),
ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class),
fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line
ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line
)->bindTo($this, self::class);
assert($afterEachTestCase instanceof Closure);
@ -66,6 +65,7 @@ final class AfterEachCall
$this,
$afterEachTestCase,
);
}
/**

View File

@ -5,9 +5,7 @@ declare(strict_types=1);
namespace Pest\PendingCalls;
use Closure;
use Pest\Exceptions\AfterBeforeTestFunction;
use Pest\PendingCalls\Concerns\Describable;
use Pest\Support\Arr;
use Pest\Support\Backtrace;
use Pest\Support\ChainableClosure;
use Pest\Support\HigherOrderMessageCollection;
@ -16,8 +14,6 @@ use Pest\TestSuite;
/**
* @internal
*
* @mixin TestCall
*/
final class BeforeEachCall
{
@ -63,23 +59,18 @@ final class BeforeEachCall
$testCaseProxies = $this->testCaseProxies;
$beforeEachTestCall = function (TestCall $testCall) use ($describing): void {
if ($this->describing !== []) {
if (Arr::last($describing) !== Arr::last($this->describing)) {
return;
}
if (! in_array(Arr::last($describing), $testCall->describing, true)) {
return;
}
if ($describing !== $this->describing) {
return;
}
if ($describing !== $testCall->describing) {
return;
}
$this->testCallProxies->chain($testCall);
};
$beforeEachTestCase = ChainableClosure::boundWhen(
fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true),
ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class),
fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line
ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line
)->bindTo($this, self::class);
assert($beforeEachTestCase instanceof Closure);
@ -92,18 +83,6 @@ final class BeforeEachCall
);
}
/**
* Runs the given closure after the test.
*/
public function after(Closure $closure): self
{
if ($this->describing === []) {
throw new AfterBeforeTestFunction($this->filename);
}
return $this->__call('after', [$closure]);
}
/**
* Saves the calls to be used on the target.
*
@ -112,8 +91,7 @@ final class BeforeEachCall
public function __call(string $name, array $arguments): self
{
if (method_exists(TestCall::class, $name)) {
$this->testCallProxies
->add(Backtrace::file(), Backtrace::line(), $name, $arguments);
$this->testCallProxies->add(Backtrace::file(), Backtrace::line(), $name, $arguments);
return $this;
}

View File

@ -9,17 +9,5 @@ namespace Pest\PendingCalls\Concerns;
*/
trait Describable
{
/**
* Note: this is property is not used; however, it gets added automatically by rector php.
*
* @var array<int, string>
*/
public array $__describing;
/**
* The describing of the test case.
*
* @var array<int, string>
*/
public array $describing = [];
public ?string $describing = null;
}

View File

@ -15,15 +15,8 @@ final class DescribeCall
{
/**
* The current describe call.
*
* @var array<int, string>
*/
private static array $describing = [];
/**
* The describe "before each" call.
*/
private ?BeforeEachCall $currentBeforeEachCall = null;
private static ?string $describing = null;
/**
* Creates a new Pending Call.
@ -39,10 +32,8 @@ final class DescribeCall
/**
* What is the current describing.
*
* @return array<int, string>
*/
public static function describing(): array
public static function describing(): ?string
{
return self::$describing;
}
@ -52,14 +43,12 @@ final class DescribeCall
*/
public function __destruct()
{
unset($this->currentBeforeEachCall);
self::$describing[] = $this->description;
self::$describing = $this->description;
try {
($this->tests)();
} finally {
array_pop(self::$describing);
self::$describing = null;
}
}
@ -68,18 +57,14 @@ final class DescribeCall
*
* @param array<int, mixed> $arguments
*/
public function __call(string $name, array $arguments): self
public function __call(string $name, array $arguments): BeforeEachCall
{
$filename = Backtrace::file();
if (! $this->currentBeforeEachCall instanceof \Pest\PendingCalls\BeforeEachCall) {
$this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename);
$beforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename);
$this->currentBeforeEachCall->describing[] = $this->description;
}
$beforeEachCall->describing = $this->description;
$this->currentBeforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line
return $this;
return $beforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line
}
}

View File

@ -5,16 +5,14 @@ declare(strict_types=1);
namespace Pest\PendingCalls;
use Closure;
use Pest\Concerns\Testable;
use Pest\Exceptions\InvalidArgumentException;
use Pest\Exceptions\TestDescriptionMissing;
use Pest\Factories\Attribute;
use Pest\Factories\Covers\CoversClass;
use Pest\Factories\Covers\CoversFunction;
use Pest\Factories\Covers\CoversNothing;
use Pest\Factories\TestCaseMethodFactory;
use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\Concerns\Describable;
use Pest\Plugins\Only;
use Pest\Support\Backtrace;
use Pest\Support\Container;
use Pest\Support\Exporter;
use Pest\Support\HigherOrderCallables;
use Pest\Support\NullClosure;
@ -26,19 +24,12 @@ use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @mixin HigherOrderCallables|TestCase|Testable
* @mixin HigherOrderCallables|TestCase
*/
final class TestCall // @phpstan-ignore-line
final class TestCall
{
use Describable;
/**
* The list of test case factory attributes.
*
* @var array<int, Attribute>
*/
private array $testCaseFactoryAttributes = [];
/**
* The Test Case Factory.
*/
@ -55,10 +46,10 @@ final class TestCall // @phpstan-ignore-line
public function __construct(
private readonly TestSuite $testSuite,
private readonly string $filename,
private ?string $description = null,
?string $description = null,
?Closure $closure = null
) {
$this->testCaseMethod = new TestCaseMethodFactory($filename, $closure);
$this->testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure);
$this->descriptionLess = $description === null;
@ -67,42 +58,6 @@ final class TestCall // @phpstan-ignore-line
$this->testSuite->beforeEach->get($this->filename)[0]($this);
}
/**
* Runs the given closure after the test.
*/
public function after(Closure $closure): self
{
if ($this->description === null) {
throw new TestDescriptionMissing($this->filename);
}
$description = $this->describing === []
? $this->description
: Str::describe($this->describing, $this->description);
$filename = $this->filename;
$when = function () use ($closure, $filename, $description): void {
if ($this::$__filename !== $filename) { // @phpstan-ignore-line
return;
}
if ($this->__description !== $description) { // @phpstan-ignore-line
return;
}
if ($this->__ran !== true) { // @phpstan-ignore-line
return;
}
$closure->call($this);
};
new AfterEachCall($this->testSuite, $this->filename, $when->bindTo(new \stdClass));
return $this;
}
/**
* Asserts that the test fails with the given message.
*/
@ -210,10 +165,7 @@ final class TestCall // @phpstan-ignore-line
public function group(string ...$groups): self
{
foreach ($groups as $group) {
$this->testCaseMethod->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\Group::class,
[$group],
);
$this->testCaseMethod->groups[] = $group;
}
return $this;
@ -224,7 +176,7 @@ final class TestCall // @phpstan-ignore-line
*/
public function only(): self
{
Only::enable($this, ...func_get_args()); // @phpstan-ignore-line
Only::enable($this);
return $this;
}
@ -354,186 +306,32 @@ final class TestCall // @phpstan-ignore-line
}
/**
* Marks the test as "todo".
* Sets the test as "todo".
*/
public function todo(// @phpstan-ignore-line
array|string|null $note = null,
array|string|null $assignee = null,
array|string|int|null $issue = null,
array|string|int|null $pr = null,
): self {
public function todo(): self
{
$this->skip('__TODO__');
$this->testCaseMethod->todo = true;
if ($issue !== null) {
$this->issue($issue);
}
if ($pr !== null) {
$this->pr($pr);
}
if ($assignee !== null) {
$this->assignee($assignee);
}
if ($note !== null) {
$this->note($note);
}
return $this;
}
/**
* Sets the test as "work in progress".
*/
public function wip(// @phpstan-ignore-line
array|string|null $note = null,
array|string|null $assignee = null,
array|string|int|null $issue = null,
array|string|int|null $pr = null,
): self {
if ($issue !== null) {
$this->issue($issue);
}
if ($pr !== null) {
$this->pr($pr);
}
if ($assignee !== null) {
$this->assignee($assignee);
}
if ($note !== null) {
$this->note($note);
}
return $this;
}
/**
* Sets the test as "done".
*/
public function done(// @phpstan-ignore-line
array|string|null $note = null,
array|string|null $assignee = null,
array|string|int|null $issue = null,
array|string|int|null $pr = null,
): self {
if ($issue !== null) {
$this->issue($issue);
}
if ($pr !== null) {
$this->pr($pr);
}
if ($assignee !== null) {
$this->assignee($assignee);
}
if ($note !== null) {
$this->note($note);
}
return $this;
}
/**
* Associates the test with the given issue(s).
*
* @param array<int, string|int>|string|int $number
*/
public function issue(array|string|int $number): self
{
$number = is_array($number) ? $number : [$number];
$number = array_map(fn (string|int $number): int => (int) ltrim((string) $number, '#'), $number);
$this->testCaseMethod->issues = array_merge($this->testCaseMethod->issues, $number);
return $this;
}
/**
* Associates the test with the given ticket(s). (Alias for `issue`)
*
* @param array<int, string|int>|string|int $number
*/
public function ticket(array|string|int $number): self
{
return $this->issue($number);
}
/**
* Sets the test assignee(s).
*
* @param array<int, string>|string $assignee
*/
public function assignee(array|string $assignee): self
{
$assignees = is_array($assignee) ? $assignee : [$assignee];
$this->testCaseMethod->assignees = array_unique(array_merge($this->testCaseMethod->assignees, $assignees));
return $this;
}
/**
* Associates the test with the given pull request(s).
*
* @param array<int, string|int>|string|int $number
*/
public function pr(array|string|int $number): self
{
$number = is_array($number) ? $number : [$number];
$number = array_map(fn (string|int $number): int => (int) ltrim((string) $number, '#'), $number);
$this->testCaseMethod->prs = array_unique(array_merge($this->testCaseMethod->prs, $number));
return $this;
}
/**
* Adds a note to the test.
*
* @param array<int, string>|string $note
*/
public function note(array|string $note): self
{
$notes = is_array($note) ? $note : [$note];
$this->testCaseMethod->notes = array_unique(array_merge($this->testCaseMethod->notes, $notes));
return $this;
}
/**
* Sets the covered classes or methods.
*
* @param array<int, string>|string $classesOrFunctions
*/
public function covers(array|string ...$classesOrFunctions): self
public function covers(string ...$classesOrFunctions): self
{
/** @var array<int, string> $classesOrFunctions */
$classesOrFunctions = array_reduce($classesOrFunctions, fn ($carry, $item): array => is_array($item) ? array_merge($carry, $item) : array_merge($carry, [$item]), []); // @pest-ignore-type
foreach ($classesOrFunctions as $classOrFunction) {
$isClass = class_exists($classOrFunction) || interface_exists($classOrFunction) || enum_exists($classOrFunction);
$isTrait = trait_exists($classOrFunction);
$isFunction = function_exists($classOrFunction);
$isClass = class_exists($classOrFunction) || trait_exists($classOrFunction);
$isMethod = function_exists($classOrFunction);
if (! $isClass && ! $isTrait && ! $isFunction) {
throw new InvalidArgumentException(sprintf('No class, trait or method named "%s" has been found.', $classOrFunction));
if (! $isClass && ! $isMethod) {
throw new InvalidArgumentException(sprintf('No class or method named "%s" has been found.', $classOrFunction));
}
if ($isClass) {
$this->coversClass($classOrFunction);
} elseif ($isTrait) {
$this->coversTrait($classOrFunction);
} else {
$this->coversFunction($classOrFunction);
}
@ -548,41 +346,7 @@ final class TestCall // @phpstan-ignore-line
public function coversClass(string ...$classes): self
{
foreach ($classes as $class) {
$this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversClass::class,
[$class],
);
}
/** @var ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if (! is_array($paths)) {
$configurationRepository->globalConfiguration('default')->class(...$classes); // @phpstan-ignore-line
}
return $this;
}
/**
* Sets the covered classes.
*/
public function coversTrait(string ...$traits): self
{
foreach ($traits as $trait) {
$this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversTrait::class,
[$trait],
);
}
/** @var ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if (! is_array($paths)) {
$configurationRepository->globalConfiguration('default')->class(...$traits); // @phpstan-ignore-line
$this->testCaseMethod->covers[] = new CoversClass($class);
}
return $this;
@ -594,10 +358,7 @@ final class TestCall // @phpstan-ignore-line
public function coversFunction(string ...$functions): self
{
foreach ($functions as $function) {
$this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversFunction::class,
[$function],
);
$this->testCaseMethod->covers[] = new CoversFunction($function);
}
return $this;
@ -608,10 +369,7 @@ final class TestCall // @phpstan-ignore-line
*/
public function coversNothing(): self
{
$this->testCaseMethod->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversNothing::class,
[],
);
$this->testCaseMethod->covers = [new CoversNothing];
return $this;
}
@ -662,11 +420,10 @@ final class TestCall // @phpstan-ignore-line
if ($this->descriptionLess) {
Exporter::default();
if ($this->description !== null) {
$this->description .= ' → ';
if ($this->testCaseMethod->description !== null) {
$this->testCaseMethod->description .= ' → ';
}
$this->description .= $arguments === null
$this->testCaseMethod->description .= $arguments === null
? $name
: sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
}
@ -679,21 +436,11 @@ final class TestCall // @phpstan-ignore-line
*/
public function __destruct()
{
if ($this->description === null) {
throw new TestDescriptionMissing($this->filename);
}
if ($this->describing !== []) {
if (! is_null($this->describing)) {
$this->testCaseMethod->describing = $this->describing;
$this->testCaseMethod->description = Str::describe($this->describing, $this->description);
} else {
$this->testCaseMethod->description = $this->description;
$this->testCaseMethod->description = Str::describe($this->describing, $this->testCaseMethod->description); // @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);
}
}
}

View File

@ -48,14 +48,11 @@ final class UsesCall
*/
public function __construct(
private readonly string $filename,
private array $classAndTraits
private readonly array $classAndTraits
) {
$this->targets = [$filename];
}
/**
* @deprecated Use `pest()->printer()->compact()` instead.
*/
public function compact(): self
{
DefaultPrinter::compact(true);
@ -64,29 +61,10 @@ final class UsesCall
}
/**
* Specifies the class or traits to use.
*
* @alias extend
* The directories or file where the
* class or traits should be used.
*/
public function use(string ...$classAndTraits): self
{
return $this->extend(...$classAndTraits);
}
/**
* Specifies the class or traits to use.
*/
public function extend(string ...$classAndTraits): self
{
$this->classAndTraits = array_merge($this->classAndTraits, array_values($classAndTraits));
return $this;
}
/**
* The directories or file where the class or traits should be used.
*/
public function in(string ...$targets): self
public function in(string ...$targets): void
{
$targets = array_map(function (string $path): string {
$startChar = DIRECTORY_SEPARATOR;
@ -100,7 +78,7 @@ final class UsesCall
return str_starts_with($path, $startChar)
? $path
: implode(DIRECTORY_SEPARATOR, [
is_dir($this->filename) ? $this->filename : dirname($this->filename),
dirname($this->filename),
$path,
]);
}, $targets);
@ -114,8 +92,6 @@ final class UsesCall
return $accumulator;
}, []);
return $this;
}
/**

View File

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

View File

@ -21,7 +21,7 @@ trait HandleArguments
return true;
}
if (str_starts_with((string) $arg, "$argument=")) { // @phpstan-ignore-line
if (str_starts_with($arg, "$argument=")) {
return true;
}
}

View File

@ -1,96 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins;
use DOMDocument;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Contracts\Plugins\Terminable;
use Pest\Plugins\Concerns\HandleArguments;
use PHPUnit\TextUI\CliArguments\Builder as CliConfigurationBuilder;
use PHPUnit\TextUI\CliArguments\XmlConfigurationFileFinder;
/**
* @internal
*/
final class Configuration implements HandlesArguments, Terminable
{
use HandleArguments;
/**
* The base PHPUnit file.
*/
public const BASE_PHPUNIT_FILE = __DIR__
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
.'resources/base-phpunit.xml';
/**
* Handles the arguments, adding the cache directory and the cache result arguments.
*/
public function handleArguments(array $arguments): array
{
if ($this->hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) {
return $arguments;
}
$arguments = $this->pushArgument('--configuration', $arguments);
return $this->pushArgument((string) realpath($this->fromGeneratedConfigurationFile()), $arguments);
}
/**
* Get the configuration file from the generated configuration file.
*/
private function fromGeneratedConfigurationFile(): string
{
$path = $this->getTempPhpunitXmlPath();
if (file_exists($path)) {
unlink($path);
}
$doc = new DOMDocument;
$doc->load(self::BASE_PHPUNIT_FILE);
$contents = $doc->saveXML();
assert(is_int(file_put_contents($path, $contents)));
return $path;
}
/**
* Check if the configuration file is custom.
*/
private function hasCustomConfigurationFile(): bool
{
$cliConfiguration = (new CliConfigurationBuilder)->fromParameters([]);
$configurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration);
return is_string($configurationFile);
}
/**
* Get the temporary phpunit.xml path.
*/
private function getTempPhpunitXmlPath(): string
{
return getcwd().'/.pest.xml';
}
/**
* Terminates the plugin.
*/
public function terminate(): void
{
$path = $this->getTempPhpunitXmlPath();
if (file_exists($path)) {
unlink($path);
}
}
}

View File

@ -27,11 +27,6 @@ final class Coverage implements AddsOutput, HandlesArguments
*/
private const MIN_OPTION = 'min';
/**
* @var string
*/
private const EXACTLY_OPTION = 'exactly';
/**
* Whether it should show the coverage or not.
*/
@ -42,11 +37,6 @@ final class Coverage implements AddsOutput, HandlesArguments
*/
public float $coverageMin = 0.0;
/**
* The exactly coverage.
*/
public ?float $coverageExactly = null;
/**
* Creates a new Plugin instance.
*/
@ -61,7 +51,7 @@ final class Coverage implements AddsOutput, HandlesArguments
public function handleArguments(array $originals): array
{
$arguments = [...[''], ...array_values(array_filter($originals, function (string $original): bool {
foreach ([self::COVERAGE_OPTION, self::MIN_OPTION, self::EXACTLY_OPTION] as $option) {
foreach ([self::COVERAGE_OPTION, self::MIN_OPTION] as $option) {
if ($original === sprintf('--%s', $option)) {
return true;
}
@ -83,7 +73,6 @@ final class Coverage implements AddsOutput, HandlesArguments
$inputs = [];
$inputs[] = new InputOption(self::COVERAGE_OPTION, null, InputOption::VALUE_NONE);
$inputs[] = new InputOption(self::MIN_OPTION, null, InputOption::VALUE_REQUIRED);
$inputs[] = new InputOption(self::EXACTLY_OPTION, null, InputOption::VALUE_REQUIRED);
$input = new ArgvInput($arguments, new InputDefinition($inputs));
if ((bool) $input->getOption(self::COVERAGE_OPTION)) {
@ -117,13 +106,6 @@ final class Coverage implements AddsOutput, HandlesArguments
$this->coverageMin = (float) $minOption;
}
if ($input->getOption(self::EXACTLY_OPTION) !== null) {
/** @var int|float $exactlyOption */
$exactlyOption = $input->getOption(self::EXACTLY_OPTION);
$this->coverageExactly = (float) $exactlyOption;
}
return $originals;
}
@ -132,10 +114,6 @@ final class Coverage implements AddsOutput, HandlesArguments
*/
public function addOutput(int $exitCode): int
{
if (Parallel::isWorker()) {
return $exitCode;
}
if ($exitCode === 0 && $this->coverage) {
if (! \Pest\Support\Coverage::isAvailable()) {
$this->output->writeln(
@ -145,26 +123,14 @@ final class Coverage implements AddsOutput, HandlesArguments
}
$coverage = \Pest\Support\Coverage::report($this->output);
$exitCode = (int) ($coverage < $this->coverageMin);
if ($exitCode === 0 && $this->coverageExactly !== null) {
$comparableCoverage = $this->computeComparableCoverage($coverage);
$comparableCoverageExactly = $this->computeComparableCoverage($this->coverageExactly);
$exitCode = $comparableCoverage === $comparableCoverageExactly ? 0 : 1;
if ($exitCode === 1) {
$this->output->writeln(sprintf(
"\n <fg=white;bg=red;options=bold> FAIL </> Code coverage not exactly <fg=white;options=bold> %s %%</>, currently <fg=red;options=bold> %s %%</>.",
number_format($this->coverageExactly, 1),
number_format(floor($coverage * 10) / 10, 1),
));
}
} elseif ($exitCode === 1) {
if ($exitCode === 1) {
$this->output->writeln(sprintf(
"\n <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected <fg=white;options=bold> %s %%</>, currently <fg=red;options=bold> %s %%</>.",
number_format($this->coverageMin, 1),
number_format(floor($coverage * 10) / 10, 1)
number_format($coverage, 1)
));
}
@ -173,12 +139,4 @@ final class Coverage implements AddsOutput, HandlesArguments
return $exitCode;
}
/**
* Computes the comparable coverage to a percentage with one decimal.
*/
private function computeComparableCoverage(float $coverage): float
{
return floor($coverage * 10) / 10;
}
}

View File

@ -14,7 +14,7 @@ use function Pest\version;
/**
* @internal
*/
final readonly class Help implements HandlesArguments
final class Help implements HandlesArguments
{
use Concerns\HandleArguments;
@ -22,7 +22,7 @@ final readonly class Help implements HandlesArguments
* Creates a new Plugin instance.
*/
public function __construct(
private OutputInterface $output
private readonly OutputInterface $output
) {
// ..
}
@ -123,21 +123,6 @@ final readonly class Help implements HandlesArguments
], [
'arg' => '--todos',
'desc' => 'Output to standard output the list of todos',
], [
'arg' => '--notes',
'desc' => 'Output to standard output tests with notes',
], [
], [
'arg' => '--issue',
'desc' => 'Output to standard output tests with the given issue number',
], [
], [
'arg' => '--pr',
'desc' => 'Output to standard output tests with the given pull request number',
], [
], [
'arg' => '--pull-request',
'desc' => 'Output to standard output tests with the given pull request number (alias for --pr)',
], [
'arg' => '--retry',
'desc' => 'Run non-passing tests first and stop execution upon first error or failure',
@ -158,59 +143,6 @@ final readonly class Help implements HandlesArguments
'desc' => 'Set the minimum required coverage percentage, and fail if not met',
], ...$content['Code Coverage']];
$content['Mutation Testing'] = [[
'arg' => '--mutate ',
'desc' => 'Runs mutation testing, to understand the quality of your tests',
], [
'arg' => '--mutate --parallel',
'desc' => 'Runs mutation testing in parallel',
], [
'arg' => '--mutate --min',
'desc' => 'Set the minimum required mutation score, and fail if not met',
], [
'arg' => '--mutate --id',
'desc' => 'Run only the mutation with the given ID. But E.g. --id=ecb35ab30ffd3491. Note, you need to provide the same options as the original run',
], [
'arg' => '--mutate --covered-only',
'desc' => 'Only generate mutations for classes that are covered by tests',
], [
'arg' => '--mutate --bail',
'desc' => 'Stop mutation testing execution upon first untested or uncovered mutation',
], [
'arg' => '--mutate --class',
'desc' => 'Generate mutations for the given class(es). E.g. --class=App\\\\Models',
], [
'arg' => '--mutate --ignore',
'desc' => 'Ignore the given class(es) when generating mutations. E.g. --ignore=App\\\\Http\\\\Requests',
], [
'arg' => '--mutate --clear-cache',
'desc' => 'Clear the mutation cache',
], [
'arg' => '--mutate --no-cache',
'desc' => 'Clear the mutation cache',
], [
'arg' => '--mutate --ignore-min-score-on-zero-mutations',
'desc' => 'Ignore the minimum score requirement when there are no mutations',
], [
'arg' => '--mutate --covered-only',
'desc' => 'Only generate mutations for classes that are covered by tests',
], [
'arg' => '--mutate --everything',
'desc' => 'Generate mutations for all classes, even if they are not covered by tests',
], [
'arg' => '--mutate --profile',
'desc' => 'Output to standard output the top ten slowest mutations',
], [
'arg' => '--mutate --retry',
'desc' => 'Run untested or uncovered mutations first and stop execution upon first error or failure',
], [
'arg' => '--mutate --stop-on-uncovered',
'desc' => 'Stop mutation testing execution upon first untested mutation',
], [
'arg' => '--mutate --stop-on-untested',
'desc' => 'Stop mutation testing execution upon first untested mutation',
]];
$content['Profiling'] = [
[
'arg' => '--profile ',

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final readonly class Init implements HandlesArguments
final class Init implements HandlesArguments
{
/**
* The option the triggers the init job.
@ -37,9 +37,9 @@ final readonly class Init implements HandlesArguments
* Creates a new Plugin instance.
*/
public function __construct(
private TestSuite $testSuite,
private InputInterface $input,
private OutputInterface $output
private readonly TestSuite $testSuite,
private readonly InputInterface $input,
private readonly OutputInterface $output
) {
// ..
}

View File

@ -28,10 +28,6 @@ final class Only implements Terminable
*/
public function terminate(): void
{
if (Parallel::isWorker()) {
return;
}
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (file_exists($lockFile)) {
@ -42,26 +38,18 @@ final class Only implements Terminable
/**
* Creates the lock file.
*/
public static function enable(TestCall $testCall, string $group = '__pest_only'): void
public static function enable(TestCall $testCall): void
{
$testCall->group($group);
if (Environment::name() === Environment::CI || Parallel::isWorker()) {
if (Environment::name() == Environment::CI) {
return;
}
$testCall->group('__pest_only');
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (file_exists($lockFile) && $group === '__pest_only') {
file_put_contents($lockFile, $group);
return;
}
if (! file_exists($lockFile)) {
touch($lockFile);
file_put_contents($lockFile, $group);
}
}
@ -74,18 +62,4 @@ final class Only implements Terminable
return file_exists($lockFile);
}
/**
* Returns the group name.
*/
public static function group(): string
{
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (! file_exists($lockFile)) {
return '__pest_only';
}
return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line
}
}

View File

@ -34,7 +34,7 @@ final class Parallel implements HandlesArguments
/**
* @var string[]
*/
private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request'];
private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry'];
/**
* Whether the given command line arguments indicate that the test suite should be run in parallel.
@ -42,7 +42,6 @@ final class Parallel implements HandlesArguments
public static function isEnabled(): bool
{
$argv = new ArgvInput;
if ($argv->hasParameterOption('--parallel')) {
return true;
}

View File

@ -26,7 +26,7 @@ final class Laravel implements HandlesArguments
*/
public function handleArguments(array $arguments): array
{
return $this->whenUsingLaravel($arguments, function (array $arguments): array {
return self::whenUsingLaravel($arguments, function (array $arguments): array {
$this->ensureRunnerIsResolvable();
$arguments = $this->ensureEnvironmentVariables($arguments);
@ -42,7 +42,7 @@ final class Laravel implements HandlesArguments
* @param CLosure(array<int, string>): array<int, string> $closure
* @return array<int, string>
*/
private function whenUsingLaravel(array $arguments, Closure $closure): array
private static function whenUsingLaravel(array $arguments, Closure $closure): array
{
$isLaravelApplication = InstalledVersions::isInstalled('laravel/framework', false);
$isLaravelPackage = class_exists(\Orchestra\Testbench\TestCase::class);

View File

@ -11,7 +11,7 @@ final class CleanConsoleOutput extends ConsoleOutput
/**
* {@inheritdoc}
*/
protected function doWrite(string $message, bool $newline): void // @pest-arch-ignore-line
protected function doWrite(string $message, bool $newline): void
{
if ($this->isOpeningHeadline($message)) {
return;

View File

@ -19,7 +19,6 @@ use Pest\TestSuite;
use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Event\TestRunner\WarningTriggered;
use PHPUnit\Runner\CodeCoverage;
use PHPUnit\Runner\ResultCache\DefaultResultCache;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
@ -47,27 +46,15 @@ use function usleep;
*/
final class WrapperRunner implements RunnerInterface
{
/**
* The time to sleep between cycles.
*/
private const CYCLE_SLEEP = 10000;
/**
* The result printer.
*/
private readonly ResultPrinter $printer;
/**
* The timer.
*/
private readonly Timer $timer;
/** @var list<non-empty-string> */
private array $pending = [];
/**
* The exit code.
*/
private int $exitcode = -1;
/** @var array<positive-int,WrapperWorker> */
@ -80,10 +67,7 @@ final class WrapperRunner implements RunnerInterface
private array $unexpectedOutputFiles = [];
/** @var list<SplFileInfo> */
private array $resultCacheFiles = [];
/** @var list<SplFileInfo> */
private array $testResultFiles = [];
private array $testresultFiles = [];
/** @var list<SplFileInfo> */
private array $coverageFiles = [];
@ -100,9 +84,6 @@ final class WrapperRunner implements RunnerInterface
/** @var non-empty-string[] */
private readonly array $parameters;
/**
* The code coverage filter registry.
*/
private CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
public function __construct(
@ -126,9 +107,6 @@ final class WrapperRunner implements RunnerInterface
$parameters = array_merge($parameters, $options->passthruPhp);
}
/** @var array<int, non-empty-string> $parameters */
$parameters = $this->handleLaravelHerd($parameters);
$parameters[] = $wrapper;
$this->parameters = $parameters;
@ -160,21 +138,6 @@ final class WrapperRunner implements RunnerInterface
return $this->complete($result);
}
/**
* Handles Laravel Herd's debug and coverage modes.
*
* @param array<string> $parameters
* @return array<string>
*/
private function handleLaravelHerd(array $parameters): array
{
if (isset($_ENV['HERD_DEBUG_INI'])) {
return array_merge($parameters, ['-c', $_ENV['HERD_DEBUG_INI']]);
}
return $parameters;
}
private function startWorkers(): void
{
for ($token = 1; $token <= $this->options->processes; $token++) {
@ -268,8 +231,7 @@ final class WrapperRunner implements RunnerInterface
$this->batches[$token] = 0;
$this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile;
$this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile;
$this->testResultFiles[] = $worker->testResultFile;
$this->testresultFiles[] = $worker->testresultFile;
if (isset($worker->junitFile)) {
$this->junitFiles[] = $worker->junitFile;
@ -303,12 +265,12 @@ final class WrapperRunner implements RunnerInterface
private function complete(TestResult $testResultSum): int
{
foreach ($this->testResultFiles as $testResultFile) {
if (! $testResultFile->isFile()) {
foreach ($this->testresultFiles as $testresultFile) {
if (! $testresultFile->isFile()) {
continue;
}
$contents = file_get_contents($testResultFile->getPathname());
$contents = file_get_contents($testresultFile->getPathname());
assert($contents !== false);
$testResult = unserialize($contents);
assert($testResult instanceof TestResult);
@ -365,20 +327,9 @@ final class WrapperRunner implements RunnerInterface
$testResultSum->phpNotices(),
$testResultSum->phpWarnings(),
$testResultSum->numberOfIssuesIgnoredByBaseline(),
);
if ($this->options->configuration->cacheResult()) {
$resultCacheSum = new DefaultResultCache($this->options->configuration->testResultCacheFile());
foreach ($this->resultCacheFiles as $resultCacheFile) {
$resultCache = new DefaultResultCache($resultCacheFile->getPathname());
$resultCache->load();
$resultCacheSum->mergeWith($resultCache);
}
$resultCacheSum->persist();
}
$this->printer->printResults(
$testResultSum,
$this->teamcityFiles,
@ -391,7 +342,7 @@ final class WrapperRunner implements RunnerInterface
$exitcode = Result::exitCode($this->options->configuration, $testResultSum);
$this->clearFiles($this->unexpectedOutputFiles);
$this->clearFiles($this->testResultFiles);
$this->clearFiles($this->testresultFiles);
$this->clearFiles($this->coverageFiles);
$this->clearFiles($this->junitFiles);
$this->clearFiles($this->teamcityFiles);
@ -439,7 +390,6 @@ final class WrapperRunner implements RunnerInterface
}
$testSuite = (new LogMerger)->merge($this->junitFiles);
assert($testSuite instanceof \ParaTest\JUnit\TestSuite);
(new Writer)->write(
$testSuite,
$this->options->configuration->logfileJunit(),

View File

@ -1,156 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest;
use Closure;
use Pest\Arch\Support\Composer;
use Pest\ArchPresets\AbstractPreset;
use Pest\ArchPresets\Custom;
use Pest\ArchPresets\Laravel;
use Pest\ArchPresets\Php;
use Pest\ArchPresets\Relaxed;
use Pest\ArchPresets\Security;
use Pest\ArchPresets\Strict;
use Pest\Exceptions\InvalidArgumentException;
use Pest\PendingCalls\TestCall;
use stdClass;
/**
* @internal
*/
final class Preset
{
/**
* The application / package base namespaces.
*
* @var ?array<int, string>
*/
private static ?array $baseNamespaces = null;
/**
* The custom presets.
*
* @var array<string, Closure>
*/
private static array $customPresets = [];
/**
* Creates a new preset instance.
*/
public function __construct()
{
//
}
/**
* Uses the Pest php preset and returns the test call instance.
*/
public function php(): Php
{
return $this->executePreset(new Php($this->baseNamespaces()));
}
/**
* Uses the Pest laravel preset and returns the test call instance.
*/
public function laravel(): Laravel
{
return $this->executePreset(new Laravel($this->baseNamespaces()));
}
/**
* Uses the Pest strict preset and returns the test call instance.
*/
public function strict(): Strict
{
return $this->executePreset(new Strict($this->baseNamespaces()));
}
/**
* Uses the Pest security preset and returns the test call instance.
*/
public function security(): AbstractPreset
{
return $this->executePreset(new Security($this->baseNamespaces()));
}
/**
* Uses the Pest relaxed preset and returns the test call instance.
*/
public function relaxed(): AbstractPreset
{
return $this->executePreset(new Relaxed($this->baseNamespaces()));
}
/**
* Uses the Pest custom preset and returns the test call instance.
*
* @internal
*/
public static function custom(string $name, Closure $execute): void
{
if (preg_match('/^[a-zA-Z]+$/', $name) === false) {
throw new InvalidArgumentException('The preset name must only contain words from a-z or A-Z.');
}
self::$customPresets[$name] = $execute;
}
/**
* Dynamically handle calls to the class.
*
* @param array<int, mixed> $arguments
*
* @throws InvalidArgumentException
*/
public function __call(string $name, array $arguments): AbstractPreset
{
if (! array_key_exists($name, self::$customPresets)) {
$availablePresets = [
...['php', 'laravel', 'strict', 'security', 'relaxed'],
...array_keys(self::$customPresets),
];
throw new InvalidArgumentException(sprintf('The preset [%s] does not exist. The available presets are [%s].', $name, implode(', ', $availablePresets)));
}
return $this->executePreset(new Custom($this->baseNamespaces(), $name, self::$customPresets[$name]));
}
/**
* Executes the given preset.
*
* @template TPreset of AbstractPreset
*
* @param TPreset $preset
* @return TPreset
*/
private function executePreset(AbstractPreset $preset): AbstractPreset
{
$this->baseNamespaces();
$preset->execute();
// $this->testCall->testCaseMethod->closure = (function () use ($preset): void {
// $preset->flush();
// })->bindTo(new stdClass);
return $preset;
}
/**
* Get the base namespaces for the application / package.
*
* @return array<int, string>
*/
private function baseNamespaces(): array
{
if (self::$baseNamespaces === null) {
self::$baseNamespaces = Composer::userNamespaces();
}
return self::$baseNamespaces;
}
}

View File

@ -9,11 +9,9 @@ use Pest\Contracts\TestCaseFilter;
use Pest\Contracts\TestCaseMethodFilter;
use Pest\Exceptions\TestCaseAlreadyInUse;
use Pest\Exceptions\TestCaseClassOrTraitNotFound;
use Pest\Factories\Attribute;
use Pest\Factories\TestCaseFactory;
use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Str;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
/**
@ -116,9 +114,9 @@ final class TestRepository
/**
* Gets the test case factory from the given filename.
*/
public function get(string $filename): ?TestCaseFactory
public function get(string $filename): TestCaseFactory
{
return $this->testCases[$filename] ?? null;
return $this->testCases[$filename];
}
/**
@ -188,10 +186,7 @@ final class TestRepository
foreach ($testCase->methods as $method) {
foreach ($groups as $group) {
$method->attributes[] = new Attribute(
Group::class,
[$group],
);
$method->groups[] = $group;
}
}

View File

@ -40,7 +40,7 @@ final class Result
*/
public static function exitCode(Configuration $configuration, TestResult $result): int
{
if ($result->wasSuccessfulIgnoringPhpunitWarnings()) {
if ($result->wasSuccessful()) {
if ($configuration->failOnWarning()) {
$warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents()
+ count($result->warnings())
@ -60,7 +60,7 @@ final class Result
return self::FAILURE_EXIT;
}
if ($result->wasSuccessfulIgnoringPhpunitWarnings()) {
if ($result->wasSuccessful()) {
if ($configuration->failOnRisky() && $result->hasTestConsideredRiskyEvents()) {
$returnCode = self::FAILURE_EXIT;
}

View File

@ -15,15 +15,15 @@ use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final readonly class EnsureTeamCityEnabled implements ConfiguredSubscriber
final class EnsureTeamCityEnabled implements ConfiguredSubscriber
{
/**
* Creates a new Configured Subscriber instance.
*/
public function __construct(
private InputInterface $input,
private OutputInterface $output,
private TestSuite $testSuite,
private readonly InputInterface $input,
private readonly OutputInterface $output,
private readonly TestSuite $testSuite,
) {}
/**

View File

@ -81,14 +81,4 @@ final class Arr
return $results;
}
/**
* Returns the value of the last element or false for empty array
*
* @param array<array-key, mixed> $array
*/
public static function last(array $array): mixed
{
return end($array);
}
}

View File

@ -17,13 +17,13 @@ final class ChainableClosure
*/
public static function boundWhen(Closure $condition, Closure $next): Closure
{
return function (...$arguments) use ($condition, $next): void {
return function () use ($condition, $next): void {
if (! is_object($this)) { // @phpstan-ignore-line
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
}
if (\Pest\Support\Closure::bind($condition, $this, self::class)(...$arguments)) {
\Pest\Support\Closure::bind($next, $this, self::class)(...$arguments);
if (\Pest\Support\Closure::bind($condition, $this, self::class)(...func_get_args())) {
\Pest\Support\Closure::bind($next, $this, self::class)(...func_get_args());
}
};
}
@ -33,13 +33,13 @@ final class ChainableClosure
*/
public static function bound(Closure $closure, Closure $next): Closure
{
return function (...$arguments) use ($closure, $next): void {
return function () use ($closure, $next): void {
if (! is_object($this)) { // @phpstan-ignore-line
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
}
\Pest\Support\Closure::bind($closure, $this, self::class)(...$arguments);
\Pest\Support\Closure::bind($next, $this, self::class)(...$arguments);
\Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args());
\Pest\Support\Closure::bind($next, $this, self::class)(...func_get_args());
};
}
@ -48,9 +48,9 @@ final class ChainableClosure
*/
public static function unbound(Closure $closure, Closure $next): Closure
{
return function (...$arguments) use ($closure, $next): void {
$closure(...$arguments);
$next(...$arguments);
return function () use ($closure, $next): void {
$closure(...func_get_args());
$next(...func_get_args());
};
}
@ -59,9 +59,9 @@ final class ChainableClosure
*/
public static function boundStatically(Closure $closure, Closure $next): Closure
{
return static function (...$arguments) use ($closure, $next): void {
\Pest\Support\Closure::bind($closure, null, self::class)(...$arguments);
\Pest\Support\Closure::bind($next, null, self::class)(...$arguments);
return static function () use ($closure, $next): void {
\Pest\Support\Closure::bind($closure, null, self::class)(...func_get_args());
\Pest\Support\Closure::bind($next, null, self::class)(...func_get_args());
};
}
}

View File

@ -20,13 +20,13 @@ final class Closure
*/
public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure
{
if (! $closure instanceof \Closure) {
if ($closure == null) {
throw ShouldNotHappen::fromMessage('Could not bind null closure.');
}
$closure = BaseClosure::bind($closure, $newThis, $newScope);
if (! $closure instanceof \Closure) {
if ($closure == false) {
throw ShouldNotHappen::fromMessage('Could not bind closure.');
}

View File

@ -13,9 +13,6 @@ use ReflectionParameter;
*/
final class Container
{
/**
* The instance of the container.
*/
private static ?Container $instance = null;
/**

View File

@ -138,7 +138,7 @@ final class Coverage
$totalCoverageAsString = $totalCoverage->asFloat() === 0.0
? '0.0'
: number_format(floor($totalCoverage->asFloat() * 10) / 10, 1, '.', '');
: number_format($totalCoverage->asFloat(), 1, '.', '');
renderUsing($output);
render(<<<HTML
@ -197,7 +197,7 @@ final class Coverage
};
$array = [];
foreach (array_filter($file->lineCoverageData(), is_array(...)) as $line => $tests) {
foreach (array_filter($file->lineCoverageData(), 'is_array') as $line => $tests) {
$array = $eachLine($array, $tests, $line);
}

View File

@ -31,7 +31,7 @@ final class ExceptionTrace
$message = str_replace(self::UNDEFINED_METHOD, 'Call to undefined method ', $message);
if (class_exists((string) $class) && (is_countable(class_parents($class)) ? count(class_parents($class)) : 0) > 0 && array_values(class_parents($class))[0] === TestCase::class) { // @phpstan-ignore-line
$message .= '. Did you forget to use the [pest()->extend()] function? Read more at: https://pestphp.com/docs/configuring-tests';
$message .= '. Did you forget to use the [uses()] function? Read more at: https://pestphp.com/docs/configuring-tests';
}
Reflection::setPropertyValue($throwable, 'message', $message);

View File

@ -10,7 +10,7 @@ use SebastianBergmann\RecursionContext\Context;
/**
* @internal
*/
final readonly class Exporter
final class Exporter
{
/**
* The maximum number of items in an array to export.
@ -21,7 +21,7 @@ final readonly class Exporter
* Creates a new Exporter instance.
*/
public function __construct(
private BaseExporter $exporter,
private readonly BaseExporter $exporter,
) {
// ...
}
@ -64,6 +64,8 @@ final readonly class Exporter
continue;
}
assert(is_array($data));
$result[] = $context->contains($data[$key]) !== false
? '*RECURSION*'
: sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context));

View File

@ -10,12 +10,12 @@ use Pest\Expectation;
/**
* @internal
*/
final readonly class HigherOrderCallables
final class HigherOrderCallables
{
/**
* Creates a new Higher Order Callables instances.
*/
public function __construct(private object $target)
public function __construct(private readonly object $target)
{
// ..
}
@ -49,6 +49,16 @@ final readonly class HigherOrderCallables
return $this->expect($value);
}
/**
* Execute the given callable after the test has executed the setup method.
*
* @deprecated This method is deprecated. Please use `defer` instead.
*/
public function tap(callable $callable): object
{
return $this->defer($callable);
}
/**
* Execute the given callable after the test has executed the setup method.
*/

View File

@ -11,7 +11,6 @@ use Pest\TestSuite;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionProperty;
@ -214,74 +213,4 @@ final class Reflection
{
return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null;
}
/**
* Get the properties from the given reflection class.
*
* Used by `expect()->toHavePropertiesDocumented()`.
*
* @param ReflectionClass<object> $reflectionClass
* @return array<int, ReflectionProperty>
*/
public static function getPropertiesFromReflectionClass(ReflectionClass $reflectionClass): array
{
$getProperties = fn (ReflectionClass $reflectionClass): array => array_filter(
array_map(
fn (ReflectionProperty $property): \ReflectionProperty => $property,
$reflectionClass->getProperties(),
), fn (ReflectionProperty $property): bool => $property->getDeclaringClass()->getName() === $reflectionClass->getName(),
);
$propertiesFromTraits = [];
foreach ($reflectionClass->getTraits() as $trait) {
$propertiesFromTraits = array_merge($propertiesFromTraits, $getProperties($trait));
}
$propertiesFromTraits = array_map(
fn (ReflectionProperty $property): string => $property->getName(),
$propertiesFromTraits,
);
return array_values(
array_filter(
$getProperties($reflectionClass),
fn (ReflectionProperty $property): bool => ! in_array($property->getName(), $propertiesFromTraits, true),
),
);
}
/**
* Get the methods from the given reflection class.
*
* Used by `expect()->toHaveMethodsDocumented()`.
*
* @param ReflectionClass<object> $reflectionClass
* @return array<int, ReflectionMethod>
*/
public static function getMethodsFromReflectionClass(ReflectionClass $reflectionClass, int $filter = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED | ReflectionMethod::IS_PRIVATE): array
{
$getMethods = fn (ReflectionClass $reflectionClass): array => array_filter(
array_map(
fn (ReflectionMethod $method): \ReflectionMethod => $method,
$reflectionClass->getMethods($filter),
), fn (ReflectionMethod $method): bool => $method->getDeclaringClass()->getName() === $reflectionClass->getName(),
);
$methodsFromTraits = [];
foreach ($reflectionClass->getTraits() as $trait) {
$methodsFromTraits = array_merge($methodsFromTraits, $getMethods($trait));
}
$methodsFromTraits = array_map(
fn (ReflectionMethod $method): string => $method->getName(),
$methodsFromTraits,
);
return array_values(
array_filter(
$getMethods($reflectionClass),
fn (ReflectionMethod $method): bool => ! in_array($method->getName(), $methodsFromTraits, true),
),
);
}
}

View File

@ -30,7 +30,6 @@ final class StateGenerator
$testResultEvent->throwable()
));
} else {
// @phpstan-ignore-next-line
$state->add(TestResult::fromBeforeFirstTestMethodErrored($testResultEvent));
}
}

View File

@ -103,14 +103,10 @@ final class Str
/**
* Creates a describe block as `$describeDescription` → `$testDescription` format.
*
* @param array<int, string> $describeDescriptions
*/
public static function describe(array $describeDescriptions, string $testDescription): string
public static function describe(string $describeDescription, string $testDescription): string
{
$descriptionComponents = [...$describeDescriptions, $testDescription];
return sprintf(str_repeat('`%s` → ', count($describeDescriptions)).'%s', ...$descriptionComponents);
return sprintf('`%s` → %s', $describeDescription, $testDescription);
}
/**

View File

@ -14,21 +14,15 @@ use Symfony\Component\Process\Process;
final class GitDirtyTestCaseFilter implements TestCaseFilter
{
/**
* @var array<int, string>|null
* @var string[]|null
*/
private ?array $changedFiles = null;
/**
* Creates a new instance of the filter.
*/
public function __construct(private readonly string $projectRoot)
{
// ...
}
/**
* {@inheritdoc}
*/
public function accept(string $testCaseFilename): bool
{
if ($this->changedFiles === null) {
@ -47,9 +41,6 @@ final class GitDirtyTestCaseFilter implements TestCaseFilter
return in_array($relativePath, $this->changedFiles, true);
}
/**
* Loads the changed files.
*/
private function loadChangedFiles(): void
{
$process = new Process(['git', 'status', '--short', '--', '*.php']);

View File

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\TestCaseMethodFilters;
use Pest\Contracts\TestCaseMethodFilter;
use Pest\Factories\TestCaseMethodFactory;
final readonly class AssigneeTestCaseFilter implements TestCaseMethodFilter
{
/**
* Create a new filter instance.
*/
public function __construct(private string $assignee)
{
//
}
/**
* Filter the test case methods.
*/
public function accept(TestCaseMethodFactory $factory): bool
{
return array_filter($factory->assignees, fn (string $assignee): bool => str_starts_with($assignee, $this->assignee)) !== [];
}
}

Some files were not shown because too many files have changed in this diff Show More