Compare commits

..

10 Commits

Author SHA1 Message Date
nuno maduro e33c239f58 Add notice for outdated Pest version and support policy
Updated README to inform users about outdated version and current support policy.

Signed-off-by: nuno maduro <enunomaduro@gmail.com>
2026-07-06 13:31:33 +01:00
nuno maduro 2440e56ac1 fix: escape generated test case filename (#1747)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:18:03 +01:00
nuno maduro 1729a09d7a chore: scope GITHUB_TOKEN to contents: read in workflows 2026-05-13 02:29:49 +01:00
nuno maduro ea6d0d8786 chore: pin GitHub Actions to commit SHAs (#1698) 2026-05-11 22:12:00 -03:00
nuno maduro 8871a6f5ef chore: bumps dependencies 2026-03-10 21:04:33 +00:00
nuno maduro e4b6fc08b6 Release 3.8.6 2026-03-10 21:03:11 +00:00
Nuno Maduro 7796630eaf chore: only runs CI against stable 2026-01-28 01:33:45 +00:00
Nuno Maduro 3aec2b26ef chore: runs only unit tests 2026-01-28 01:30:45 +00:00
Nuno Maduro 7a7c35292c release: 3.8.5 2026-01-28 01:23:37 +00:00
Nuno Maduro 72cf695554 release: 3.8.4 2025-08-20 20:12:42 +01:00
157 changed files with 458 additions and 1673 deletions
-48
View File
@@ -1,48 +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.3
tools: composer:v2
coverage: none
extensions: sockets
- name: Install Dependencies
run: composer update --prefer-stable --no-interaction --no-progress --ansi
- name: Profanity Check
run: composer test:profanity
- 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
+8 -6
View File
@@ -4,6 +4,9 @@ on:
push:
pull_request:
permissions:
contents: read
jobs:
tests:
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
@@ -12,24 +15,23 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest] # windows-latest
symfony: ['7.3']
php: ['8.3', '8.4']
os: [ubuntu-latest, macos-latest, windows-latest]
symfony: ['7.1']
php: ['8.2', '8.3', '8.4']
dependency_version: [prefer-stable]
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
coverage: none
extensions: sockets
- name: Setup Problem Matches
run: |
+3 -3
View File
@@ -1,7 +1,7 @@
<p align="center">
<img src="https://raw.githubusercontent.com/pestphp/art/master/v4/social.png" width="600" alt="PEST">
<img src="https://raw.githubusercontent.com/pestphp/art/master/v3/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=4.x&label=Tests%204.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=3.x&label=Tests%203.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>
@@ -10,7 +10,7 @@
------
> Pest v4 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest-v4-is-here-now-with-browser-testing)**.
> You are viewing the README for an outdated version of Pest; please refer to the current support policy: **[pestphp.com/docs/support-policy](https://pestphp.com/docs/support-policy)**
**Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP.
+3 -3
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 v3 you should use the `3.x` branch instead.**
> **For Pest v2 you should use the `2.x` branch instead.**
- Clear your local repository with: `git add . && git reset --hard && git checkout 4.x`
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...4.x](https://github.com/pestphp/pest/compare/{latest_version}...4.x)
- 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)
- 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"`
+19 -33
View File
@@ -17,21 +17,19 @@
}
],
"require": {
"php": "^8.3.0",
"brianium/paratest": "^7.11.2",
"nunomaduro/collision": "^8.8.2",
"nunomaduro/termwind": "^2.3.1",
"pestphp/pest-plugin": "^4.0.0",
"pestphp/pest-plugin-arch": "^4.0.0",
"pestphp/pest-plugin-mutate": "^4.0.0",
"pestphp/pest-plugin-profanity": "^4.0.0",
"phpunit/phpunit": "^12.3.5",
"symfony/process": "^7.3.0"
"php": "^8.2.0",
"brianium/paratest": "^7.8.5",
"nunomaduro/collision": "^8.9.1",
"nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^3.0.0",
"pestphp/pest-plugin-arch": "^3.1.1",
"pestphp/pest-plugin-mutate": "^3.0.5",
"phpunit/phpunit": "^11.5.50"
},
"conflict": {
"filp/whoops": "<2.18.3",
"phpunit/phpunit": ">12.3.5",
"sebastian/exporter": "<7.0.0",
"filp/whoops": "<2.16.0",
"phpunit/phpunit": ">11.5.50",
"sebastian/exporter": "<6.0.0",
"webmozart/assert": "<1.11.0"
},
"autoload": {
@@ -55,10 +53,9 @@
]
},
"require-dev": {
"pestphp/pest-dev-tools": "^4.0.0",
"pestphp/pest-plugin-browser": "^4.0.0",
"pestphp/pest-plugin-type-coverage": "^4.0.1",
"psy/psysh": "^0.12.10"
"pestphp/pest-dev-tools": "^3.4.0",
"pestphp/pest-plugin-type-coverage": "^3.6.1",
"symfony/process": "^7.4.5"
},
"minimum-stability": "dev",
"prefer-stable": true,
@@ -74,22 +71,12 @@
],
"scripts": {
"refacto": "rector",
"lint": "pint --parallel",
"test:refacto": "rector --dry-run",
"test:lint": "pint --parallel --test",
"test:profanity": "php bin/pest --profanity --compact",
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
"test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100",
"test:unit": "php bin/pest --exclude-group=integration --compact",
"test:inline": "php bin/pest --configuration=phpunit.inline.xml",
"test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3",
"test:integration": "php bin/pest --group=integration -v",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots",
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
"test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3",
"test:integration": "php bin/pest --colors=always --group=integration -v",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
"test": [
"@test:refacto",
"@test:lint",
"@test:type:check",
"@test:type:coverage",
"@test:unit",
"@test:parallel",
"@test:integration"
@@ -115,7 +102,6 @@
"Pest\\Plugins\\Snapshot",
"Pest\\Plugins\\Verbose",
"Pest\\Plugins\\Version",
"Pest\\Plugins\\Shard",
"Pest\\Plugins\\Parallel"
]
},
+1 -3
View File
@@ -52,8 +52,6 @@ use PHPUnit\Util\Filter;
use PHPUnit\Util\ThrowableToStringMapper;
/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final readonly class ThrowableBuilder
@@ -84,7 +82,7 @@ final readonly class ThrowableBuilder
$t->getMessage(),
ThrowableToStringMapper::map($t),
$trace,
$previous,
$previous
);
}
}
+5 -2
View File
@@ -14,6 +14,9 @@ namespace PHPUnit\Logging\JUnit;
use DOMDocument;
use DOMElement;
use Pest\Logging\Converter;
use Pest\Support\Container;
use Pest\TestSuite;
use PHPUnit\Event\Code\Test;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\EventFacadeIsSealedException;
@@ -50,7 +53,7 @@ final class JunitXmlLogger
{
private readonly Printer $printer;
private readonly \Pest\Logging\Converter $converter; // pest-added
private readonly Converter $converter; // pest-added
private DOMDocument $document;
@@ -108,7 +111,7 @@ final class JunitXmlLogger
public function __construct(Printer $printer, Facade $facade)
{
$this->printer = $printer;
$this->converter = new \Pest\Logging\Converter(\Pest\Support\Container::getInstance()->get(\Pest\TestSuite::class)->rootPath); // pest-added
$this->converter = new Converter(Container::getInstance()->get(TestSuite::class)->rootPath); // pest-added
$this->registerSubscribers($facade);
$this->createDocument();
-98
View File
@@ -1,98 +0,0 @@
<?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use const PHP_EOL;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
use function dirname;
use function serialize;
use function str_contains;
final class PHP
{
public function process(CodeCoverage $coverage, ?string $target = null): string
{
$coverage->clearCache();
$buffer = "<?php return \unserialize(<<<'END_OF_COVERAGE_SERIALIZATION'".PHP_EOL.serialize($coverage).PHP_EOL.'END_OF_COVERAGE_SERIALIZATION'.PHP_EOL.');';
if ($target !== null) {
if (! str_contains($target, '://')) {
Filesystem::createDirectory(dirname($target));
}
if (! is_writable(dirname($target))) {
throw new WriteOperationFailedException($target);
}
$fp = @fopen($target, 'wb');
if (! $fp) {
throw new WriteOperationFailedException($target);
}
$chunkSize = 1024 * 1024 * 8;
$offset = 0;
$total = strlen($buffer);
while ($offset < $total) {
$written = @fwrite($fp, substr($buffer, $offset, $chunkSize));
if ($written === false) {
fclose($fp);
throw new WriteOperationFailedException($target);
}
$offset += $written;
}
fclose($fp);
}
return $buffer;
}
}
@@ -99,9 +99,7 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
}
if ($test instanceof HasPrintableTestCaseName) {
$name = trim(
$test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName().$test->dataSetAsString()
);
$name = $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName();
} else {
$name = $test::class.'::'.$test->nameWithDataSet();
}
@@ -72,7 +72,10 @@ use function Pest\version;
*/
final class DefaultResultCache implements ResultCache
{
private const string DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
/**
* @var string
*/
private const DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
private readonly string $cacheFilename;
@@ -45,7 +45,6 @@ declare(strict_types=1);
namespace PHPUnit\TextUI;
use Pest\Plugins\Only;
use Pest\Runner\Filter\EnsureTestCaseIsInitiatedFilter;
use PHPUnit\Event;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\Filter\Factory;
@@ -67,12 +66,6 @@ final readonly class TestSuiteFilterProcessor
{
$factory = new Factory;
// @phpstan-ignore-next-line
(fn () => $this->filters[] = [
'className' => EnsureTestCaseIsInitiatedFilter::class,
'argument' => '',
])->call($factory);
if (! $configuration->hasFilter() &&
! $configuration->hasGroups() &&
! $configuration->hasExcludeGroups() &&
@@ -80,8 +73,6 @@ final readonly class TestSuiteFilterProcessor
! $configuration->hasTestsCovering() &&
! $configuration->hasTestsUsing() &&
! Only::isEnabled()) {
$suite->injectFilter($factory);
return;
}
-1
View File
@@ -16,7 +16,6 @@
<testsuites>
<testsuite name="default">
<directory suffix=".php">./tests</directory>
<directory suffix=".php">./tests-external</directory>
<exclude>./tests/.snapshots</exclude>
<exclude>./tests/.tests</exclude>
<exclude>./tests/Fixtures/Inheritance</exclude>
@@ -1,22 +0,0 @@
<div class="mx-2 mb-1">
<p>
<span>Using the <span class="text-yellow font-bold">visit()</span> function requires the Pest Plugin Browser to be installed.</span>
<span class="ml-1 text-yellow font-bold">Run:</span>
</p>
<div>
<span class="text-gray mr-1">- </span>
<span>composer require pestphp/pest-plugin-browser:^4.0 --dev</span>
</div>
<div>
<span class="text-gray mr-1">- </span>
<span>npm install playwright@latest</span>
</div>
<div>
<span class="text-gray mr-1">- </span>
<span>npx playwright install</span>
</div>
</div>
-7
View File
@@ -4,9 +4,6 @@ declare(strict_types=1);
namespace Pest\ArchPresets;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/**
* @internal
*/
@@ -92,9 +89,5 @@ final class Php extends AbstractPreset
'xdebug_var_dump',
'trap',
])->not->toBeUsed();
$this->eachUserNamespace(
fn (Expectation $namespace): ArchExpectation => $namespace->not->toHaveSuspiciousCharacters(),
);
}
}
+1
View File
@@ -32,6 +32,7 @@ final class Security extends AbstractPreset
'create_function',
'unserialize',
'extract',
'parse_str',
'mb_parse_str',
'dl',
'assert',
+1 -1
View File
@@ -17,7 +17,7 @@ final class BootExcludeList implements Bootstrapper
*
* @var array<int, non-empty-string>
*/
private const array EXCLUDE_LIST = [
private const EXCLUDE_LIST = [
'bin',
'overrides',
'resources',
+1 -6
View File
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Pest\Bootstrappers;
use Pest\Contracts\Bootstrapper;
use Pest\Exceptions\FatalException;
use Pest\Support\DatasetInfo;
use Pest\Support\Str;
use Pest\TestSuite;
@@ -25,7 +24,7 @@ final class BootFiles implements Bootstrapper
*
* @var array<int, string>
*/
private const array STRUCTURE = [
private const STRUCTURE = [
'Expectations',
'Expectations.php',
'Helpers',
@@ -41,10 +40,6 @@ final class BootFiles implements Bootstrapper
$rootPath = TestSuite::getInstance()->rootPath;
$testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory();
if (! is_dir($testsPath)) {
throw new FatalException(sprintf('The test directory [%s] does not exist.', $testsPath));
}
foreach (self::STRUCTURE as $filename) {
$filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename);
+10 -11
View File
@@ -15,18 +15,17 @@ final class BootOverrides implements Bootstrapper
/**
* The list of files to be overridden.
*
* @var array<int, string>
* @var array<string, string>
*/
public const array FILES = [
'Runner/Filter/NameFilterIterator.php',
'Runner/ResultCache/DefaultResultCache.php',
'Runner/TestSuiteLoader.php',
'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
'TextUI/TestSuiteFilterProcessor.php',
'Event/Value/ThrowableBuilder.php',
'Logging/JUnit/JunitXmlLogger.php',
'Report/PHP.php',
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',
];
/**
+1 -1
View File
@@ -20,7 +20,7 @@ final readonly class BootSubscribers implements Bootstrapper
*
* @var array<int, class-string<Subscriber>>
*/
private const array SUBSCRIBERS = [
private const SUBSCRIBERS = [
Subscribers\EnsureConfigurationIsAvailable::class,
Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
Subscribers\EnsureKernelDumpIsFlushed::class,
+1 -1
View File
@@ -66,6 +66,6 @@ trait Pipeable
*/
private function pipes(string $name, object $context, string $scope): array
{
return array_map(fn (Closure $pipe): \Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []);
return array_map(fn (Closure $pipe): Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []);
}
}
+37 -47
View File
@@ -6,12 +6,10 @@ namespace Pest\Concerns;
use Closure;
use Pest\Exceptions\DatasetArgumentsMismatch;
use Pest\Panic;
use Pest\Preset;
use Pest\Support\ChainableClosure;
use Pest\Support\ExceptionTrace;
use Pest\Support\Reflection;
use Pest\Support\Shell;
use Pest\TestSuite;
use PHPUnit\Framework\Attributes\PostCondition;
use PHPUnit\Framework\TestCase;
@@ -103,6 +101,27 @@ trait Testable
*/
private array $__snapshotChanges = [];
/**
* Creates a new Test Case instance.
*/
public function __construct(string $name)
{
parent::__construct($name);
$test = TestSuite::getInstance()->tests->get(self::$__filename);
if ($test->hasMethod($name)) {
$method = $test->getMethod($name);
$this->__description = self::$__latestDescription = $method->description;
self::$__latestAssignees = $method->assignees;
self::$__latestNotes = $method->notes;
self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs;
$this->__describing = $method->describing;
$this->__test = $method->getClosure();
}
}
/**
* Resets the test case static properties.
*/
@@ -129,7 +148,7 @@ trait Testable
*/
public function __addBeforeAll(?Closure $hook): void
{
if (! $hook instanceof \Closure) {
if (! $hook instanceof Closure) {
return;
}
@@ -143,7 +162,7 @@ trait Testable
*/
public function __addAfterAll(?Closure $hook): void
{
if (! $hook instanceof \Closure) {
if (! $hook instanceof Closure) {
return;
}
@@ -173,7 +192,7 @@ trait Testable
*/
private function __addHook(string $property, ?Closure $hook): void
{
if (! $hook instanceof \Closure) {
if (! $hook instanceof Closure) {
return;
}
@@ -195,11 +214,7 @@ trait Testable
$beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll);
}
try {
call_user_func(Closure::bind($beforeAll, null, self::class));
} catch (Throwable $e) {
Panic::with($e);
}
call_user_func(Closure::bind($beforeAll, null, self::class));
}
/**
@@ -227,6 +242,8 @@ trait Testable
$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')
@@ -268,33 +285,6 @@ trait Testable
$this->__callClosure($beforeEach, $arguments);
}
/**
* Initialize test case properties from TestSuite.
*/
public function __initializeTestCase(): void
{
// Return if the test case has already been initialized
if (isset($this->__test)) {
return;
}
$name = $this->name();
$test = TestSuite::getInstance()->tests->get(self::$__filename);
if ($test->hasMethod($name)) {
$method = $test->getMethod($name);
$this->__description = self::$__latestDescription = $method->description;
self::$__latestAssignees = $method->assignees;
self::$__latestNotes = $method->notes;
self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs;
$this->__describing = $method->describing;
$this->__test = $method->getClosure();
$method->setUp($this);
}
}
/**
* Gets executed after the Test Case.
*/
@@ -444,7 +434,15 @@ trait Testable
return;
}
$this->markTestIncomplete(implode('. ', $this->__snapshotChanges));
if (count($this->__snapshotChanges) === 1) {
$this->markTestIncomplete($this->__snapshotChanges[0]);
return;
}
$messages = implode(PHP_EOL, array_map(static fn (string $message): string => '- $message', $this->__snapshotChanges));
$this->markTestIncomplete($messages);
}
/**
@@ -468,7 +466,7 @@ trait Testable
*/
public static function getLatestPrintableTestCaseMethodName(): string
{
return self::$__latestDescription ?? '';
return self::$__latestDescription;
}
/**
@@ -483,12 +481,4 @@ trait Testable
'notes' => self::$__latestNotes,
];
}
/**
* Opens a shell for the test case.
*/
public function shell(): void
{
Shell::open();
}
}
-8
View File
@@ -102,14 +102,6 @@ final readonly class Configuration
return Configuration\Project::getInstance();
}
/**
* Gets the browser configuration.
*/
public function browser(): Browser\Configuration
{
return new Browser\Configuration;
}
/**
* Proxies calls to the uses method.
*
+1 -1
View File
@@ -16,7 +16,7 @@ final readonly class Help
*
* @var array<int, string>
*/
private const array HELP_MESSAGES = [
private const HELP_MESSAGES = [
'<comment>Pest Options:</comment>',
' <info>--init</info> Initialise a standard Pest configuration',
' <info>--coverage</info> Enable coverage and output to standard output',
+1 -1
View File
@@ -22,7 +22,7 @@ final readonly class Thanks
*
* @var array<string, string>
*/
private const array FUNDING_MESSAGES = [
private const FUNDING_MESSAGES = [
'Star' => 'https://github.com/pestphp/pest',
'YouTube' => 'https://youtube.com/@nunomaduro',
'TikTok' => 'https://tiktok.com/@nunomaduro',
+1 -1
View File
@@ -22,7 +22,7 @@ final class DatasetMissing extends BadFunctionCallException implements Exception
public function __construct(string $file, string $name, array $arguments)
{
parent::__construct(sprintf(
'A test with the description [%s] has [%d] argument(s) ([%s]) and no dataset(s) provided in [%s]',
"A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s",
$name,
count($arguments),
implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)),
+2 -6
View File
@@ -19,11 +19,7 @@ final class TestCaseAlreadyInUse extends InvalidArgumentException implements Exc
*/
public function __construct(string $inUse, string $newOne, string $folder)
{
parent::__construct(sprintf(
'Test case [%s] can not be used. The folder [%s] already uses the test case [%s].',
$newOne,
$folder,
$inUse,
));
parent::__construct(sprintf('Test case `%s` can not be used. The folder `%s` already uses the test case `%s`',
$newOne, $folder, $inUse));
}
}
@@ -22,7 +22,7 @@ final class TestClosureMustNotBeStatic extends InvalidArgumentException implemen
{
parent::__construct(
sprintf(
'Test closure must not be static. Please remove the [static] keyword from the [%s] method in [%s].',
'Test closure must not be static. Please remove the `static` keyword from the `%s` method in `%s`.',
$method->description,
$method->filename
)
+3 -15
View File
@@ -134,7 +134,7 @@ final class Expectation
/**
* Dump the expectation value when the result of the condition is truthy.
*
* @param (\Closure(TValue): bool)|bool $condition
* @param (Closure(TValue): bool)|bool $condition
* @return self<TValue>
*/
public function ddWhen(Closure|bool $condition, mixed ...$arguments): Expectation
@@ -151,7 +151,7 @@ final class Expectation
/**
* Dump the expectation value when the result of the condition is falsy.
*
* @param (\Closure(TValue): bool)|bool $condition
* @param (Closure(TValue): bool)|bool $condition
* @return self<TValue>
*/
public function ddUnless(Closure|bool $condition, mixed ...$arguments): Expectation
@@ -330,7 +330,7 @@ final class Expectation
* @param array<int, mixed> $parameters
* @return Expectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue>
*/
public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation|ArchExpectation
public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation
{
if (! self::hasMethod($method)) {
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $method)) {
@@ -355,10 +355,6 @@ final class Expectation
$reflectionClosure = new \ReflectionFunction($closure);
$expectation = $reflectionClosure->getClosureThis();
if ($reflectionClosure->getReturnType()?->__toString() === ArchExpectation::class) {
return $closure(...$parameters);
}
assert(is_object($expectation));
ExpectationPipeline::for($closure)
@@ -894,14 +890,6 @@ final class Expectation
return ToUseNothing::make($this);
}
/**
* Asserts that the source code of the given expectation target does not include suspicious characters.
*/
public function toHaveSuspiciousCharacters(): ArchExpectation
{
throw InvalidExpectation::fromMethods(['toHaveSuspiciousCharacters']);
}
/**
* Not supported.
*/
-19
View File
@@ -24,7 +24,6 @@ use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExpectationFailedException;
use ReflectionMethod;
use ReflectionProperty;
use Spoofchecker;
use stdClass;
/**
@@ -279,24 +278,6 @@ final readonly class OppositeExpectation
);
}
/**
* Asserts that the given expectation target does not have suspicious characters.
*/
public function toHaveSuspiciousCharacters(): ArchExpectation
{
$checker = new Spoofchecker;
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make(
$original,
fn (ObjectDescription $object): bool => ! $checker->isSuspicious((string) file_get_contents($object->path)),
'to not include suspicious characters',
FileLineFinder::where(fn (string $line): bool => $checker->isSuspicious($line)),
);
}
/**
* Asserts that the given expectation target does not have the given methods.
*
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Covers;
/**
* @internal
*/
final class CoversNothing {}
+7 -3
View File
@@ -17,6 +17,7 @@ use Pest\Factories\Concerns\HigherOrderable;
use Pest\Support\Reflection;
use Pest\Support\Str;
use Pest\TestSuite;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
use RuntimeException;
@@ -88,7 +89,8 @@ final class TestCaseFactory
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', static fn (array $match): string => strtolower($match['drive']), $filename);
}
$filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename)));
$realpath = (string) realpath($filename);
$filename = str_replace('\\\\', '\\', addslashes($realpath));
$rootPath = TestSuite::getInstance()->rootPath;
$relativePath = str_replace($rootPath.DIRECTORY_SEPARATOR, '', $filename);
@@ -135,7 +137,7 @@ final class TestCaseFactory
$this->attributes = [
new Attribute(
\PHPUnit\Framework\Attributes\TestDox::class,
TestDox::class,
[$this->filename],
),
...$this->attributes,
@@ -143,6 +145,8 @@ final class TestCaseFactory
$attributesCode = Attributes::code($this->attributes);
$filenameLiteral = var_export($realpath, true);
$methodsCode = implode('', array_map(
fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(),
$methods
@@ -160,7 +164,7 @@ final class TestCaseFactory
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode
private static \$__filename = '$filename';
private static \$__filename = $filenameLiteral;
$methodsCode
}
+7 -4
View File
@@ -13,6 +13,9 @@ use Pest\Support\Str;
use Pest\TestSuite;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Depends;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
/**
@@ -32,7 +35,7 @@ final class TestCaseMethodFactory
/**
* The test's describing, if any.
*
* @var array<int, \Pest\Support\Description>
* @var array<int, string>
*/
public array $describing = [];
@@ -192,11 +195,11 @@ final class TestCaseMethodFactory
$this->attributes = [
new Attribute(
\PHPUnit\Framework\Attributes\Test::class,
Test::class,
[],
),
new Attribute(
\PHPUnit\Framework\Attributes\TestDox::class,
TestDox::class,
[str_replace('*/', '{@*}', $this->description)],
),
...$this->attributes,
@@ -206,7 +209,7 @@ final class TestCaseMethodFactory
$depend = Str::evaluable($this->describing === [] ? $depend : Str::describe($this->describing, $depend));
$this->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\Depends::class,
Depends::class,
[$depend],
);
}
+4 -56
View File
@@ -2,14 +2,11 @@
declare(strict_types=1);
use Pest\Browser\Api\ArrayablePendingAwaitablePage;
use Pest\Browser\Api\PendingAwaitablePage;
use Pest\Concerns\Expectable;
use Pest\Configuration;
use Pest\Exceptions\AfterAllWithinDescribe;
use Pest\Exceptions\BeforeAllWithinDescribe;
use Pest\Expectation;
use Pest\Installers\PluginBrowser;
use Pest\Mutate\Contracts\MutationTestRunner;
use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\AfterEachCall;
@@ -21,7 +18,6 @@ use Pest\Repositories\DatasetsRepository;
use Pest\Support\Backtrace;
use Pest\Support\Container;
use Pest\Support\DatasetInfo;
use Pest\Support\Description;
use Pest\Support\HigherOrderTapProxy;
use Pest\TestSuite;
use PHPUnit\Framework\TestCase;
@@ -99,7 +95,7 @@ if (! function_exists('describe')) {
{
$filename = Backtrace::testFile();
return new DescribeCall(TestSuite::getInstance(), $filename, new Description($description), $tests);
return new DescribeCall(TestSuite::getInstance(), $filename, $description, $tests);
}
}
@@ -140,7 +136,7 @@ if (! function_exists('test')) {
*/
function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall
{
if ($description === null && TestSuite::getInstance()->test instanceof \PHPUnit\Framework\TestCase) {
if ($description === null && TestSuite::getInstance()->test instanceof TestCase) {
return new HigherOrderTapProxy(TestSuite::getInstance()->test);
}
@@ -236,7 +232,7 @@ if (! function_exists('covers')) {
/** @var MutationTestRunner $runner */
$runner = Container::getInstance()->get(MutationTestRunner::class);
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */
/** @var ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
@@ -263,7 +259,7 @@ if (! function_exists('mutates')) {
/** @var MutationTestRunner $runner */
$runner = Container::getInstance()->get(MutationTestRunner::class);
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */
/** @var ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
@@ -282,51 +278,3 @@ if (! function_exists('mutates')) {
}
}
}
if (! function_exists('fixture')) {
/**
* Returns the absolute path to a fixture file.
*/
function fixture(string $file): string
{
$file = implode(DIRECTORY_SEPARATOR, [
TestSuite::getInstance()->rootPath,
TestSuite::getInstance()->testPath,
'Fixtures',
str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file),
]);
$fileRealPath = realpath($file);
if ($fileRealPath === false) {
throw new InvalidArgumentException(
'The fixture file ['.$file.'] does not exist.',
);
}
return $fileRealPath;
}
}
if (! function_exists('visit')) {
/**
* Browse to the given URL.
*
* @template TUrl of array<int, string>|string
*
* @param TUrl $url
* @param array<string, mixed> $options
* @return (TUrl is array<int, string> ? ArrayablePendingAwaitablePage : PendingAwaitablePage)
*/
function visit(array|string $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage
{
if (! class_exists(\Pest\Browser\Configuration::class)) {
PluginBrowser::install();
exit(0);
}
// @phpstan-ignore-next-line
return test()->visit($url, $options);
}
}
-15
View File
@@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Installers;
use Pest\Support\View;
final readonly class PluginBrowser
{
public static function install(): void
{
View::render('installers/plugin-browser');
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ final readonly class Kernel
*
* @var array<int, class-string>
*/
private const array BOOTSTRAPPERS = [
private const BOOTSTRAPPERS = [
Bootstrappers\BootOverrides::class,
Bootstrappers\BootSubscribers::class,
Bootstrappers\BootFiles::class,
+3 -3
View File
@@ -31,7 +31,7 @@ final readonly class Converter
/**
* The prefix for the test suite name.
*/
private const string PREFIX = 'P\\';
private const PREFIX = 'P\\';
/**
* The state generator.
@@ -151,7 +151,7 @@ final readonly class Converter
{
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
$firstTest = $this->getFirstTest($testSuite);
if ($firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
if ($firstTest instanceof TestMethod) {
return $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
}
}
@@ -179,7 +179,7 @@ final readonly class Converter
public function getTestSuiteLocation(TestSuite $testSuite): ?string
{
$firstTest = $this->getFirstTest($testSuite);
if (! $firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
if (! $firstTest instanceof TestMethod) {
return null;
}
$path = $firstTest->testDox()->prettifiedClassName();
+1 -1
View File
@@ -200,7 +200,7 @@ final class TeamCityLogger
public function testFinished(Finished $event): void
{
if (! $this->time instanceof \PHPUnit\Event\Telemetry\HRTime) {
if (! $this->time instanceof HRTime) {
throw ShouldNotHappen::fromMessage('Start time has not been set.');
}
+20 -32
View File
@@ -9,6 +9,7 @@ use Closure;
use Countable;
use DateTimeInterface;
use Error;
use Illuminate\Testing\TestResponse;
use InvalidArgumentException;
use JsonSerializable;
use Pest\Exceptions\InvalidExpectationValue;
@@ -781,13 +782,15 @@ final class Expectation
foreach ($array as $key => $value) {
Assert::assertArrayHasKey($key, $valueAsArray, $message);
$assertMessage = $message !== '' ? $message : sprintf(
'Failed asserting that an array has a key %s with the value %s.',
$this->export($key),
$this->export($valueAsArray[$key]),
);
if ($message === '') {
$message = sprintf(
'Failed asserting that an array has a key %s with the value %s.',
$this->export($key),
$this->export($valueAsArray[$key]),
);
}
Assert::assertEquals($value, $valueAsArray[$key], $assertMessage);
Assert::assertEquals($value, $valueAsArray[$key], $message);
}
return $this;
@@ -800,7 +803,7 @@ final class Expectation
* @param iterable<string, mixed> $object
* @return self<TValue>
*/
public function toMatchObject(object|iterable $object, string $message = ''): self
public function toMatchObject(iterable $object, string $message = ''): self
{
foreach ((array) $object as $property => $value) {
if (! is_object($this->value) && ! is_string($this->value)) {
@@ -812,13 +815,15 @@ final class Expectation
/* @phpstan-ignore-next-line */
$propertyValue = $this->value->{$property};
$assertMessage = $message !== '' ? $message : sprintf(
'Failed asserting that an object has a property %s with the value %s.',
$this->export($property),
$this->export($propertyValue),
);
if ($message === '') {
$message = sprintf(
'Failed asserting that an object has a property %s with the value %s.',
$this->export($property),
$this->export($propertyValue),
);
}
Assert::assertEquals($value, $propertyValue, $assertMessage);
Assert::assertEquals($value, $propertyValue, $message);
}
return $this;
@@ -842,7 +847,7 @@ final class Expectation
is_object($this->value) && method_exists($this->value, 'toSnapshot') => $this->value->toSnapshot(),
is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(),
is_object($this->value) && method_exists($this->value, 'toString') => $this->value->toString(),
$this->value instanceof \Illuminate\Testing\TestResponse => $this->value->getContent(), // @phpstan-ignore-line
$this->value instanceof TestResponse => $this->value->getContent(), // @phpstan-ignore-line
is_array($this->value) => json_encode($this->value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
$this->value instanceof Traversable => json_encode(iterator_to_array($this->value), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
$this->value instanceof JsonSerializable => json_encode($this->value->jsonSerialize(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
@@ -983,7 +988,7 @@ final class Expectation
*/
private function export(mixed $value): string
{
if (! $this->exporter instanceof \Pest\Support\Exporter) {
if (! $this->exporter instanceof Exporter) {
$this->exporter = Exporter::default();
}
@@ -1154,21 +1159,4 @@ final class Expectation
return $this;
}
/**
* Asserts that the value can be converted to a slug
*
* @return self<TValue>
*/
public function toBeSlug(string $message = ''): self
{
if ($message === '') {
$message = "Failed asserting that {$this->value} can be converted to a slug.";
}
$slug = Str::slugify((string) $this->value);
Assert::assertNotEmpty($slug, $message);
return $this;
}
}
+2 -2
View File
@@ -12,14 +12,14 @@ trait Describable
/**
* Note: this is property is not used; however, it gets added automatically by rector php.
*
* @var array<int, \Pest\Support\Description>
* @var array<int, string>
*/
public array $__describing;
/**
* The describing of the test case.
*
* @var array<int, \Pest\Support\Description>
* @var array<int, string>
*/
public array $describing = [];
}
+4 -5
View File
@@ -6,7 +6,6 @@ namespace Pest\PendingCalls;
use Closure;
use Pest\Support\Backtrace;
use Pest\Support\Description;
use Pest\TestSuite;
/**
@@ -17,7 +16,7 @@ final class DescribeCall
/**
* The current describe call.
*
* @var array<int, Description>
* @var array<int, string>
*/
private static array $describing = [];
@@ -32,7 +31,7 @@ final class DescribeCall
public function __construct(
public readonly TestSuite $testSuite,
public readonly string $filename,
public readonly Description $description,
public readonly string $description,
public readonly Closure $tests
) {
//
@@ -41,7 +40,7 @@ final class DescribeCall
/**
* What is the current describing.
*
* @return array<int, Description>
* @return array<int, string>
*/
public static function describing(): array
{
@@ -73,7 +72,7 @@ final class DescribeCall
{
$filename = Backtrace::file();
if (! $this->currentBeforeEachCall instanceof \Pest\PendingCalls\BeforeEachCall) {
if (! $this->currentBeforeEachCall instanceof BeforeEachCall) {
$this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename);
$this->currentBeforeEachCall->describing[] = $this->description;
+18 -79
View File
@@ -12,7 +12,6 @@ use Pest\Factories\Attribute;
use Pest\Factories\TestCaseMethodFactory;
use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\Concerns\Describable;
use Pest\Plugins\Environment;
use Pest\Plugins\Only;
use Pest\Support\Backtrace;
use Pest\Support\Container;
@@ -22,6 +21,11 @@ use Pest\Support\NullClosure;
use Pest\Support\Str;
use Pest\TestSuite;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversFunction;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\CoversTrait;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
/**
@@ -179,9 +183,10 @@ final class TestCall // @phpstan-ignore-line
}
/**
* Runs the current test multiple times with each item of the given `iterable`.
* Runs the current test multiple times with
* each item of the given `iterable`.
*
* @param Closure|iterable<array-key, mixed>|string $data
* @param array<Closure|iterable<int|string, mixed>|string> $data
*/
public function with(Closure|iterable|string ...$data): self
{
@@ -211,7 +216,7 @@ final class TestCall // @phpstan-ignore-line
{
foreach ($groups as $group) {
$this->testCaseMethod->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\Group::class,
Group::class,
[$group],
);
}
@@ -315,61 +320,6 @@ final class TestCall // @phpstan-ignore-line
: $this;
}
/**
* Weather the current test is running on a CI environment.
*/
private function runningOnCI(): bool
{
foreach ([
'CI',
'GITHUB_ACTIONS',
'GITLAB_CI',
'CIRCLECI',
'TRAVIS',
'APPVEYOR',
'BITBUCKET_BUILD_NUMBER',
'BUILDKITE',
'TEAMCITY_VERSION',
'JENKINS_URL',
'SYSTEM_COLLECTIONURI',
'CI_NAME',
'TASKCLUSTER_ROOT_URL',
'DRONE',
'WERCKER',
'NEVERCODE',
'SEMAPHORE',
'NETLIFY',
'NOW_BUILDER',
] as $env) {
if (getenv($env) !== false) {
return true;
}
}
return Environment::name() === Environment::CI;
}
/**
* Skips the current test when running on a CI environments.
*/
public function skipOnCI(): self
{
if ($this->runningOnCI()) {
return $this->skip('This test is skipped on [CI].');
}
return $this;
}
public function skipLocally(): self
{
if ($this->runningOnCI() === false) {
return $this->skip('This test is skipped [locally].');
}
return $this;
}
/**
* Skips the current test unless the given test is running on Windows.
*/
@@ -604,7 +554,7 @@ final class TestCall // @phpstan-ignore-line
{
foreach ($classes as $class) {
$this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversClass::class,
CoversClass::class,
[$class],
);
}
@@ -627,7 +577,7 @@ final class TestCall // @phpstan-ignore-line
{
foreach ($traits as $trait) {
$this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversTrait::class,
CoversTrait::class,
[$trait],
);
}
@@ -650,7 +600,7 @@ final class TestCall // @phpstan-ignore-line
{
foreach ($functions as $function) {
$this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversFunction::class,
CoversFunction::class,
[$function],
);
}
@@ -659,29 +609,18 @@ final class TestCall // @phpstan-ignore-line
}
/**
* Adds one or more references to the tested method or class. This helps
* to link test cases to the source code for easier navigation.
*
* @param array<class-string|string>|class-string ...$classes
* Sets that the current test covers nothing.
*/
public function references(string|array ...$classes): self
public function coversNothing(): self
{
assert($classes !== []);
$this->testCaseMethod->attributes[] = new Attribute(
CoversNothing::class,
[],
);
return $this;
}
/**
* Adds one or more references to the tested method or class. This helps
* to link test cases to the source code for easier navigation.
*
* @param array<class-string|string>|class-string ...$classes
*/
public function see(string|array ...$classes): self
{
return $this->references(...$classes);
}
/**
* Informs the test runner that no expectations happen in this test,
* and its purpose is simply to check whether the given code can
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Pest;
function version(): string
{
return '4.0.0';
return '3.8.6';
}
function testDirectory(string $file = ''): string
+1 -1
View File
@@ -21,7 +21,7 @@ final class Cache implements HandlesArguments
/**
* The temporary folder.
*/
private const string TEMPORARY_FOLDER = __DIR__
private const TEMPORARY_FOLDER = __DIR__
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
+2 -2
View File
@@ -21,7 +21,7 @@ final class Configuration implements HandlesArguments, Terminable
/**
* The base PHPUnit file.
*/
public const string BASE_PHPUNIT_FILE = __DIR__
public const BASE_PHPUNIT_FILE = __DIR__
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
@@ -34,7 +34,7 @@ final class Configuration implements HandlesArguments, Terminable
*/
public function handleArguments(array $arguments): array
{
if ($this->hasArgument('--configuration', $arguments) || $this->hasArgument('-c', $arguments) || $this->hasCustomConfigurationFile()) {
if ($this->hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) {
return $arguments;
}
+12 -3
View File
@@ -17,11 +17,20 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
final class Coverage implements AddsOutput, HandlesArguments
{
private const string COVERAGE_OPTION = 'coverage';
/**
* @var string
*/
private const COVERAGE_OPTION = 'coverage';
private const string MIN_OPTION = 'min';
/**
* @var string
*/
private const MIN_OPTION = 'min';
private const string EXACTLY_OPTION = 'exactly';
/**
* @var string
*/
private const EXACTLY_OPTION = 'exactly';
/**
* Whether it should show the coverage or not.
+2 -2
View File
@@ -14,12 +14,12 @@ final class Environment implements HandlesArguments
/**
* The continuous integration environment.
*/
public const string CI = 'ci';
public const CI = 'ci';
/**
* The local environment.
*/
public const string LOCAL = 'local';
public const LOCAL = 'local';
/**
* The current environment.
+2 -2
View File
@@ -20,12 +20,12 @@ final readonly class Init implements HandlesArguments
/**
* The option the triggers the init job.
*/
private const string INIT_OPTION = '--init';
private const INIT_OPTION = '--init';
/**
* The files that will be created.
*/
private const array STUBS = [
private const STUBS = [
'phpunit.xml.stub' => 'phpunit.xml',
'Pest.php.stub' => 'tests/Pest.php',
'TestCase.php.stub' => 'tests/TestCase.php',
+19 -29
View File
@@ -5,10 +5,7 @@ declare(strict_types=1);
namespace Pest\Plugins;
use Pest\Contracts\Plugins\Terminable;
use Pest\Factories\Attribute;
use Pest\Factories\TestCaseMethodFactory;
use Pest\PendingCalls\TestCall;
use PHPUnit\Framework\Attributes\Group;
/**
* @internal
@@ -18,7 +15,7 @@ final class Only implements Terminable
/**
* The temporary folder.
*/
private const string TEMPORARY_FOLDER = __DIR__
private const TEMPORARY_FOLDER = __DIR__
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
@@ -26,19 +23,28 @@ final class Only implements Terminable
.DIRECTORY_SEPARATOR
.'.temp';
/**
* {@inheritDoc}
*/
public function terminate(): void
{
if (Parallel::isWorker()) {
return;
}
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (file_exists($lockFile)) {
unlink($lockFile);
}
}
/**
* Creates the lock file.
*/
public static function enable(TestCall|TestCaseMethodFactory $testCall, string $group = '__pest_only'): void
public static function enable(TestCall $testCall, string $group = '__pest_only'): void
{
if ($testCall instanceof TestCall) {
$testCall->group($group);
} else {
$testCall->attributes[] = new Attribute(
Group::class,
[$group],
);
}
$testCall->group($group);
if (Environment::name() === Environment::CI || Parallel::isWorker()) {
return;
@@ -82,20 +88,4 @@ final class Only implements Terminable
return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line
}
/**
* {@inheritDoc}
*/
public function terminate(): void
{
if (Parallel::isWorker()) {
return;
}
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (file_exists($lockFile)) {
unlink($lockFile);
}
}
}
+3 -3
View File
@@ -23,9 +23,9 @@ final class Parallel implements HandlesArguments
{
use HandleArguments;
private const string GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_';
private const GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_';
private const array HANDLERS = [
private const HANDLERS = [
Parallel\Handlers\Parallel::class,
Parallel\Handlers\Pest::class,
Parallel\Handlers\Laravel::class,
@@ -34,7 +34,7 @@ final class Parallel implements HandlesArguments
/**
* @var string[]
*/
private const array UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request'];
private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request'];
/**
* Whether the given command line arguments indicate that the test suite should be run in parallel.
+3 -2
View File
@@ -7,6 +7,7 @@ namespace Pest\Plugins\Parallel\Handlers;
use Closure;
use Composer\InstalledVersions;
use Illuminate\Testing\ParallelRunner;
use Orchestra\Testbench\TestCase;
use ParaTest\Options;
use ParaTest\RunnerInterface;
use Pest\Contracts\Plugins\HandlesArguments;
@@ -39,13 +40,13 @@ final class Laravel implements HandlesArguments
* Executes the given closure when running Laravel.
*
* @param array<int, string> $arguments
* @param CLosure(array<int, string>): array<int, string> $closure
* @param Closure(array<int, string>): array<int, string> $closure
* @return array<int, string>
*/
private function whenUsingLaravel(array $arguments, Closure $closure): array
{
$isLaravelApplication = InstalledVersions::isInstalled('laravel/framework', false);
$isLaravelPackage = class_exists(\Orchestra\Testbench\TestCase::class);
$isLaravelPackage = class_exists(TestCase::class);
if ($isLaravelApplication && ! $isLaravelPackage) {
return $closure($arguments);
+1 -1
View File
@@ -18,7 +18,7 @@ final class Parallel implements HandlesArguments
/**
* The list of arguments to remove.
*/
private const array ARGS_TO_REMOVE = [
private const ARGS_TO_REMOVE = [
'--parallel',
'-p',
'--no-output',
@@ -11,7 +11,6 @@ final class CleanConsoleOutput extends ConsoleOutput
/**
* {@inheritdoc}
*/
#[\Override]
protected function doWrite(string $message, bool $newline): void // @pest-arch-ignore-line
{
if ($this->isOpeningHeadline($message)) {
@@ -59,10 +59,10 @@ final class ResultPrinter
private readonly OutputInterface $output,
private readonly Options $options
) {
$this->printer = new readonly class($this->output) implements Printer
$this->printer = new class($this->output) implements Printer
{
public function __construct(
private OutputInterface $output,
private readonly OutputInterface $output,
) {}
public function print(string $buffer): void
@@ -17,7 +17,6 @@ use ParaTest\WrapperRunner\WrapperWorker;
use Pest\Result;
use Pest\TestSuite;
use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Event\Test\AfterLastTestMethodFailed;
use PHPUnit\Event\TestRunner\WarningTriggered;
use PHPUnit\Runner\CodeCoverage;
use PHPUnit\Runner\ResultCache\DefaultResultCache;
@@ -51,7 +50,7 @@ final class WrapperRunner implements RunnerInterface
/**
* The time to sleep between cycles.
*/
private const int CYCLE_SLEEP = 10000;
private const CYCLE_SLEEP = 10000;
/**
* The result printer.
@@ -314,42 +313,27 @@ final class WrapperRunner implements RunnerInterface
$testResult = unserialize($contents);
assert($testResult instanceof TestResult);
/** @var list<AfterLastTestMethodFailed> $failedEvents */
$failedEvents = array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents());
$testResultSum = new TestResult(
(int) $testResultSum->hasTests() + (int) $testResult->hasTests(),
$testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
$testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(),
array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()),
$failedEvents,
array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()),
array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()),
array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()),
array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()),
array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredNoticeEvents(), $testResult->testRunnerTriggeredNoticeEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->errors(), $testResult->errors()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->notices(), $testResult->notices()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->warnings(), $testResult->warnings()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()),
$testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
);
@@ -367,10 +351,8 @@ final class WrapperRunner implements RunnerInterface
$testResultSum->testMarkedIncompleteEvents(),
$testResultSum->testTriggeredPhpunitDeprecationEvents(),
$testResultSum->testTriggeredPhpunitErrorEvents(),
$testResultSum->testTriggeredPhpunitNoticeEvents(),
$testResultSum->testTriggeredPhpunitWarningEvents(),
$testResultSum->testRunnerTriggeredDeprecationEvents(),
$testResultSum->testRunnerTriggeredNoticeEvents(),
array_values(array_filter(
$testResultSum->testRunnerTriggeredWarningEvents(),
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')
@@ -34,7 +34,7 @@ final class CompactPrinter
/**
* @var array<string, array<int, string>>
*/
private const array LOOKUP_TABLE = [
private const LOOKUP_TABLE = [
'.' => ['gray', '.'],
'S' => ['yellow', 's'],
'T' => ['cyan', 't'],
@@ -131,14 +131,14 @@ final class CompactPrinter
$status['collected'],
$status['threshold'],
$status['roots'],
0.00,
0.00,
0.00,
0.00,
false,
false,
false,
0,
null,
null,
null,
null,
null,
null,
null,
null,
);
$telemetry = new Info(
-177
View File
@@ -1,177 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins;
use Pest\Contracts\Plugins\AddsOutput;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Exceptions\InvalidOption;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Process;
/**
* @internal
*/
final class Shard implements AddsOutput, HandlesArguments
{
use Concerns\HandleArguments;
private const string SHARD_OPTION = 'shard';
/**
* The shard index and total number of shards.
*
* @var array{
* index: int,
* total: int,
* testsRan: int,
* testsCount: int
* }|null
*/
private static ?array $shard = null;
/**
* Creates a new Plugin instance.
*/
public function __construct(
private readonly OutputInterface $output,
) {
//
}
/**
* {@inheritDoc}
*/
public function handleArguments(array $arguments): array
{
if (! $this->hasArgument('--shard', $arguments)) {
return $arguments;
}
// @phpstan-ignore-next-line
$input = new ArgvInput($arguments);
['index' => $index, 'total' => $total] = self::getShard($input);
$arguments = $this->popArgument("--shard=$index/$total", $this->popArgument('--shard', $this->popArgument(
"$index/$total",
$arguments,
)));
/** @phpstan-ignore-next-line */
$tests = $this->allTests($arguments);
$testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? [];
self::$shard = [
'index' => $index,
'total' => $total,
'testsRan' => count($testsToRun),
'testsCount' => count($tests),
];
return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)];
}
/**
* Returns all tests that the test suite would run.
*
* @param list<string> $arguments
* @return list<string>
*/
private function allTests(array $arguments): array
{
$output = (new Process([
'php',
...$this->removeParallelArguments($arguments),
'--list-tests',
]))->mustRun()->getOutput();
preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches);
return array_values(array_unique($matches[1]));
}
/**
* @param array<int, string> $arguments
* @return array<int, string>
*/
private function removeParallelArguments(array $arguments): array
{
return array_filter($arguments, fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true));
}
/**
* Builds the filter argument for the given tests to run.
*/
private function buildFilterArgument(mixed $testsToRun): string
{
return addslashes(implode('|', $testsToRun));
}
/**
* Adds output after the Test Suite execution.
*/
public function addOutput(int $exitCode): int
{
if (self::$shard === null) {
return $exitCode;
}
[
'index' => $index,
'total' => $total,
'testsRan' => $testsRan,
'testsCount' => $testsCount,
] = self::$shard;
$this->output->writeln(sprintf(
' <fg=gray>Shard:</> <fg=default>%d of %d</> — %d file%s ran, out of %d.',
$index,
$total,
$testsRan,
$testsRan === 1 ? '' : 's',
$testsCount,
));
return $exitCode;
}
/**
* Returns the shard information.
*
* @return array{index: int, total: int}
*/
public static function getShard(InputInterface $input): array
{
if ($input->hasParameterOption('--'.self::SHARD_OPTION)) {
$shard = $input->getParameterOption('--'.self::SHARD_OPTION);
} else {
$shard = null;
}
if (! is_string($shard) || ! preg_match('/^\d+\/\d+$/', $shard)) {
throw new InvalidOption('The [--shard] option must be in the format "index/total".');
}
[$index, $total] = explode('/', $shard);
if (! is_numeric($index) || ! is_numeric($total)) {
throw new InvalidOption('The [--shard] option must be in the format "index/total".');
}
if ($index <= 0 || $total <= 0 || $index > $total) {
throw new InvalidOption('The [--shard] option index must be a non-negative integer less than the total number of shards.');
}
$index = (int) $index;
$total = (int) $total;
return [
'index' => $index,
'total' => $total,
];
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ final class Verbose implements HandlesArguments
/**
* The list of verbosity levels.
*/
private const array VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
private const VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
/**
* {@inheritDoc}
+2 -1
View File
@@ -19,7 +19,7 @@ use function sprintf;
*/
final class DatasetsRepository
{
private const string SEPARATOR = '>>';
private const SEPARATOR = '>>';
/**
* Holds the datasets.
@@ -200,6 +200,7 @@ final class DatasetsRepository
throw new DatasetDoesNotExist($name);
}
// @phpstan-ignore-next-line
return $matchingDatasets[$closestScopeDatasetKey];
}
+1 -14
View File
@@ -19,7 +19,6 @@ final class SnapshotRepository
* Creates a snapshot repository instance.
*/
public function __construct(
private readonly string $rootPath,
private readonly string $testsPath,
private readonly string $snapshotsPath,
) {}
@@ -104,19 +103,7 @@ final class SnapshotRepository
*/
private function getSnapshotFilename(): string
{
$testFile = TestSuite::getInstance()->getFilename();
if (str_starts_with($testFile, $this->testsPath)) {
// if the test file is in the tests directory
$startPath = $this->testsPath;
} else {
// if the test file is in the app, src, etc. directory
$startPath = $this->rootPath;
}
// relative path: we use substr() and not str_replace() to remove the start path
// for instance, if the $startPath is /app/ and the $testFile is /app/app/tests/Unit/ExampleTest.php, we should only remove the first /app/ from the path
$relativePath = substr($testFile, strlen($startPath));
$relativePath = str_replace($this->testsPath, '', TestSuite::getInstance()->getFilename());
// remove extension from filename
$relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.'));
+44 -4
View File
@@ -4,16 +4,20 @@ declare(strict_types=1);
namespace Pest;
use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\Configuration;
use PHPUnit\TextUI\ShellExitCodeCalculator;
/**
* @internal
*/
final class Result
{
private const int SUCCESS_EXIT = 0;
private const SUCCESS_EXIT = 0;
private const FAILURE_EXIT = 1;
private const EXCEPTION_EXIT = 2;
/**
* If the exit code is different from 0.
@@ -36,8 +40,44 @@ final class Result
*/
public static function exitCode(Configuration $configuration, TestResult $result): int
{
$shell = new ShellExitCodeCalculator;
if ($result->wasSuccessful()) {
if ($configuration->failOnWarning()) {
$warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents()
+ count($result->warnings())
+ count($result->phpWarnings());
return $shell->calculate($configuration, $result);
if ($warnings > 0) {
return self::FAILURE_EXIT;
}
}
if (! $result->hasTestTriggeredPhpunitWarningEvents()) {
return self::SUCCESS_EXIT;
}
}
if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) {
return self::FAILURE_EXIT;
}
if ($result->wasSuccessful()) {
if ($configuration->failOnRisky() && $result->hasTestConsideredRiskyEvents()) {
$returnCode = self::FAILURE_EXIT;
}
if ($configuration->failOnIncomplete() && $result->hasTestMarkedIncompleteEvents()) {
$returnCode = self::FAILURE_EXIT;
}
if ($configuration->failOnSkipped() && $result->hasTestSkippedEvents()) {
$returnCode = self::FAILURE_EXIT;
}
}
if ($result->hasTestErroredEvents()) {
return self::EXCEPTION_EXIT;
}
return self::FAILURE_EXIT;
}
}
@@ -1,39 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Runner\Filter;
use Pest\Contracts\HasPrintableTestCaseName;
use PHPUnit\Framework\Test;
use RecursiveFilterIterator;
use RecursiveIterator;
/**
* @internal
*/
final class EnsureTestCaseIsInitiatedFilter extends RecursiveFilterIterator
{
/**
* @param RecursiveIterator<int, Test> $iterator
*/
public function __construct(RecursiveIterator $iterator)
{
parent::__construct($iterator);
}
/**
* {@inheritdoc}
*/
public function accept(): bool
{
$test = $this->getInnerIterator()->current();
if ($test instanceof HasPrintableTestCaseName) {
/** @phpstan-ignore-next-line */
$test->__initializeTestCase();
}
return true;
}
}
@@ -33,7 +33,7 @@ final class EnsureIgnorableTestCasesAreIgnored implements StartedSubscriber
/** @var array<int, WarningTriggered> $testRunnerTriggeredWarningEvents */
$testRunnerTriggeredWarningEvents = $property->getValue($collector);
$testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => str_contains($event->message(), 'No tests found in class') === false));
$testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => $event->message() !== 'No tests found in class "Pest\TestCases\IgnorableTestCase".'));
$property->setValue($collector, $testRunnerTriggeredWarningEvents);
}
+5 -2
View File
@@ -11,9 +11,12 @@ use Pest\Exceptions\ShouldNotHappen;
*/
final class Backtrace
{
private const string FILE = 'file';
/**
* @var string
*/
private const FILE = 'file';
private const int BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS;
private const BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS;
/**
* Returns the current test file.
+2 -2
View File
@@ -19,14 +19,14 @@ final class Closure
*/
public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure
{
if (! $closure instanceof \Closure) {
if (! $closure instanceof BaseClosure) {
throw ShouldNotHappen::fromMessage('Could not bind null closure.');
}
// @phpstan-ignore-next-line
$closure = BaseClosure::bind($closure, $newThis, $newScope);
if (! $closure instanceof \Closure) {
if (! $closure instanceof BaseClosure) {
throw ShouldNotHappen::fromMessage('Could not bind closure.');
}
+1 -1
View File
@@ -28,7 +28,7 @@ final class Container
*/
public static function getInstance(): self
{
if (! self::$instance instanceof \Pest\Support\Container) {
if (! self::$instance instanceof Container) {
self::$instance = new self;
}
+3 -12
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Pest\Support;
use Pest\Exceptions\ShouldNotHappen;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\Directory;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\Environment\Runtime;
@@ -87,20 +88,10 @@ final class Coverage
throw ShouldNotHappen::fromMessage(sprintf('Coverage not found in path: %s.', $reportPath));
}
$handle = fopen($reportPath, 'r');
$code = '';
while (is_resource($handle) && ! feof($handle)) {
$code .= fread($handle, 8192);
}
if (is_resource($handle)) {
fclose($handle);
}
/** @var CodeCoverage $codeCoverage */
$codeCoverage = require $reportPath;
unlink($reportPath);
$codeCoverage = eval(substr($code, 5));
$totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines();
/** @var Directory<File|Directory> $report */
+2 -2
View File
@@ -11,9 +11,9 @@ use function Pest\testDirectory;
*/
final class DatasetInfo
{
public const string DATASETS_DIR_NAME = 'Datasets';
public const DATASETS_DIR_NAME = 'Datasets';
public const string DATASETS_FILE_NAME = 'Datasets.php';
public const DATASETS_FILE_NAME = 'Datasets.php';
public static function isInsideADatasetsDirectory(string $file): bool
{
-21
View File
@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
final readonly class Description implements \Stringable
{
/**
* Creates a new Description instance.
*/
public function __construct(private string $description) {}
/**
* Returns the description as a string.
*/
public function __toString(): string
{
return $this->description;
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ use Throwable;
*/
final class ExceptionTrace
{
private const string UNDEFINED_METHOD = 'Call to undefined method P\\';
private const UNDEFINED_METHOD = 'Call to undefined method P\\';
/**
* Ensures the given closure reports the good execution context.
+1 -1
View File
@@ -15,7 +15,7 @@ final readonly class Exporter
/**
* The maximum number of items in an array to export.
*/
private const int MAX_ARRAY_ITEMS = 3;
private const MAX_ARRAY_ITEMS = 3;
/**
* Creates a new Exporter instance.
+1
View File
@@ -46,6 +46,7 @@ final readonly class HigherOrderCallables
*/
public function and(mixed $value): Expectation
{
// @phpstan-ignore-next-line
return $this->expect($value);
}
+1 -1
View File
@@ -13,7 +13,7 @@ use Throwable;
*/
final class HigherOrderMessage
{
public const string UNDEFINED_METHOD = 'Method %s does not exist';
public const UNDEFINED_METHOD = 'Method %s does not exist';
/**
* An optional condition that will determine if the message will be executed.
+4 -3
View File
@@ -8,6 +8,7 @@ use Closure;
use InvalidArgumentException;
use Pest\Exceptions\ShouldNotHappen;
use Pest\TestSuite;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
@@ -66,7 +67,7 @@ final class Reflection
{
$test = TestSuite::getInstance()->test;
if (! $test instanceof \PHPUnit\Framework\TestCase) {
if (! $test instanceof TestCase) {
return self::bindCallable($callable);
}
@@ -221,7 +222,7 @@ final class Reflection
{
$getProperties = fn (ReflectionClass $reflectionClass): array => array_filter(
array_map(
fn (ReflectionProperty $property): \ReflectionProperty => $property,
fn (ReflectionProperty $property): ReflectionProperty => $property,
$reflectionClass->getProperties(),
), fn (ReflectionProperty $property): bool => $property->getDeclaringClass()->getName() === $reflectionClass->getName(),
);
@@ -256,7 +257,7 @@ final class Reflection
{
$getMethods = fn (ReflectionClass $reflectionClass): array => array_filter(
array_map(
fn (ReflectionMethod $method): \ReflectionMethod => $method,
fn (ReflectionMethod $method): ReflectionMethod => $method,
$reflectionClass->getMethods($filter),
), fn (ReflectionMethod $method): bool => $method->getDeclaringClass()->getName() === $reflectionClass->getName(),
);
-101
View File
@@ -1,101 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
use Illuminate\Support\Env;
use Laravel\Tinker\ClassAliasAutoloader;
use Pest\TestSuite;
use Psy\Configuration;
use Psy\Shell as PsyShell;
use Psy\VersionUpdater\Checker;
/**
* @internal
*/
final class Shell
{
/**
* Creates a new interactive shell.
*/
public static function open(): void
{
$config = new Configuration;
$config->setUpdateCheck(Checker::NEVER);
$config->getPresenter()->addCasters(self::casters());
$shell = new PsyShell($config);
$loader = self::tinkered($shell);
try {
$shell->run();
} finally {
$loader?->unregister(); // @phpstan-ignore-line
}
}
/**
* Returns the casters for the Psy Shell.
*
* @return array<string, callable>
*/
private static function casters(): array
{
$casters = [
'Illuminate\Support\Collection' => 'Laravel\Tinker\TinkerCaster::castCollection',
'Illuminate\Support\HtmlString' => 'Laravel\Tinker\TinkerCaster::castHtmlString',
'Illuminate\Support\Stringable' => 'Laravel\Tinker\TinkerCaster::castStringable',
];
if (class_exists('Illuminate\Database\Eloquent\Model')) {
$casters['Illuminate\Database\Eloquent\Model'] = 'Laravel\Tinker\TinkerCaster::castModel';
}
if (class_exists('Illuminate\Process\ProcessResult')) {
$casters['Illuminate\Process\ProcessResult'] = 'Laravel\Tinker\TinkerCaster::castProcessResult';
}
if (class_exists('Illuminate\Foundation\Application')) {
$casters['Illuminate\Foundation\Application'] = 'Laravel\Tinker\TinkerCaster::castApplication';
}
if (function_exists('app') === false) {
return $casters; // @phpstan-ignore-line
}
$config = app()->make('config');
return array_merge($casters, (array) $config->get('tinker.casters', []));
}
/**
* Tinkers the current shell, if the Tinker package is available.
*/
private static function tinkered(PsyShell $shell): ?object
{
if (function_exists('app') === false
|| ! class_exists(Env::class)
|| ! class_exists(ClassAliasAutoloader::class)
) {
return null;
}
$path = Env::get('COMPOSER_VENDOR_DIR', app()->basePath().DIRECTORY_SEPARATOR.'vendor');
$path .= '/composer/autoload_classmap.php';
if (! file_exists($path)) {
$path = TestSuite::getInstance()->rootPath.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'composer'.DIRECTORY_SEPARATOR.'autoload_classmap.php';
}
$config = app()->make('config');
return ClassAliasAutoloader::register(
$shell, $path, $config->get('tinker.alias', []), $config->get('tinker.dont_alias', [])
);
}
}
+6 -13
View File
@@ -13,9 +13,12 @@ final class Str
* Pool of alpha-numeric characters for generating (unsafe) random strings
* from.
*/
private const string POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
private const POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
private const string PREFIX = '__pest_evaluable_';
/**
* @var string
*/
private const PREFIX = '__pest_evaluable_';
/**
* Create a (unsecure & non-cryptographically safe) random alpha-numeric
@@ -101,7 +104,7 @@ final class Str
/**
* Creates a describe block as `$describeDescription` → `$testDescription` format.
*
* @param array<int, Description> $describeDescriptions
* @param array<int, string> $describeDescriptions
*/
public static function describe(array $describeDescriptions, string $testDescription): string
{
@@ -117,14 +120,4 @@ final class Str
{
return (bool) filter_var($value, FILTER_VALIDATE_URL);
}
/**
* Converts the given `$target` to a URL-friendly "slug".
*/
public static function slugify(string $target): string
{
$target = preg_replace('/[^a-zA-Z0-9]+/', '-', $target);
return strtolower(trim((string) $target, '-'));
}
}
+2 -3
View File
@@ -78,7 +78,6 @@ final class TestSuite
$this->afterAll = new AfterAllRepository;
$this->rootPath = (string) realpath($rootPath);
$this->snapshots = new SnapshotRepository(
$this->rootPath,
implode(DIRECTORY_SEPARATOR, [$this->rootPath, $this->testPath]),
implode(DIRECTORY_SEPARATOR, ['.pest', 'snapshots']),
);
@@ -102,7 +101,7 @@ final class TestSuite
}
if (! self::$instance instanceof self) {
throw new InvalidPestCommand;
Panic::with(new InvalidPestCommand);
}
return self::$instance;
@@ -120,7 +119,7 @@ final class TestSuite
assert($this->test instanceof TestCase);
$description = str_replace('__pest_evaluable_', '', $this->test->name());
$datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($this->test->dataSetAsString()));
$datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($this->test->dataSetAsStringWithData()));
return str_replace(' ', '_', $description.$datasetAsString);
}
@@ -1,35 +0,0 @@
<?php
use Pest\TestSuite;
beforeEach(function () {
$this->snapshotable = <<<'HTML'
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Snapshot</h1>
</div>
</div>
</div>
HTML;
});
test('pass with dataset', function ($data) {
TestSuite::getInstance()->snapshots->save($this->snapshotable);
[$filename] = TestSuite::getInstance()->snapshots->get();
expect($filename)->toStartWith('tests/.pest/snapshots-external/')
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot();
})->with(['my-datas-set-value']);
describe('within describe', function () {
test('pass with dataset', function ($data) {
TestSuite::getInstance()->snapshots->save($this->snapshotable);
[$filename] = TestSuite::getInstance()->snapshots->get();
expect($filename)->toStartWith('tests/.pest/snapshots-external/')
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot();
});
})->with(['my-datas-set-value']);
@@ -1,7 +0,0 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Snapshot</h1>
</div>
</div>
</div>
@@ -1,7 +0,0 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Snapshot</h1>
</div>
</div>
</div>
@@ -1,7 +0,0 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Snapshot</h1>
</div>
</div>
</div>

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