Merge branch '4.x' into 3.x

This commit is contained in:
nuno maduro
2025-07-26 03:56:09 +01:00
committed by GitHub
84 changed files with 1502 additions and 437 deletions

View File

@ -24,15 +24,19 @@ jobs:
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: 8.2 php-version: 8.3
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
extensions: sockets
- name: Install Dependencies - name: Install Dependencies
run: composer update --prefer-stable --no-interaction --no-progress --ansi run: composer update --prefer-stable --no-interaction --no-progress --ansi
# - name: Type Check - name: Profanity Check
# run: composer test:type:check run: composer test:profanity
- name: Type Check
run: composer test:type:check
- name: Type Coverage - name: Type Coverage
run: composer test:type:coverage run: composer test:type:coverage

View File

@ -13,9 +13,9 @@ jobs:
fail-fast: true fail-fast: true
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
symfony: ['7.1'] symfony: ['7.3']
php: ['8.2', '8.3', '8.4'] php: ['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 }} name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
@ -29,6 +29,7 @@ jobs:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
extensions: sockets
- name: Setup Problem Matches - name: Setup Problem Matches
run: | run: |

View File

@ -15,8 +15,13 @@
**Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP. **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)** - Explore our docs at **[pestphp.com »](https://pestphp.com)**
- Follow us on Twitter at **[@pestphp »](https://twitter.com/pestphp)** - Follow the creator Nuno Maduro:
- Join us at **[discord.gg/kaHY6p54JH »](https://discord.gg/kaHY6p54JH)** or **[t.me/+kYH5G4d5MV83ODk0 »](https://t.me/+kYH5G4d5MV83ODk0)** - YouTube: **[youtube.com/@nunomaduro](https://www.youtube.com/@nunomaduro)** — Videos every weekday
- Twitch: **[twitch.tv/enunomaduro](https://www.twitch.tv/enunomaduro)** — Streams (almost) every weekday
- Twitter / X: **[x.com/enunomaduro](https://x.com/enunomaduro)**
- LinkedIn: **[linkedin.com/in/nunomaduro](https://www.linkedin.com/in/nunomaduro)**
- Instagram: **[instagram.com/enunomaduro](https://www.instagram.com/enunomaduro)**
- Tiktok: **[tiktok.com/@enunomaduro](https://www.tiktok.com/@enunomaduro)**
## Sponsors ## Sponsors
@ -29,17 +34,16 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
### Gold Sponsors ### Gold Sponsors
- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
- **[LaraJobs](https://larajobs.com/?ref=pestphp)**
- **[Brokerchooser](https://brokerchooser.com/?ref=pestphp)** - **[Brokerchooser](https://brokerchooser.com/?ref=pestphp)**
- **[Forge](https://forge.laravel.com/?ref=pestphp)** - **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
- **[NativePHP](https://nativephp.com/mobile?ref=pestphp.com)**
### Premium Sponsors ### Premium Sponsors
- [Akaunting](https://akaunting.com/?ref=pestphp) - [Akaunting](https://akaunting.com/?ref=pestphp)
- [Codecourse](https://codecourse.com/?ref=pestphp)
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp) - [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
- [Localazy](https://localazy.com/?ref=pestphp) - [Localazy](https://localazy.com/?ref=pestphp)
- [Forge](https://forge.laravel.com/?ref=pestphp)
- [Route4Me](https://www.route4me.com/?ref=pestphp) - [Route4Me](https://www.route4me.com/?ref=pestphp)
- [Spatie](https://spatie.be/?ref=pestphp) - [Spatie](https://spatie.be/?ref=pestphp)
- [Worksome](https://www.worksome.com/?ref=pestphp) - [Worksome](https://www.worksome.com/?ref=pestphp)

View File

@ -17,19 +17,21 @@
} }
], ],
"require": { "require": {
"php": "^8.2.0", "php": "^8.3.0",
"brianium/paratest": "^7.7.0", "brianium/paratest": "^7.11.0",
"nunomaduro/collision": "^8.6.1", "nunomaduro/collision": "^8.8.2",
"nunomaduro/termwind": "^2.3.0", "nunomaduro/termwind": "^2.3.1",
"pestphp/pest-plugin": "^3.0.0", "pestphp/pest-plugin": "^4.0.0",
"pestphp/pest-plugin-arch": "^3.0.0", "pestphp/pest-plugin-arch": "^4.0.0",
"pestphp/pest-plugin-mutate": "^3.0.5", "pestphp/pest-plugin-mutate": "^4.0.0",
"phpunit/phpunit": "^11.5.6" "pestphp/pest-plugin-profanity": "^4.0.0",
"phpunit/phpunit": "^12.2.7",
"symfony/process": "^7.3.0"
}, },
"conflict": { "conflict": {
"filp/whoops": "<2.16.0", "filp/whoops": "<2.18.3",
"phpunit/phpunit": ">11.5.6", "phpunit/phpunit": ">12.2.7",
"sebastian/exporter": "<6.0.0", "sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0" "webmozart/assert": "<1.11.0"
}, },
"autoload": { "autoload": {
@ -53,9 +55,10 @@
] ]
}, },
"require-dev": { "require-dev": {
"pestphp/pest-dev-tools": "^3.3.0", "pestphp/pest-dev-tools": "^4.0.0",
"pestphp/pest-plugin-type-coverage": "^3.2.3", "pestphp/pest-plugin-browser": "^4.0.0",
"symfony/process": "^7.2.0" "pestphp/pest-plugin-type-coverage": "^4.0.0",
"psy/psysh": "^0.12.9"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,
@ -71,16 +74,17 @@
], ],
"scripts": { "scripts": {
"refacto": "rector", "refacto": "rector",
"lint": "pint", "lint": "pint --parallel",
"test:refacto": "rector --dry-run", "test:refacto": "rector --dry-run",
"test:lint": "pint --test", "test:lint": "pint --parallel --test",
"test:profanity": "php bin/pest --profanity --compact",
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug", "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: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:unit": "php bin/pest --exclude-group=integration --compact",
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml", "test:inline": "php bin/pest --configuration=phpunit.inline.xml",
"test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3", "test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3",
"test:integration": "php bin/pest --colors=always --group=integration -v", "test:integration": "php bin/pest --group=integration -v",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots",
"test": [ "test": [
"@test:refacto", "@test:refacto",
"@test:lint", "@test:lint",
@ -111,6 +115,7 @@
"Pest\\Plugins\\Snapshot", "Pest\\Plugins\\Snapshot",
"Pest\\Plugins\\Verbose", "Pest\\Plugins\\Verbose",
"Pest\\Plugins\\Version", "Pest\\Plugins\\Version",
"Pest\\Plugins\\Shard",
"Pest\\Plugins\\Parallel" "Pest\\Plugins\\Parallel"
] ]
}, },

View File

@ -52,6 +52,8 @@ use PHPUnit\Util\Filter;
use PHPUnit\Util\ThrowableToStringMapper; 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 * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
final readonly class ThrowableBuilder final readonly class ThrowableBuilder
@ -82,7 +84,7 @@ final readonly class ThrowableBuilder
$t->getMessage(), $t->getMessage(),
ThrowableToStringMapper::map($t), ThrowableToStringMapper::map($t),
$trace, $trace,
$previous $previous,
); );
} }
} }

View File

@ -27,6 +27,7 @@ use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\PreparationStarted; use PHPUnit\Event\Test\PreparationStarted;
use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\PrintedUnexpectedOutput;
use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestSuite\Started; use PHPUnit\Event\TestSuite\Started;
use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\Event\UnknownSubscriberTypeException;
@ -41,6 +42,8 @@ use function str_replace;
use function trim; use function trim;
/** /**
* @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 * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
final class JunitXmlLogger final class JunitXmlLogger
@ -59,32 +62,32 @@ final class JunitXmlLogger
private array $testSuites = []; private array $testSuites = [];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteTests = [0]; private array $testSuiteTests = [0];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteAssertions = [0]; private array $testSuiteAssertions = [0];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteErrors = [0]; private array $testSuiteErrors = [0];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteFailures = [0]; private array $testSuiteFailures = [0];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteSkipped = [0]; private array $testSuiteSkipped = [0];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteTimes = [0]; private array $testSuiteTimes = [0];
@ -113,7 +116,7 @@ final class JunitXmlLogger
public function flush(): void public function flush(): void
{ {
$this->printer->print($this->document->saveXML()); $this->printer->print($this->document->saveXML() ?: '');
$this->printer->flush(); $this->printer->flush();
} }
@ -195,28 +198,34 @@ final class JunitXmlLogger
$this->createTestCase($event); $this->createTestCase($event);
} }
/**
* @throws InvalidArgumentException
*/
public function testPreparationFailed(): void public function testPreparationFailed(): void
{ {
$this->preparationFailed = true; $this->preparationFailed = true;
} }
/**
* @throws InvalidArgumentException
*/
public function testPrepared(): void public function testPrepared(): void
{ {
$this->prepared = true; $this->prepared = true;
} }
public function testPrintedUnexpectedOutput(PrintedUnexpectedOutput $event): void
{
assert($this->currentTestCase !== null);
$systemOut = $this->document->createElement(
'system-out',
Xml::prepareString($event->output()),
);
$this->currentTestCase->appendChild($systemOut);
}
/** /**
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function testFinished(Finished $event): void public function testFinished(Finished $event): void
{ {
if ($this->preparationFailed) { if (! $this->prepared || $this->preparationFailed) {
return; return;
} }
@ -305,9 +314,11 @@ final class JunitXmlLogger
new TestPreparationStartedSubscriber($this), new TestPreparationStartedSubscriber($this),
new TestPreparationFailedSubscriber($this), new TestPreparationFailedSubscriber($this),
new TestPreparedSubscriber($this), new TestPreparedSubscriber($this),
new TestPrintedUnexpectedOutputSubscriber($this),
new TestFinishedSubscriber($this), new TestFinishedSubscriber($this),
new TestErroredSubscriber($this), new TestErroredSubscriber($this),
new TestFailedSubscriber($this), new TestFailedSubscriber($this),
new TestMarkedIncompleteSubscriber($this),
new TestSkippedSubscriber($this), new TestSkippedSubscriber($this),
new TestRunnerExecutionFinishedSubscriber($this), new TestRunnerExecutionFinishedSubscriber($this),
); );
@ -431,7 +442,7 @@ final class JunitXmlLogger
/** /**
* @throws InvalidArgumentException * @throws InvalidArgumentException
* *
* @psalm-assert !null $this->currentTestCase * @phpstan-assert !null $this->currentTestCase
*/ */
private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void
{ {

98
overrides/Report/PHP.php Normal file
View File

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

View File

@ -46,9 +46,10 @@ declare(strict_types=1);
namespace PHPUnit\Runner\ResultCache; namespace PHPUnit\Runner\ResultCache;
use const DIRECTORY_SEPARATOR; use const DIRECTORY_SEPARATOR;
use const LOCK_EX;
use PHPUnit\Framework\TestStatus\TestStatus; use PHPUnit\Framework\TestStatus\TestStatus;
use PHPUnit\Runner\DirectoryCannotBeCreatedException; use PHPUnit\Runner\DirectoryDoesNotExistException;
use PHPUnit\Runner\Exception; use PHPUnit\Runner\Exception;
use PHPUnit\Util\Filesystem; use PHPUnit\Util\Filesystem;
@ -65,24 +66,23 @@ use function json_encode;
use function Pest\version; use function Pest\version;
/** /**
* @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 * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
final class DefaultResultCache implements ResultCache 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; private readonly string $cacheFilename;
/** /**
* @psalm-var array<string, TestStatus> * @var array<string, TestStatus>
*/ */
private array $defects = []; private array $defects = [];
/** /**
* @psalm-var array<string, float> * @var array<string, float>
*/ */
private array $times = []; private array $times = [];
@ -95,28 +95,39 @@ final class DefaultResultCache implements ResultCache
$this->cacheFilename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_RESULT_CACHE_FILENAME; $this->cacheFilename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_RESULT_CACHE_FILENAME;
} }
public function setStatus(string $id, TestStatus $status): void public function setStatus(ResultCacheId $id, TestStatus $status): void
{ {
if ($status->isSuccess()) { if ($status->isSuccess()) {
return; return;
} }
$this->defects[$id] = $status; $this->defects[$id->asString()] = $status;
} }
public function status(string $id): TestStatus public function status(ResultCacheId $id): TestStatus
{ {
return $this->defects[$id] ?? TestStatus::unknown(); return $this->defects[$id->asString()] ?? TestStatus::unknown();
} }
public function setTime(string $id, float $time): void public function setTime(ResultCacheId $id, float $time): void
{ {
$this->times[$id] = $time; $this->times[$id->asString()] = $time;
} }
public function time(string $id): float public function time(ResultCacheId $id): float
{ {
return $this->times[$id] ?? 0.0; return $this->times[$id->asString()] ?? 0.0;
}
public function mergeWith(self $other): void
{
foreach ($other->defects as $id => $defect) {
$this->defects[$id] = $defect;
}
foreach ($other->times as $id => $time) {
$this->times[$id] = $time;
}
} }
public function load(): void public function load(): void
@ -165,7 +176,7 @@ final class DefaultResultCache implements ResultCache
public function persist(): void public function persist(): void
{ {
if (! Filesystem::createDirectory(dirname($this->cacheFilename))) { if (! Filesystem::createDirectory(dirname($this->cacheFilename))) {
throw new DirectoryCannotBeCreatedException($this->cacheFilename); throw new DirectoryDoesNotExistException(dirname($this->cacheFilename));
} }
$data = [ $data = [

View File

@ -45,6 +45,7 @@ declare(strict_types=1);
namespace PHPUnit\TextUI; namespace PHPUnit\TextUI;
use Pest\Plugins\Only; use Pest\Plugins\Only;
use Pest\Runner\Filter\EnsureTestCaseIsInitiatedFilter;
use PHPUnit\Event; use PHPUnit\Event;
use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\Filter\Factory; use PHPUnit\Runner\Filter\Factory;
@ -66,6 +67,12 @@ final readonly class TestSuiteFilterProcessor
{ {
$factory = new Factory; $factory = new Factory;
// @phpstan-ignore-next-line
(fn () => $this->filters[] = [
'className' => EnsureTestCaseIsInitiatedFilter::class,
'argument' => '',
])->call($factory);
if (! $configuration->hasFilter() && if (! $configuration->hasFilter() &&
! $configuration->hasGroups() && ! $configuration->hasGroups() &&
! $configuration->hasExcludeGroups() && ! $configuration->hasExcludeGroups() &&
@ -73,6 +80,8 @@ final readonly class TestSuiteFilterProcessor
! $configuration->hasTestsCovering() && ! $configuration->hasTestsCovering() &&
! $configuration->hasTestsUsing() && ! $configuration->hasTestsUsing() &&
! Only::isEnabled()) { ! Only::isEnabled()) {
$suite->injectFilter($factory);
return; return;
} }

199
phpstan-baseline.neon Normal file
View File

@ -0,0 +1,199 @@
parameters:
ignoreErrors:
-
message: '#^Parameter \#1 of callable callable\(Pest\\Expectation\<string\|null\>\)\: Pest\\Arch\\Contracts\\ArchExpectation expects Pest\\Expectation\<string\|null\>, Pest\\Expectation\<string\|null\> given\.$#'
identifier: argument.type
count: 1
path: src/ArchPresets/AbstractPreset.php
-
message: '#^Trait Pest\\Concerns\\Expectable is used zero times and is not analysed\.$#'
identifier: trait.unused
count: 1
path: src/Concerns/Expectable.php
-
message: '#^Trait Pest\\Concerns\\Logging\\WritesToConsole is used zero times and is not analysed\.$#'
identifier: trait.unused
count: 1
path: src/Concerns/Logging/WritesToConsole.php
-
message: '#^Trait Pest\\Concerns\\Testable is used zero times and is not analysed\.$#'
identifier: trait.unused
count: 1
path: src/Concerns/Testable.php
-
message: '#^Loose comparison using \!\= between \(Closure\|null\) and false will always evaluate to false\.$#'
identifier: notEqual.alwaysFalse
count: 1
path: src/Expectation.php
-
message: '#^Method Pest\\Expectation\:\:and\(\) should return Pest\\Expectation\<TAndValue\> but returns \(Pest\\Expectation&TAndValue\)\|Pest\\Expectation\<TAndValue of mixed\>\.$#'
identifier: return.type
count: 1
path: src/Expectation.php
-
message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$each contains generic class Pest\\Expectations\\EachExpectation but does not specify its types\: TValue$#'
identifier: missingType.generics
count: 1
path: src/Expectation.php
-
message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$not contains generic class Pest\\Expectations\\OppositeExpectation but does not specify its types\: TValue$#'
identifier: missingType.generics
count: 1
path: src/Expectation.php
-
message: '#^Parameter \#2 \$newScope of method Closure\:\:bindTo\(\) expects ''static''\|class\-string\|object\|null, string given\.$#'
identifier: argument.type
count: 1
path: src/Expectation.php
-
message: '#^Function expect\(\) should return Pest\\Expectation\<TValue\|null\> but returns Pest\\Expectation\<TValue\|null\>\.$#'
identifier: return.type
count: 1
path: src/Functions.php
-
message: '#^Parameter \#1 \$argv of method PHPUnit\\TextUI\\Application\:\:run\(\) expects list\<string\>, array\<int, string\> given\.$#'
identifier: argument.type
count: 1
path: src/Kernel.php
-
message: '#^Call to an undefined method object&TValue of mixed\:\:__toString\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Mixins/Expectation.php
-
message: '#^Call to an undefined method object&TValue of mixed\:\:toArray\(\)\.$#'
identifier: method.notFound
count: 4
path: src/Mixins/Expectation.php
-
message: '#^Call to an undefined method object&TValue of mixed\:\:toSnapshot\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Mixins/Expectation.php
-
message: '#^Call to an undefined method object&TValue of mixed\:\:toString\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Mixins/Expectation.php
-
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#'
identifier: staticMethod.alreadyNarrowedType
count: 2
path: src/Mixins/Expectation.php
-
message: '#^PHPDoc tag @var with type callable\(\)\: bool is not subtype of native type Closure\|null\.$#'
identifier: varTag.nativeType
count: 1
path: src/PendingCalls/TestCall.php
-
message: '#^Parameter \#1 \$argv of class Symfony\\Component\\Console\\Input\\ArgvInput constructor expects list\<string\>\|null, array\<int, string\> given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel.php
-
message: '#^Parameter \#13 \$testRunnerTriggeredDeprecationEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestRunner\\DeprecationTriggered\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#14 \$testRunnerTriggeredWarningEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestRunner\\WarningTriggered\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#15 \$errors of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#16 \$deprecations of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#17 \$notices of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#18 \$warnings of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#19 \$phpDeprecations of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#20 \$phpNotices of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#21 \$phpWarnings of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#4 \$testErroredEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\AfterLastTestMethodErrored\|PHPUnit\\Event\\Test\\BeforeFirstTestMethodErrored\|PHPUnit\\Event\\Test\\Errored\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#5 \$testFailedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\Failed\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#7 \$testSuiteSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestSuite\\Skipped\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#8 \$testSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\Skipped\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#9 \$testMarkedIncompleteEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\MarkedIncomplete\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Property Pest\\Plugins\\Parallel\\Paratest\\WrapperRunner\:\:\$pending \(list\<non\-empty\-string\>\) does not accept array\<int, non\-empty\-string\>\.$#'
identifier: assign.propertyType
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php

View File

@ -1,14 +1,12 @@
includes: includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon - phpstan-baseline.neon
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
parameters: parameters:
level: max level: 7
paths: paths:
- src - src
checkMissingIterableValueType: true reportUnmatchedIgnoredErrors: false
reportUnmatchedIgnoredErrors: true
ignoreErrors: ignoreErrors:
- "#type mixed is not subtype of native#" - "#type mixed is not subtype of native#"

View File

@ -0,0 +1,22 @@
<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>

View File

@ -17,7 +17,7 @@ final class BootExcludeList implements Bootstrapper
* *
* @var array<int, non-empty-string> * @var array<int, non-empty-string>
*/ */
private const EXCLUDE_LIST = [ private const array EXCLUDE_LIST = [
'bin', 'bin',
'overrides', 'overrides',
'resources', 'resources',

View File

@ -24,7 +24,7 @@ final class BootFiles implements Bootstrapper
* *
* @var array<int, string> * @var array<int, string>
*/ */
private const STRUCTURE = [ private const array STRUCTURE = [
'Expectations', 'Expectations',
'Expectations.php', 'Expectations.php',
'Helpers', 'Helpers',

View File

@ -15,17 +15,18 @@ final class BootOverrides implements Bootstrapper
/** /**
* The list of files to be overridden. * The list of files to be overridden.
* *
* @var array<string, string> * @var array<int, string>
*/ */
public const FILES = [ public const array FILES = [
'53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php', 'Runner/Filter/NameFilterIterator.php',
'77ffb7647b583bd82e37962c6fbdc4b04d3344d8a2c1ed103e625ed1ff7cb5c2' => 'Runner/ResultCache/DefaultResultCache.php', 'Runner/ResultCache/DefaultResultCache.php',
'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php', 'Runner/TestSuiteLoader.php',
'3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
'8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php', 'TextUI/TestSuiteFilterProcessor.php',
'8cfcb4999af79463eca51a42058e502ea4ddc776cba5677bf2f8eb6093e21a5c' => 'Event/Value/ThrowableBuilder.php', 'Event/Value/ThrowableBuilder.php',
'86cd9bcaa53cdd59c5b13e58f30064a015c549501e7629d93b96893d4dee1eb1' => 'Logging/JUnit/JunitXmlLogger.php', 'Logging/JUnit/JunitXmlLogger.php',
'Report/PHP.php',
]; ];
/** /**

View File

@ -20,7 +20,7 @@ final readonly class BootSubscribers implements Bootstrapper
* *
* @var array<int, class-string<Subscriber>> * @var array<int, class-string<Subscriber>>
*/ */
private const SUBSCRIBERS = [ private const array SUBSCRIBERS = [
Subscribers\EnsureConfigurationIsAvailable::class, Subscribers\EnsureConfigurationIsAvailable::class,
Subscribers\EnsureIgnorableTestCasesAreIgnored::class, Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
Subscribers\EnsureKernelDumpIsFlushed::class, Subscribers\EnsureKernelDumpIsFlushed::class,

View File

@ -6,10 +6,12 @@ namespace Pest\Concerns;
use Closure; use Closure;
use Pest\Exceptions\DatasetArgumentsMismatch; use Pest\Exceptions\DatasetArgumentsMismatch;
use Pest\Panic;
use Pest\Preset; use Pest\Preset;
use Pest\Support\ChainableClosure; use Pest\Support\ChainableClosure;
use Pest\Support\ExceptionTrace; use Pest\Support\ExceptionTrace;
use Pest\Support\Reflection; use Pest\Support\Reflection;
use Pest\Support\Shell;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\Attributes\PostCondition; use PHPUnit\Framework\Attributes\PostCondition;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -101,27 +103,6 @@ trait Testable
*/ */
private array $__snapshotChanges = []; 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. * Resets the test case static properties.
*/ */
@ -214,7 +195,11 @@ trait Testable
$beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll); $beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll);
} }
call_user_func(Closure::bind($beforeAll, null, self::class)); try {
call_user_func(Closure::bind($beforeAll, null, self::class));
} catch (Throwable $e) {
Panic::with($e);
}
} }
/** /**
@ -242,8 +227,6 @@ trait Testable
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$method->setUp($this);
$description = $method->description; $description = $method->description;
if ($this->dataName()) { if ($this->dataName()) {
$description = str_contains((string) $description, ':dataset') $description = str_contains((string) $description, ':dataset')
@ -285,6 +268,33 @@ trait Testable
$this->__callClosure($beforeEach, $arguments); $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. * Gets executed after the Test Case.
*/ */
@ -434,15 +444,7 @@ trait Testable
return; return;
} }
if (count($this->__snapshotChanges) === 1) { $this->markTestIncomplete(implode('. ', $this->__snapshotChanges));
$this->markTestIncomplete($this->__snapshotChanges[0]);
return;
}
$messages = implode(PHP_EOL, array_map(static fn (string $message): string => '- $message', $this->__snapshotChanges));
$this->markTestIncomplete($messages);
} }
/** /**
@ -466,7 +468,7 @@ trait Testable
*/ */
public static function getLatestPrintableTestCaseMethodName(): string public static function getLatestPrintableTestCaseMethodName(): string
{ {
return self::$__latestDescription; return self::$__latestDescription ?? '';
} }
/** /**
@ -481,4 +483,12 @@ trait Testable
'notes' => self::$__latestNotes, 'notes' => self::$__latestNotes,
]; ];
} }
/**
* Opens a shell for the test case.
*/
public function shell(): void
{
Shell::open();
}
} }

View File

@ -102,6 +102,14 @@ final readonly class Configuration
return Configuration\Project::getInstance(); return Configuration\Project::getInstance();
} }
/**
* Gets the browser configuration.
*/
public function browser(): Browser\Configuration
{
return new Browser\Configuration;
}
/** /**
* Proxies calls to the uses method. * Proxies calls to the uses method.
* *

View File

@ -16,7 +16,7 @@ final readonly class Help
* *
* @var array<int, string> * @var array<int, string>
*/ */
private const HELP_MESSAGES = [ private const array HELP_MESSAGES = [
'<comment>Pest Options:</comment>', '<comment>Pest Options:</comment>',
' <info>--init</info> Initialise a standard Pest configuration', ' <info>--init</info> Initialise a standard Pest configuration',
' <info>--coverage</info> Enable coverage and output to standard output', ' <info>--coverage</info> Enable coverage and output to standard output',

View File

@ -22,10 +22,14 @@ final readonly class Thanks
* *
* @var array<string, string> * @var array<string, string>
*/ */
private const FUNDING_MESSAGES = [ private const array FUNDING_MESSAGES = [
'Star' => 'https://github.com/pestphp/pest', 'Star' => 'https://github.com/pestphp/pest',
'News' => 'https://twitter.com/pestphp', 'YouTube' => 'https://youtube.com/@nunomaduro',
'Videos' => 'https://youtube.com/@nunomaduro', 'TikTok' => 'https://tiktok.com/@nunomaduro',
'Twitch' => 'https://twitch.tv/enunomaduro',
'LinkedIn' => 'https://linkedin.com/in/nunomaduro',
'Instagram' => 'https://instagram.com/enunomaduro',
'X' => 'https://x.com/enunomaduro',
'Sponsor' => 'https://github.com/sponsors/nunomaduro', 'Sponsor' => 'https://github.com/sponsors/nunomaduro',
]; ];

View File

@ -535,7 +535,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && $object->reflectionClass->isFinal(), fn (ObjectDescription $object): bool => ! enum_exists($object->name) && isset($object->reflectionClass) && $object->reflectionClass->isFinal(),
'to be final', 'to be final',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -548,7 +548,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line fn (ObjectDescription $object): bool => ! enum_exists($object->name) && isset($object->reflectionClass) && $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line
'to be readonly', 'to be readonly',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -561,7 +561,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => $object->reflectionClass->isTrait(), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isTrait(),
'to be trait', 'to be trait',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -582,7 +582,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => $object->reflectionClass->isAbstract(), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isAbstract(),
'to be abstract', 'to be abstract',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -599,7 +599,7 @@ final class Expectation
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => $object->reflectionClass->hasMethod($method))) === count($methods), fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => isset($object->reflectionClass) && $object->reflectionClass->hasMethod($method))) === count($methods),
sprintf("to have method '%s'", implode("', '", $methods)), sprintf("to have method '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -670,7 +670,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum(), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isEnum(),
'to be enum', 'to be enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -712,7 +712,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => $object->reflectionClass->isInterface(), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isInterface(),
'to be interface', 'to be interface',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -733,7 +733,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => $class === $object->reflectionClass->getName() || $object->reflectionClass->isSubclassOf($class), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && ($class === $object->reflectionClass->getName() || $object->reflectionClass->isSubclassOf($class)),
sprintf("to extend '%s'", $class), sprintf("to extend '%s'", $class),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -773,6 +773,10 @@ final class Expectation
$this, $this,
function (ObjectDescription $object) use ($traits): bool { function (ObjectDescription $object) use ($traits): bool {
foreach ($traits as $trait) { foreach ($traits as $trait) {
if (isset($object->reflectionClass) === false) {
return false;
}
if (! in_array($trait, $object->reflectionClass->getTraitNames(), true)) { if (! in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
return false; return false;
} }
@ -792,7 +796,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() === [], fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getInterfaceNames() === [],
'to implement nothing', 'to implement nothing',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -809,7 +813,8 @@ final class Expectation
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => count($interfaces) === count($object->reflectionClass->getInterfaceNames()) fn (ObjectDescription $object): bool => isset($object->reflectionClass)
&& (count($interfaces) === count($object->reflectionClass->getInterfaceNames()))
&& array_diff($interfaces, $object->reflectionClass->getInterfaceNames()) === [], && array_diff($interfaces, $object->reflectionClass->getInterfaceNames()) === [],
"to only implement '".implode("', '", $interfaces)."'", "to only implement '".implode("', '", $interfaces)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
@ -823,7 +828,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => str_starts_with($object->reflectionClass->getShortName(), $prefix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_starts_with($object->reflectionClass->getShortName(), $prefix),
"to have prefix '{$prefix}'", "to have prefix '{$prefix}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -836,7 +841,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => str_ends_with($object->reflectionClass->getName(), $suffix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_ends_with($object->reflectionClass->getName(), $suffix),
"to have suffix '{$suffix}'", "to have suffix '{$suffix}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -855,7 +860,7 @@ final class Expectation
$this, $this,
function (ObjectDescription $object) use ($interfaces): bool { function (ObjectDescription $object) use ($interfaces): bool {
foreach ($interfaces as $interface) { foreach ($interfaces as $interface) {
if (! $object->reflectionClass->implementsInterface($interface)) { if (! isset($object->reflectionClass) || ! $object->reflectionClass->implementsInterface($interface)) {
return false; return false;
} }
} }
@ -928,7 +933,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => $object->reflectionClass->hasMethod('__invoke'), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->hasMethod('__invoke'),
'to be invokable', 'to be invokable',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
); );
@ -1037,7 +1042,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) !== [], fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getAttributes($attribute) !== [],
"to have attribute '{$attribute}'", "to have attribute '{$attribute}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -1066,7 +1071,8 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum() fn (ObjectDescription $object): bool => isset($object->reflectionClass)
&& $object->reflectionClass->isEnum()
&& (new ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line && (new ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
&& (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line && (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line
'to be '.$backingType.' backed enum', 'to be '.$backingType.' backed enum',

View File

@ -74,7 +74,10 @@ final readonly class OppositeExpectation
*/ */
public function toUse(array|string $targets): ArchExpectation public function toUse(array|string $targets): ArchExpectation
{ {
return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): SingleArchExpectation => ToUse::make($this->original, $target)->opposite( /** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return GroupArchExpectation::fromExpectations($original, array_map(fn (string $target): SingleArchExpectation => ToUse::make($original, $target)->opposite(
fn () => $this->throwExpectationFailedException('toUse', $target), fn () => $this->throwExpectationFailedException('toUse', $target),
), is_string($targets) ? [$targets] : $targets)); ), is_string($targets) ? [$targets] : $targets));
} }
@ -84,8 +87,11 @@ final readonly class OppositeExpectation
*/ */
public function toHaveFileSystemPermissions(string $permissions): ArchExpectation public function toHaveFileSystemPermissions(string $permissions): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) !== $permissions, fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) !== $permissions,
sprintf('permissions not to be [%s]', $permissions), sprintf('permissions not to be [%s]', $permissions),
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')), FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
@ -105,8 +111,11 @@ final readonly class OppositeExpectation
*/ */
public function toHaveMethodsDocumented(): ArchExpectation public function toHaveMethodsDocumented(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| array_filter( || array_filter(
Reflection::getMethodsFromReflectionClass($object->reflectionClass), Reflection::getMethodsFromReflectionClass($object->reflectionClass),
@ -124,8 +133,11 @@ final readonly class OppositeExpectation
*/ */
public function toHavePropertiesDocumented(): ArchExpectation public function toHavePropertiesDocumented(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| array_filter( || array_filter(
Reflection::getPropertiesFromReflectionClass($object->reflectionClass), Reflection::getPropertiesFromReflectionClass($object->reflectionClass),
@ -144,8 +156,11 @@ final readonly class OppositeExpectation
*/ */
public function toUseStrictTypes(): ArchExpectation public function toUseStrictTypes(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! (bool) preg_match('/^<\?php\s+declare\(.*?strict_types\s?=\s?1.*?\);/', (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)),
'not to use strict types', 'not to use strict types',
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')), FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
@ -157,8 +172,11 @@ final readonly class OppositeExpectation
*/ */
public function toUseStrictEquality(): ArchExpectation public function toUseStrictEquality(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' === ') && ! str_contains((string) file_get_contents($object->path), ' !== '), fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' === ') && ! str_contains((string) file_get_contents($object->path), ' !== '),
'to use strict equality', 'to use strict equality',
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' === ') || str_contains($line, ' !== ')), FileLineFinder::where(fn (string $line): bool => str_contains($line, ' === ') || str_contains($line, ' !== ')),
@ -170,9 +188,12 @@ final readonly class OppositeExpectation
*/ */
public function toBeFinal(): ArchExpectation public function toBeFinal(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isFinal(), fn (ObjectDescription $object): bool => ! enum_exists($object->name) && (isset($object->reflectionClass) === false || ! $object->reflectionClass->isFinal()),
'not to be final', 'not to be final',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -183,9 +204,12 @@ final readonly class OppositeExpectation
*/ */
public function toBeReadonly(): ArchExpectation public function toBeReadonly(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line fn (ObjectDescription $object): bool => ! enum_exists($object->name) && (isset($object->reflectionClass) === false || ! $object->reflectionClass->isReadOnly()) && assert(true), // @phpstan-ignore-line
'not to be readonly', 'not to be readonly',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -196,9 +220,12 @@ final readonly class OppositeExpectation
*/ */
public function toBeTrait(): ArchExpectation public function toBeTrait(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isTrait(), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isTrait(),
'not to be trait', 'not to be trait',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -217,9 +244,12 @@ final readonly class OppositeExpectation
*/ */
public function toBeAbstract(): ArchExpectation public function toBeAbstract(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isAbstract(), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isAbstract(),
'not to be abstract', 'not to be abstract',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -234,11 +264,14 @@ final readonly class OppositeExpectation
{ {
$methods = is_array($method) ? $method : [$method]; $methods = is_array($method) ? $method : [$method];
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => array_filter( fn (ObjectDescription $object): bool => array_filter(
$methods, $methods,
fn (string $method): bool => $object->reflectionClass->hasMethod($method), fn (string $method): bool => isset($object->reflectionClass) === false || $object->reflectionClass->hasMethod($method),
) === [], ) === [],
'to not have methods: '.implode(', ', $methods), 'to not have methods: '.implode(', ', $methods),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
@ -266,8 +299,11 @@ final readonly class OppositeExpectation
$state = new stdClass; $state = new stdClass;
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
function (ObjectDescription $object) use ($methods, &$state): bool { function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass) $reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PUBLIC) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PUBLIC)
@ -309,8 +345,11 @@ final readonly class OppositeExpectation
$state = new stdClass; $state = new stdClass;
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
function (ObjectDescription $object) use ($methods, &$state): bool { function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass) $reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PROTECTED) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PROTECTED)
@ -352,8 +391,11 @@ final readonly class OppositeExpectation
$state = new stdClass; $state = new stdClass;
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
function (ObjectDescription $object) use ($methods, &$state): bool { function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass) $reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PRIVATE) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PRIVATE)
@ -389,9 +431,12 @@ final readonly class OppositeExpectation
*/ */
public function toBeEnum(): ArchExpectation public function toBeEnum(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum(), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isEnum(),
'not to be enum', 'not to be enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -410,8 +455,11 @@ final readonly class OppositeExpectation
*/ */
public function toBeClass(): ArchExpectation public function toBeClass(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! class_exists($object->name), fn (ObjectDescription $object): bool => ! class_exists($object->name),
'not to be class', 'not to be class',
FileLineFinder::where(fn (string $line): bool => true), FileLineFinder::where(fn (string $line): bool => true),
@ -431,9 +479,12 @@ final readonly class OppositeExpectation
*/ */
public function toBeInterface(): ArchExpectation public function toBeInterface(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isInterface(), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isInterface(),
'not to be interface', 'not to be interface',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -452,9 +503,12 @@ final readonly class OppositeExpectation
*/ */
public function toExtend(string $class): ArchExpectation public function toExtend(string $class): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isSubclassOf($class), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isSubclassOf($class),
sprintf("not to extend '%s'", $class), sprintf("not to extend '%s'", $class),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -465,9 +519,12 @@ final readonly class OppositeExpectation
*/ */
public function toExtendNothing(): ArchExpectation public function toExtendNothing(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() !== false, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getParentClass() !== false,
'to extend a class', 'to extend a class',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -490,11 +547,14 @@ final readonly class OppositeExpectation
{ {
$traits = is_array($traits) ? $traits : [$traits]; $traits = is_array($traits) ? $traits : [$traits];
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
function (ObjectDescription $object) use ($traits): bool { function (ObjectDescription $object) use ($traits): bool {
foreach ($traits as $trait) { foreach ($traits as $trait) {
if (in_array($trait, $object->reflectionClass->getTraitNames(), true)) { if (isset($object->reflectionClass) && in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
return false; return false;
} }
} }
@ -515,11 +575,14 @@ final readonly class OppositeExpectation
{ {
$interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; $interfaces = is_array($interfaces) ? $interfaces : [$interfaces];
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
function (ObjectDescription $object) use ($interfaces): bool { function (ObjectDescription $object) use ($interfaces): bool {
foreach ($interfaces as $interface) { foreach ($interfaces as $interface) {
if ($object->reflectionClass->implementsInterface($interface)) { if (isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface)) {
return false; return false;
} }
} }
@ -536,9 +599,12 @@ final readonly class OppositeExpectation
*/ */
public function toImplementNothing(): ArchExpectation public function toImplementNothing(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() !== [], fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getInterfaceNames() !== [],
'to implement an interface', 'to implement an interface',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -557,9 +623,12 @@ final readonly class OppositeExpectation
*/ */
public function toHavePrefix(string $prefix): ArchExpectation public function toHavePrefix(string $prefix): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! str_starts_with($object->reflectionClass->getShortName(), $prefix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_starts_with($object->reflectionClass->getShortName(), $prefix),
"not to have prefix '{$prefix}'", "not to have prefix '{$prefix}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -570,9 +639,12 @@ final readonly class OppositeExpectation
*/ */
public function toHaveSuffix(string $suffix): ArchExpectation public function toHaveSuffix(string $suffix): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! str_ends_with($object->reflectionClass->getName(), $suffix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_ends_with($object->reflectionClass->getName(), $suffix),
"not to have suffix '{$suffix}'", "not to have suffix '{$suffix}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@ -599,7 +671,10 @@ final readonly class OppositeExpectation
*/ */
public function toBeUsed(): ArchExpectation public function toBeUsed(): ArchExpectation
{ {
return ToBeUsedInNothing::make($this->original); /** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return ToBeUsedInNothing::make($original);
} }
/** /**
@ -609,7 +684,10 @@ final readonly class OppositeExpectation
*/ */
public function toBeUsedIn(array|string $targets): ArchExpectation public function toBeUsedIn(array|string $targets): ArchExpectation
{ {
return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): GroupArchExpectation => ToBeUsedIn::make($this->original, $target)->opposite( /** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return GroupArchExpectation::fromExpectations($original, array_map(fn (string $target): GroupArchExpectation => ToBeUsedIn::make($original, $target)->opposite(
fn () => $this->throwExpectationFailedException('toBeUsedIn', $target), fn () => $this->throwExpectationFailedException('toBeUsedIn', $target),
), is_string($targets) ? [$targets] : $targets)); ), is_string($targets) ? [$targets] : $targets));
} }
@ -632,9 +710,12 @@ final readonly class OppositeExpectation
*/ */
public function toBeInvokable(): ArchExpectation public function toBeInvokable(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod('__invoke'), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->hasMethod('__invoke'),
'to not be invokable', 'to not be invokable',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
); );
@ -645,9 +726,12 @@ final readonly class OppositeExpectation
*/ */
public function toHaveAttribute(string $attribute): ArchExpectation public function toHaveAttribute(string $attribute): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) === [], fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getAttributes($attribute) === [],
"to not have attribute '{$attribute}'", "to not have attribute '{$attribute}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
); );
@ -737,9 +821,13 @@ final readonly class OppositeExpectation
*/ */
private function toBeBackedEnum(string $backingType): ArchExpectation private function toBeBackedEnum(string $backingType): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */
$original = $this->original;
return Targeted::make( return Targeted::make(
$this->original, $original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum() fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| ! $object->reflectionClass->isEnum()
|| ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line || ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
|| (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line || (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line
'not to be '.$backingType.' backed enum', 'not to be '.$backingType.' backed enum',

View File

@ -155,7 +155,7 @@ final class TestCaseMethodFactory
assert($testCase instanceof TestCaseFactory); assert($testCase instanceof TestCaseFactory);
$method = $this; $method = $this;
return function (...$arguments) use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line return function (...$arguments) use ($testCase, $method, $closure): mixed {
/* @var TestCase $this */ /* @var TestCase $this */
$testCase->proxies->proxy($this); $testCase->proxies->proxy($this);
$method->proxies->proxy($this); $method->proxies->proxy($this);

View File

@ -2,11 +2,14 @@
declare(strict_types=1); declare(strict_types=1);
use Pest\Browser\Api\ArrayablePendingAwaitablePage;
use Pest\Browser\Api\PendingAwaitablePage;
use Pest\Concerns\Expectable; use Pest\Concerns\Expectable;
use Pest\Configuration; use Pest\Configuration;
use Pest\Exceptions\AfterAllWithinDescribe; use Pest\Exceptions\AfterAllWithinDescribe;
use Pest\Exceptions\BeforeAllWithinDescribe; use Pest\Exceptions\BeforeAllWithinDescribe;
use Pest\Expectation; use Pest\Expectation;
use Pest\Installers\PluginBrowser;
use Pest\Mutate\Contracts\MutationTestRunner; use Pest\Mutate\Contracts\MutationTestRunner;
use Pest\Mutate\Repositories\ConfigurationRepository; use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\AfterEachCall; use Pest\PendingCalls\AfterEachCall;
@ -278,3 +281,51 @@ 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);
}
}

View File

@ -0,0 +1,15 @@
<?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');
}
}

View File

@ -34,7 +34,7 @@ final readonly class Kernel
* *
* @var array<int, class-string> * @var array<int, class-string>
*/ */
private const BOOTSTRAPPERS = [ private const array BOOTSTRAPPERS = [
Bootstrappers\BootOverrides::class, Bootstrappers\BootOverrides::class,
Bootstrappers\BootSubscribers::class, Bootstrappers\BootSubscribers::class,
Bootstrappers\BootFiles::class, Bootstrappers\BootFiles::class,

View File

@ -31,7 +31,7 @@ final readonly class Converter
/** /**
* The prefix for the test suite name. * The prefix for the test suite name.
*/ */
private const PREFIX = 'P\\'; private const string PREFIX = 'P\\';
/** /**
* The state generator. * The state generator.

View File

@ -183,7 +183,6 @@ final class Expectation
{ {
foreach ($needles as $needle) { foreach ($needles as $needle) {
if (is_string($this->value)) { if (is_string($this->value)) {
// @phpstan-ignore-next-line
Assert::assertStringContainsString((string) $needle, $this->value); Assert::assertStringContainsString((string) $needle, $this->value);
} else { } else {
if (! is_iterable($this->value)) { if (! is_iterable($this->value)) {
@ -1159,4 +1158,21 @@ final class Expectation
return $this; 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;
}
} }

View File

@ -46,7 +46,7 @@ final readonly class Panic
{ {
try { try {
$output = Container::getInstance()->get(OutputInterface::class); $output = Container::getInstance()->get(OutputInterface::class);
} catch (Throwable) { // @phpstan-ignore-line } catch (Throwable) {
$output = new ConsoleOutput; $output = new ConsoleOutput;
} }

View File

@ -78,7 +78,7 @@ final class DescribeCall
$this->currentBeforeEachCall->describing[] = $this->description; $this->currentBeforeEachCall->describing[] = $this->description;
} }
$this->currentBeforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line $this->currentBeforeEachCall->{$name}(...$arguments);
return $this; return $this;
} }

View File

@ -12,6 +12,7 @@ use Pest\Factories\Attribute;
use Pest\Factories\TestCaseMethodFactory; use Pest\Factories\TestCaseMethodFactory;
use Pest\Mutate\Repositories\ConfigurationRepository; use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\Concerns\Describable; use Pest\PendingCalls\Concerns\Describable;
use Pest\Plugins\Environment;
use Pest\Plugins\Only; use Pest\Plugins\Only;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\Container; use Pest\Support\Container;
@ -178,10 +179,9 @@ final class TestCall // @phpstan-ignore-line
} }
/** /**
* Runs the current test multiple times with * Runs the current test multiple times with each item of the given `iterable`.
* each item of the given `iterable`.
* *
* @param array<\Closure|iterable<int|string, mixed>|string> $data * @param Closure|iterable<array-key, mixed>|string $data
*/ */
public function with(Closure|iterable|string ...$data): self public function with(Closure|iterable|string ...$data): self
{ {
@ -224,7 +224,7 @@ final class TestCall // @phpstan-ignore-line
*/ */
public function only(): self public function only(): self
{ {
Only::enable($this, ...func_get_args()); // @phpstan-ignore-line Only::enable($this, ...func_get_args());
return $this; return $this;
} }
@ -315,6 +315,61 @@ final class TestCall // @phpstan-ignore-line
: $this; : $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. * Skips the current test unless the given test is running on Windows.
*/ */
@ -616,6 +671,30 @@ final class TestCall // @phpstan-ignore-line
return $this; 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 references(string|array ...$classes): self
{
assert($classes !== []);
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, * Informs the test runner that no expectations happen in this test,
* and its purpose is simply to check whether the given code can * and its purpose is simply to check whether the given code can

View File

@ -6,7 +6,7 @@ namespace Pest;
function version(): string function version(): string
{ {
return '3.7.4'; return '4.0.0-alpha.6';
} }
function testDirectory(string $file = ''): string function testDirectory(string $file = ''): string

View File

@ -21,7 +21,7 @@ final class Cache implements HandlesArguments
/** /**
* The temporary folder. * The temporary folder.
*/ */
private const TEMPORARY_FOLDER = __DIR__ private const string TEMPORARY_FOLDER = __DIR__
.DIRECTORY_SEPARATOR .DIRECTORY_SEPARATOR
.'..' .'..'
.DIRECTORY_SEPARATOR .DIRECTORY_SEPARATOR

View File

@ -21,7 +21,7 @@ final class Configuration implements HandlesArguments, Terminable
/** /**
* The base PHPUnit file. * The base PHPUnit file.
*/ */
public const BASE_PHPUNIT_FILE = __DIR__ public const string BASE_PHPUNIT_FILE = __DIR__
.DIRECTORY_SEPARATOR .DIRECTORY_SEPARATOR
.'..' .'..'
.DIRECTORY_SEPARATOR .DIRECTORY_SEPARATOR
@ -34,7 +34,7 @@ final class Configuration implements HandlesArguments, Terminable
*/ */
public function handleArguments(array $arguments): array public function handleArguments(array $arguments): array
{ {
if ($this->hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) { if ($this->hasArgument('--configuration', $arguments) || $this->hasArgument('-c', $arguments) || $this->hasCustomConfigurationFile()) {
return $arguments; return $arguments;
} }

View File

@ -17,20 +17,11 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final class Coverage implements AddsOutput, HandlesArguments 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. * Whether it should show the coverage or not.

View File

@ -14,12 +14,12 @@ final class Environment implements HandlesArguments
/** /**
* The continuous integration environment. * The continuous integration environment.
*/ */
public const CI = 'ci'; public const string CI = 'ci';
/** /**
* The local environment. * The local environment.
*/ */
public const LOCAL = 'local'; public const string LOCAL = 'local';
/** /**
* The current environment. * The current environment.

View File

@ -20,12 +20,12 @@ final readonly class Init implements HandlesArguments
/** /**
* The option the triggers the init job. * The option the triggers the init job.
*/ */
private const INIT_OPTION = '--init'; private const string INIT_OPTION = '--init';
/** /**
* The files that will be created. * The files that will be created.
*/ */
private const STUBS = [ private const array STUBS = [
'phpunit.xml.stub' => 'phpunit.xml', 'phpunit.xml.stub' => 'phpunit.xml',
'Pest.php.stub' => 'tests/Pest.php', 'Pest.php.stub' => 'tests/Pest.php',
'TestCase.php.stub' => 'tests/TestCase.php', 'TestCase.php.stub' => 'tests/TestCase.php',
@ -119,6 +119,6 @@ final readonly class Init implements HandlesArguments
*/ */
private function isLaravelInstalled(): bool private function isLaravelInstalled(): bool
{ {
return InstalledVersions::isInstalled('laravel/laravel'); return InstalledVersions::isInstalled('laravel/framework');
} }
} }

View File

@ -5,7 +5,10 @@ declare(strict_types=1);
namespace Pest\Plugins; namespace Pest\Plugins;
use Pest\Contracts\Plugins\Terminable; use Pest\Contracts\Plugins\Terminable;
use Pest\Factories\Attribute;
use Pest\Factories\TestCaseMethodFactory;
use Pest\PendingCalls\TestCall; use Pest\PendingCalls\TestCall;
use PHPUnit\Framework\Attributes\Group;
/** /**
* @internal * @internal
@ -15,7 +18,7 @@ final class Only implements Terminable
/** /**
* The temporary folder. * The temporary folder.
*/ */
private const TEMPORARY_FOLDER = __DIR__ private const string TEMPORARY_FOLDER = __DIR__
.DIRECTORY_SEPARATOR .DIRECTORY_SEPARATOR
.'..' .'..'
.DIRECTORY_SEPARATOR .DIRECTORY_SEPARATOR
@ -23,28 +26,19 @@ final class Only implements Terminable
.DIRECTORY_SEPARATOR .DIRECTORY_SEPARATOR
.'.temp'; .'.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. * Creates the lock file.
*/ */
public static function enable(TestCall $testCall, string $group = '__pest_only'): void public static function enable(TestCall|TestCaseMethodFactory $testCall, string $group = '__pest_only'): void
{ {
$testCall->group($group); if ($testCall instanceof TestCall) {
$testCall->group($group);
} else {
$testCall->attributes[] = new Attribute(
Group::class,
[$group],
);
}
if (Environment::name() === Environment::CI || Parallel::isWorker()) { if (Environment::name() === Environment::CI || Parallel::isWorker()) {
return; return;
@ -88,4 +82,20 @@ final class Only implements Terminable
return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line 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);
}
}
} }

View File

@ -23,9 +23,9 @@ final class Parallel implements HandlesArguments
{ {
use HandleArguments; use HandleArguments;
private const GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_'; private const string GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_';
private const HANDLERS = [ private const array HANDLERS = [
Parallel\Handlers\Parallel::class, Parallel\Handlers\Parallel::class,
Parallel\Handlers\Pest::class, Parallel\Handlers\Pest::class,
Parallel\Handlers\Laravel::class, Parallel\Handlers\Laravel::class,
@ -34,7 +34,7 @@ final class Parallel implements HandlesArguments
/** /**
* @var string[] * @var string[]
*/ */
private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request']; private const array 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. * Whether the given command line arguments indicate that the test suite should be run in parallel.

View File

@ -18,7 +18,7 @@ final class Parallel implements HandlesArguments
/** /**
* The list of arguments to remove. * The list of arguments to remove.
*/ */
private const ARGS_TO_REMOVE = [ private const array ARGS_TO_REMOVE = [
'--parallel', '--parallel',
'-p', '-p',
'--no-output', '--no-output',

View File

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

View File

@ -59,10 +59,10 @@ final class ResultPrinter
private readonly OutputInterface $output, private readonly OutputInterface $output,
private readonly Options $options private readonly Options $options
) { ) {
$this->printer = new class($this->output) implements Printer $this->printer = new readonly class($this->output) implements Printer
{ {
public function __construct( public function __construct(
private readonly OutputInterface $output, private OutputInterface $output,
) {} ) {}
public function print(string $buffer): void public function print(string $buffer): void

View File

@ -17,6 +17,7 @@ use ParaTest\WrapperRunner\WrapperWorker;
use Pest\Result; use Pest\Result;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Event\Test\AfterLastTestMethodFailed;
use PHPUnit\Event\TestRunner\WarningTriggered; use PHPUnit\Event\TestRunner\WarningTriggered;
use PHPUnit\Runner\CodeCoverage; use PHPUnit\Runner\CodeCoverage;
use PHPUnit\Runner\ResultCache\DefaultResultCache; use PHPUnit\Runner\ResultCache\DefaultResultCache;
@ -50,7 +51,7 @@ final class WrapperRunner implements RunnerInterface
/** /**
* The time to sleep between cycles. * The time to sleep between cycles.
*/ */
private const CYCLE_SLEEP = 10000; private const int CYCLE_SLEEP = 10000;
/** /**
* The result printer. * The result printer.
@ -313,27 +314,42 @@ final class WrapperRunner implements RunnerInterface
$testResult = unserialize($contents); $testResult = unserialize($contents);
assert($testResult instanceof TestResult); assert($testResult instanceof TestResult);
/** @var list<AfterLastTestMethodFailed> $failedEvents */
$failedEvents = array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents());
$testResultSum = new TestResult( $testResultSum = new TestResult(
(int) $testResultSum->hasTests() + (int) $testResult->hasTests(), (int) $testResultSum->hasTests() + (int) $testResult->hasTests(),
$testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(), $testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
$testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(), $testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(),
array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()), array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()),
array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()), $failedEvents,
array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()), array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()),
array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()), array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()),
array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()), array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()),
array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()), array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()), 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()), array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->errors(), $testResult->errors()), array_merge_recursive($testResultSum->errors(), $testResult->errors()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()), array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->notices(), $testResult->notices()), array_merge_recursive($testResultSum->notices(), $testResult->notices()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->warnings(), $testResult->warnings()), array_merge_recursive($testResultSum->warnings(), $testResult->warnings()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()), array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()), array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()), array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()),
$testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(), $testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
); );
@ -351,8 +367,10 @@ final class WrapperRunner implements RunnerInterface
$testResultSum->testMarkedIncompleteEvents(), $testResultSum->testMarkedIncompleteEvents(),
$testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResultSum->testTriggeredPhpunitDeprecationEvents(),
$testResultSum->testTriggeredPhpunitErrorEvents(), $testResultSum->testTriggeredPhpunitErrorEvents(),
$testResultSum->testTriggeredPhpunitNoticeEvents(),
$testResultSum->testTriggeredPhpunitWarningEvents(), $testResultSum->testTriggeredPhpunitWarningEvents(),
$testResultSum->testRunnerTriggeredDeprecationEvents(), $testResultSum->testRunnerTriggeredDeprecationEvents(),
$testResultSum->testRunnerTriggeredNoticeEvents(),
array_values(array_filter( array_values(array_filter(
$testResultSum->testRunnerTriggeredWarningEvents(), $testResultSum->testRunnerTriggeredWarningEvents(),
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found') fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')

View File

@ -34,7 +34,7 @@ final class CompactPrinter
/** /**
* @var array<string, array<int, string>> * @var array<string, array<int, string>>
*/ */
private const LOOKUP_TABLE = [ private const array LOOKUP_TABLE = [
'.' => ['gray', '.'], '.' => ['gray', '.'],
'S' => ['yellow', 's'], 'S' => ['yellow', 's'],
'T' => ['cyan', 't'], 'T' => ['cyan', 't'],
@ -131,14 +131,14 @@ final class CompactPrinter
$status['collected'], $status['collected'],
$status['threshold'], $status['threshold'],
$status['roots'], $status['roots'],
null, 0.00,
null, 0.00,
null, 0.00,
null, 0.00,
null, false,
null, false,
null, false,
null, 0,
); );
$telemetry = new Info( $telemetry = new Info(

177
src/Plugins/Shard.php Normal file
View File

@ -0,0 +1,177 @@
<?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,
];
}
}

View File

@ -16,7 +16,7 @@ final class Verbose implements HandlesArguments
/** /**
* The list of verbosity levels. * The list of verbosity levels.
*/ */
private const VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q']; private const array VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
/** /**
* {@inheritDoc} * {@inheritDoc}

View File

@ -19,7 +19,7 @@ use function sprintf;
*/ */
final class DatasetsRepository final class DatasetsRepository
{ {
private const SEPARATOR = '>>'; private const string SEPARATOR = '>>';
/** /**
* Holds the datasets. * Holds the datasets.
@ -71,7 +71,7 @@ final class DatasetsRepository
* *
* @throws ShouldNotHappen * @throws ShouldNotHappen
*/ */
public static function get(string $filename, string $description): Closure|array public static function get(string $filename, string $description): Closure|array // @phpstan-ignore-line
{ {
$dataset = self::$withs[$filename.self::SEPARATOR.$description]; $dataset = self::$withs[$filename.self::SEPARATOR.$description];
@ -110,7 +110,6 @@ final class DatasetsRepository
foreach ($datasetCombination as $datasetCombinationElement) { foreach ($datasetCombination as $datasetCombinationElement) {
$partialDescriptions[] = $datasetCombinationElement['label']; $partialDescriptions[] = $datasetCombinationElement['label'];
// @phpstan-ignore-next-line
$values = array_merge($values, $datasetCombinationElement['values']); $values = array_merge($values, $datasetCombinationElement['values']);
} }
@ -221,7 +220,6 @@ final class DatasetsRepository
$result = $tmp; $result = $tmp;
} }
// @phpstan-ignore-next-line
return $result; return $result;
} }

View File

@ -19,9 +19,9 @@ final class SnapshotRepository
* Creates a snapshot repository instance. * Creates a snapshot repository instance.
*/ */
public function __construct( public function __construct(
readonly private string $rootPath, private readonly string $rootPath,
readonly private string $testsPath, private readonly string $testsPath,
readonly private string $snapshotsPath, private readonly string $snapshotsPath,
) {} ) {}
/** /**

View File

@ -4,20 +4,16 @@ declare(strict_types=1);
namespace Pest; namespace Pest;
use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection;
use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Configuration\Configuration;
use PHPUnit\TextUI\ShellExitCodeCalculator;
/** /**
* @internal * @internal
*/ */
final class Result final class Result
{ {
private const SUCCESS_EXIT = 0; private const int SUCCESS_EXIT = 0;
private const FAILURE_EXIT = 1;
private const EXCEPTION_EXIT = 2;
/** /**
* If the exit code is different from 0. * If the exit code is different from 0.
@ -40,44 +36,8 @@ final class Result
*/ */
public static function exitCode(Configuration $configuration, TestResult $result): int public static function exitCode(Configuration $configuration, TestResult $result): int
{ {
if ($result->wasSuccessfulIgnoringPhpunitWarnings()) { $shell = new ShellExitCodeCalculator;
if ($configuration->failOnWarning()) {
$warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents()
+ count($result->warnings())
+ count($result->phpWarnings());
if ($warnings > 0) { return $shell->calculate($configuration, $result);
return self::FAILURE_EXIT;
}
}
if (! $result->hasTestTriggeredPhpunitWarningEvents()) {
return self::SUCCESS_EXIT;
}
}
if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) {
return self::FAILURE_EXIT;
}
if ($result->wasSuccessfulIgnoringPhpunitWarnings()) {
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;
} }
} }

View File

@ -0,0 +1,39 @@
<?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;
}
}

View File

@ -35,7 +35,7 @@ final class EnsureIgnorableTestCasesAreIgnored implements StartedSubscriber
/** @var array<int, WarningTriggered> $testRunnerTriggeredWarningEvents */ /** @var array<int, WarningTriggered> $testRunnerTriggeredWarningEvents */
$testRunnerTriggeredWarningEvents = $property->getValue($collector); $testRunnerTriggeredWarningEvents = $property->getValue($collector);
$testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => $event->message() !== 'No tests found in class "Pest\TestCases\IgnorableTestCase".')); $testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => str_contains($event->message(), 'No tests found in class') === false));
$property->setValue($collector, $testRunnerTriggeredWarningEvents); $property->setValue($collector, $testRunnerTriggeredWarningEvents);
} }

View File

@ -11,12 +11,9 @@ use Pest\Exceptions\ShouldNotHappen;
*/ */
final class Backtrace final class Backtrace
{ {
/** private const string FILE = 'file';
* @var string
*/
private const FILE = 'file';
private const BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS; private const int BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS;
/** /**
* Returns the current test file. * Returns the current test file.

View File

@ -15,7 +15,6 @@ final class Closure
/** /**
* Binds the given closure to the given "this". * Binds the given closure to the given "this".
* *
*
* @throws ShouldNotHappen * @throws ShouldNotHappen
*/ */
public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure
@ -24,6 +23,7 @@ final class Closure
throw ShouldNotHappen::fromMessage('Could not bind null closure.'); throw ShouldNotHappen::fromMessage('Could not bind null closure.');
} }
// @phpstan-ignore-next-line
$closure = BaseClosure::bind($closure, $newThis, $newScope); $closure = BaseClosure::bind($closure, $newThis, $newScope);
if (! $closure instanceof \Closure) { if (! $closure instanceof \Closure) {

View File

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

View File

@ -11,9 +11,9 @@ use function Pest\testDirectory;
*/ */
final class DatasetInfo final class DatasetInfo
{ {
public const DATASETS_DIR_NAME = 'Datasets'; public const string DATASETS_DIR_NAME = 'Datasets';
public const DATASETS_FILE_NAME = 'Datasets.php'; public const string DATASETS_FILE_NAME = 'Datasets.php';
public static function isInsideADatasetsDirectory(string $file): bool public static function isInsideADatasetsDirectory(string $file): bool
{ {

View File

@ -13,7 +13,7 @@ use Throwable;
*/ */
final class ExceptionTrace final class ExceptionTrace
{ {
private const UNDEFINED_METHOD = 'Call to undefined method P\\'; private const string UNDEFINED_METHOD = 'Call to undefined method P\\';
/** /**
* Ensures the given closure reports the good execution context. * Ensures the given closure reports the good execution context.

View File

@ -15,7 +15,7 @@ final readonly class Exporter
/** /**
* The maximum number of items in an array to export. * The maximum number of items in an array to export.
*/ */
private const MAX_ARRAY_ITEMS = 3; private const int MAX_ARRAY_ITEMS = 3;
/** /**
* Creates a new Exporter instance. * Creates a new Exporter instance.
@ -66,6 +66,7 @@ final readonly class Exporter
$result[] = $context->contains($data[$key]) !== false $result[] = $context->contains($data[$key]) !== false
? '*RECURSION*' ? '*RECURSION*'
// @phpstan-ignore-next-line
: sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context)); : sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context));
} }

View File

@ -13,7 +13,7 @@ use Throwable;
*/ */
final class HigherOrderMessage final class HigherOrderMessage
{ {
public const UNDEFINED_METHOD = 'Method %s does not exist'; public const string UNDEFINED_METHOD = 'Method %s does not exist';
/** /**
* An optional condition that will determine if the message will be executed. * An optional condition that will determine if the message will be executed.
@ -50,14 +50,13 @@ final class HigherOrderMessage
} }
if ($this->hasHigherOrderCallable()) { if ($this->hasHigherOrderCallable()) {
/* @phpstan-ignore-next-line */
return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments); return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments);
} }
try { try {
return is_array($this->arguments) return is_array($this->arguments)
? Reflection::call($target, $this->name, $this->arguments) ? Reflection::call($target, $this->name, $this->arguments)
: $target->{$this->name}; /* @phpstan-ignore-line */ : $target->{$this->name};
} catch (Throwable $throwable) { } catch (Throwable $throwable) {
Reflection::setPropertyValue($throwable, 'file', $this->filename); Reflection::setPropertyValue($throwable, 'file', $this->filename);
Reflection::setPropertyValue($throwable, 'line', $this->line); Reflection::setPropertyValue($throwable, 'line', $this->line);
@ -65,7 +64,6 @@ final class HigherOrderMessage
if ($throwable->getMessage() === $this->getUndefinedMethodMessage($target, $this->name)) { if ($throwable->getMessage() === $this->getUndefinedMethodMessage($target, $this->name)) {
/** @var ReflectionClass<TValue> $reflection */ /** @var ReflectionClass<TValue> $reflection */
$reflection = new ReflectionClass($target); $reflection = new ReflectionClass($target);
/* @phpstan-ignore-next-line */
$reflection = $reflection->getParentClass() ?: $reflection; $reflection = $reflection->getParentClass() ?: $reflection;
Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->name)); Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->name));
} }
@ -96,10 +94,6 @@ final class HigherOrderMessage
private function getUndefinedMethodMessage(object $target, string $methodName): string private function getUndefinedMethodMessage(object $target, string $methodName): string
{ {
if (\PHP_MAJOR_VERSION >= 8) { return sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName));
return sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName));
}
return sprintf(self::UNDEFINED_METHOD, $methodName);
} }
} }

View File

@ -40,7 +40,6 @@ final class HigherOrderMessageCollection
public function chain(object $target): void public function chain(object $target): void
{ {
foreach ($this->messages as $message) { foreach ($this->messages as $message) {
// @phpstan-ignore-next-line
$target = $message->call($target) ?? $target; $target = $message->call($target) ?? $target;
} }
} }

View File

@ -26,7 +26,7 @@ final class HigherOrderTapProxy
*/ */
public function __set(string $property, mixed $value): void public function __set(string $property, mixed $value): void
{ {
$this->target->{$property} = $value; // @phpstan-ignore-line $this->target->{$property} = $value;
} }
/** /**
@ -37,7 +37,7 @@ final class HigherOrderTapProxy
public function __get(string $property) public function __get(string $property)
{ {
if (property_exists($this->target, $property)) { if (property_exists($this->target, $property)) {
return $this->target->{$property}; // @phpstan-ignore-line return $this->target->{$property};
} }
$className = (new ReflectionClass($this->target))->getName(); $className = (new ReflectionClass($this->target))->getName();

101
src/Support/Shell.php Normal file
View File

@ -0,0 +1,101 @@
<?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', [])
);
}
}

View File

@ -13,12 +13,9 @@ final class Str
* Pool of alpha-numeric characters for generating (unsafe) random strings * Pool of alpha-numeric characters for generating (unsafe) random strings
* from. * from.
*/ */
private const POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; private const string POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
/** private const string PREFIX = '__pest_evaluable_';
* @var string
*/
private const PREFIX = '__pest_evaluable_';
/** /**
* Create a (unsecure & non-cryptographically safe) random alpha-numeric * Create a (unsecure & non-cryptographically safe) random alpha-numeric
@ -120,4 +117,14 @@ final class Str
{ {
return (bool) filter_var($value, FILTER_VALIDATE_URL); 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, '-'));
}
} }

View File

@ -12,7 +12,7 @@
*/ */
pest()->extend(Tests\TestCase::class) pest()->extend(Tests\TestCase::class)
->use(Illuminate\Foundation\Testing\RefreshDatabase::class) // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature'); ->in('Feature');
/* /*

View File

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

View File

@ -11,7 +11,7 @@
| |
*/ */
// pest()->extend(Tests\TestCase::class)->in('Feature'); pest()->extend(Tests\TestCase::class)->in('Feature');
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php" bootstrap="vendor/autoload.php"
colors="true" colors="true"
> >
@ -11,8 +11,8 @@
</testsuites> </testsuites>
<source> <source>
<include> <include>
<directory suffix=".php">./app</directory> <directory>app</directory>
<directory suffix=".php">./src</directory> <directory>src</directory>
</include> </include>
</source> </source>
</phpunit> </phpunit>

View File

@ -1,5 +1,5 @@
Pest Testing Framework 3.7.4. Pest Testing Framework 4.0.0-alpha.6.
USAGE: pest <file> [options] USAGE: pest <file> [options]
@ -53,7 +53,7 @@
--disallow-test-output ................. Be strict about output during tests --disallow-test-output ................. Be strict about output during tests
--enforce-time-limit ................. Enforce time limit based on test size --enforce-time-limit ................. Enforce time limit based on test size
--default-time-limit [sec] Timeout in seconds for tests that have no declared size --default-time-limit [sec] Timeout in seconds for tests that have no declared size
--dont-report-useless-tests .. Do not report tests that do not test anything --do-not-report-useless-tests Do not report tests that do not test anything
--stop-on-defect ... Stop after first error, failure, warning, or risky test --stop-on-defect ... Stop after first error, failure, warning, or risky test
--stop-on-error ..................................... Stop after first error --stop-on-error ..................................... Stop after first error
--stop-on-failure ................................. Stop after first failure --stop-on-failure ................................. Stop after first failure
@ -68,9 +68,22 @@
--fail-on-risky Signal failure using shell exit code when a test was considered risky --fail-on-risky Signal failure using shell exit code when a test was considered risky
--fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered --fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered
--fail-on-phpunit-deprecation Signal failure using shell exit code when a PHPUnit deprecation was triggered --fail-on-phpunit-deprecation Signal failure using shell exit code when a PHPUnit deprecation was triggered
--fail-on-phpunit-notice Signal failure using shell exit code when a PHPUnit notice was triggered
--fail-on-phpunit-warning Signal failure using shell exit code when a PHPUnit warning was triggered
--fail-on-notice Signal failure using shell exit code when a notice was triggered --fail-on-notice Signal failure using shell exit code when a notice was triggered
--fail-on-skipped Signal failure using shell exit code when a test was skipped --fail-on-skipped Signal failure using shell exit code when a test was skipped
--fail-on-incomplete Signal failure using shell exit code when a test was marked incomplete --fail-on-incomplete Signal failure using shell exit code when a test was marked incomplete
--fail-on-all-issues Signal failure using shell exit code when an issue is triggered
--do-not-fail-on-empty-test-suite Do not signal failure using shell exit code when no tests were run
--do-not-fail-on-warning Do not signal failure using shell exit code when a warning was triggered
--do-not-fail-on-risky Do not signal failure using shell exit code when a test was considered risky
--do-not-fail-on-deprecation Do not signal failure using shell exit code when a deprecation was triggered
--do-not-fail-on-phpunit-deprecation Do not signal failure using shell exit code when a PHPUnit deprecation was triggered
--do-not-fail-on-phpunit-notice Do not signal failure using shell exit code when a PHPUnit notice was triggered
--do-not-fail-on-phpunit-warning Do not signal failure using shell exit code when a PHPUnit warning was triggered
--do-not-fail-on-notice Do not signal failure using shell exit code when a notice was triggered
--do-not-fail-on-skipped Do not signal failure using shell exit code when a test was skipped
--do-not-fail-on-incomplete Do not signal failure using shell exit code when a test was marked incomplete
--cache-result ............................ Write test results to cache file --cache-result ............................ Write test results to cache file
--do-not-cache-result .............. Do not write test results to cache file --do-not-cache-result .............. Do not write test results to cache file
--order-by [order] Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size --order-by [order] Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size
@ -88,18 +101,23 @@
--display-skipped ........................ Display details for skipped tests --display-skipped ........................ Display details for skipped tests
--display-deprecations . Display details for deprecations triggered by tests --display-deprecations . Display details for deprecations triggered by tests
--display-phpunit-deprecations .... Display details for PHPUnit deprecations --display-phpunit-deprecations .... Display details for PHPUnit deprecations
--display-phpunit-notices .............. Display details for PHPUnit notices
--display-errors ............. Display details for errors triggered by tests --display-errors ............. Display details for errors triggered by tests
--display-notices ........... Display details for notices triggered by tests --display-notices ........... Display details for notices triggered by tests
--display-warnings ......... Display details for warnings triggered by tests --display-warnings ......... Display details for warnings triggered by tests
--display-all-issues ..... Display details for all issues that are triggered
--reverse-list .............................. Print defects in reverse order --reverse-list .............................. Print defects in reverse order
--teamcity . Replace default progress and result output with TeamCity format --teamcity . Replace default progress and result output with TeamCity format
--testdox ................ Replace default result output with TestDox format --testdox ................ Replace default result output with TestDox format
--testdox-summary Repeat TestDox output for tests with errors, failures, or issues --testdox-summary Repeat TestDox output for tests with errors, failures, or issues
--debug Replace default progress and result output with debugging information --debug Replace default progress and result output with debugging information
--with-telemetry Include telemetry information in debugging information output
--compact ................ Replace default result output with Compact format --compact ................ Replace default result output with Compact format
LOGGING OPTIONS: LOGGING OPTIONS:
--log-junit [file] .......... Write test results in JUnit XML format to file --log-junit [file] .......... Write test results in JUnit XML format to file
--log-otr [file] Write test results in Open Test Reporting XML format to file
--include-git-information Include Git information in Open Test Reporting XML logfile
--log-teamcity [file] ........ Write test results in TeamCity format to file --log-teamcity [file] ........ Write test results in TeamCity format to file
--testdox-html [file] .. Write test results in TestDox format (HTML) to file --testdox-html [file] .. Write test results in TestDox format (HTML) to file
--testdox-text [file] Write test results in TestDox format (plain text) to file --testdox-text [file] Write test results in TestDox format (plain text) to file
@ -111,6 +129,7 @@
--coverage ..... Generate code coverage report and output to standard output --coverage ..... Generate code coverage report and output to standard output
--coverage --min Set the minimum required coverage percentage, and fail if not met --coverage --min Set the minimum required coverage percentage, and fail if not met
--coverage-clover [file] Write code coverage report in Clover XML format to file --coverage-clover [file] Write code coverage report in Clover XML format to file
--coverage-openclover [file] Write code coverage report in OpenClover XML format to file
--coverage-cobertura [file] Write code coverage report in Cobertura XML format to file --coverage-cobertura [file] Write code coverage report in Cobertura XML format to file
--coverage-crap4j [file] Write code coverage report in Crap4J XML format to file --coverage-crap4j [file] Write code coverage report in Crap4J XML format to file
--coverage-html [dir] Write code coverage report in HTML format to directory --coverage-html [dir] Write code coverage report in HTML format to directory

View File

@ -1,3 +1,3 @@
Pest Testing Framework 3.7.4. Pest Testing Framework 4.0.0-alpha.6.

View File

@ -4,7 +4,6 @@
✓ preset → strict → ignoring ['usleep'] ✓ preset → strict → ignoring ['usleep']
✓ preset → security → ignoring ['eval', 'str_shuffle', 'exec', …] ✓ preset → security → ignoring ['eval', 'str_shuffle', 'exec', …]
✓ globals ✓ globals
✓ dependencies
✓ contracts ✓ contracts
PASS Tests\Environments\Windows PASS Tests\Environments\Windows
@ -68,14 +67,24 @@
✓ it adds coverage if --min exist ✓ it adds coverage if --min exist
✓ it generates coverage based on file input ✓ it generates coverage based on file input
PASS Tests\Features\Covers PASS Tests\Features\Covers\ClassCoverage
✓ it uses the correct PHPUnit attribute for class ✓ it uses the correct PHPUnit attribute for class
✓ it uses the correct PHPUnit attribute for function
✓ it guesses if the given argument is a class or function PASS Tests\Features\Covers\CoversNothing
✓ it uses the correct PHPUnit attribute for trait
✓ it uses the correct PHPUnit attribute for covers nothing ✓ it uses the correct PHPUnit attribute for covers nothing
PASS Tests\Features\Covers\ExceptionHandling
✓ it throws exception if no class nor method has been found ✓ it throws exception if no class nor method has been found
PASS Tests\Features\Covers\FunctionCoverage
✓ it uses the correct PHPUnit attribute for function
PASS Tests\Features\Covers\GuessCoverage
✓ it guesses if the given argument is a class or function
PASS Tests\Features\Covers\TraitCoverage
✓ it uses the correct PHPUnit attribute for trait
PASS Tests\Features\DatasetsTests - 1 todo PASS Tests\Features\DatasetsTests - 1 todo
✓ it throws exception if dataset does not exist ✓ it throws exception if dataset does not exist
✓ it throws exception if dataset already exist ✓ it throws exception if dataset already exist
@ -619,6 +628,13 @@
✓ pass ✓ pass
✓ failures ✓ failures
✓ failures with custom message ✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeSlug
✓ pass
✓ failures
✓ failures with custom message
✓ failures with default message
✓ not failures ✓ not failures
PASS Tests\Features\Expect\toBeSnakeCase PASS Tests\Features\Expect\toBeSnakeCase
@ -1030,6 +1046,10 @@
✓ it may fail ✓ it may fail
✓ it may fail with the given message ✓ it may fail with the given message
PASS Tests\Features\Fixture
✓ it may return a file path
✓ it may throw an exception if the file does not exist
WARN Tests\Features\Helpers WARN Tests\Features\Helpers
✓ it can set/get properties on $this ✓ it can set/get properties on $this
! it gets null if property do not exist → Undefined property Tests\Features\Helpers::$wqdwqdqw ! it gets null if property do not exist → Undefined property Tests\Features\Helpers::$wqdwqdqw
@ -1119,6 +1139,10 @@
✓ nested → it may be associated with an pr #1, #4, #5, #6, #3 ✓ nested → it may be associated with an pr #1, #4, #5, #6, #3
// an note between an the pr // an note between an the pr
PASS Tests\Features\References
✓ it can reference a specific class
✓ it can reference a specific class method
PASS Tests\Features\Repeat PASS Tests\Features\Repeat
✓ once ✓ once
✓ multiple times @ repetition 1 of 5 ✓ multiple times @ repetition 1 of 5
@ -1295,6 +1319,10 @@
✓ it can see datasets defined in Pest.php file with ('B') ✓ it can see datasets defined in Pest.php file with ('B')
✓ Pest.php dataset is taken ✓ Pest.php dataset is taken
PASS Tests\Features\See
✓ it can reference a specific class
✓ it can reference a specific class method
WARN Tests\Features\Skip WARN Tests\Features\Skip
✓ it do not skips ✓ it do not skips
- it skips with truthy → 1 - it skips with truthy → 1
@ -1425,6 +1453,11 @@
✓ nested → nested afterEach execution order ✓ nested → nested afterEach execution order
✓ global afterEach execution order ✓ global afterEach execution order
PASS Tests\Hooks\BeforeAllTest
✓ it gets called before all tests 1 @ repetition 1 of 2
✓ it gets called before all tests 1 @ repetition 2 of 2
✓ it gets called before all tests 2
PASS Tests\Hooks\BeforeEachTest PASS Tests\Hooks\BeforeEachTest
✓ global beforeEach execution order ✓ global beforeEach execution order
@ -1698,4 +1731,4 @@
WARN Tests\Visual\Version WARN Tests\Visual\Version
- visual snapshot of help command output - visual snapshot of help command output
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1144 passed (2736 assertions) Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1157 passed (2766 assertions)

View File

@ -27,26 +27,6 @@ arch('globals')
->not->toBeUsed() ->not->toBeUsed()
->ignoring(Expectation::class); ->ignoring(Expectation::class);
arch('dependencies')
->expect('Pest')
->toOnlyUse([
'dd',
'dump',
'expect',
'uses',
'Termwind',
'ParaTest',
'Pest\Arch',
'Pest\Mutate\Contracts\Configuration',
'Pest\Mutate\Decorators\TestCallDecorator',
'Pest\Mutate\Repositories\ConfigurationRepository',
'Pest\Plugin',
'NunoMaduro\Collision',
'Whoops',
'Symfony\Component\Console',
'Symfony\Component\Process',
])->ignoring(['Composer', 'PHPUnit', 'SebastianBergmann']);
arch('contracts') arch('contracts')
->expect('Pest\Contracts') ->expect('Pest\Contracts')
->toOnlyUse([ ->toOnlyUse([

View File

@ -1,59 +0,0 @@
<?php
use Pest\PendingCalls\TestCall;
use Pest\TestSuite;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversFunction;
use Tests\Fixtures\Covers\CoversClass1;
use Tests\Fixtures\Covers\CoversClass3;
use Tests\Fixtures\Covers\CoversTrait;
$runCounter = 0;
function testCoversFunction() {}
covers([CoversClass1::class]);
it('uses the correct PHPUnit attribute for class', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[1]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass');
expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1');
});
it('uses the correct PHPUnit attribute for function', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[3]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction');
expect($attributes[3]->getArguments()[0])->toBe('testCoversFunction');
})->coversFunction('testCoversFunction');
it('guesses if the given argument is a class or function', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[5]->getName())->toBe(CoversClass::class);
expect($attributes[5]->getArguments()[0])->toBe(CoversClass3::class);
expect($attributes[6]->getName())->toBe(CoversFunction::class);
expect($attributes[6]->getArguments()[0])->toBe('testCoversFunction');
})->covers(CoversClass3::class, 'testCoversFunction');
it('uses the correct PHPUnit attribute for trait', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[8]->getName())->toBe('PHPUnit\Framework\Attributes\CoversTrait');
expect($attributes[8]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait');
})->coversTrait(CoversTrait::class);
it('uses the correct PHPUnit attribute for covers nothing', function () {
$attributes = (new ReflectionMethod($this, $this->name()))->getAttributes();
expect($attributes[3]->getName())->toBe('PHPUnit\Framework\Attributes\CoversNothing');
expect($attributes[3]->getArguments())->toHaveCount(0);
})->coversNothing();
it('throws exception if no class nor method has been found', function () {
$testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure');
$testCall->covers('fakeName');
})->throws(InvalidArgumentException::class, 'No class, trait or method named "fakeName" has been found.');

View File

@ -0,0 +1,13 @@
<?php
use PHPUnit\Framework\Attributes\CoversClass;
use Tests\Fixtures\Covers\CoversClass1;
covers([CoversClass1::class]);
it('uses the correct PHPUnit attribute for class', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[1]->getName())->toBe(CoversClass::class);
expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1');
});

View File

@ -0,0 +1,10 @@
<?php
use PHPUnit\Framework\Attributes\CoversNothing;
it('uses the correct PHPUnit attribute for covers nothing', function () {
$attributes = (new ReflectionMethod($this, $this->name()))->getAttributes();
expect($attributes[2]->getName())->toBe(CoversNothing::class);
expect($attributes[2]->getArguments())->toHaveCount(0);
})->coversNothing();

View File

@ -0,0 +1,10 @@
<?php
use Pest\PendingCalls\TestCall;
use Pest\TestSuite;
it('throws exception if no class nor method has been found', function () {
$testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure');
$testCall->covers('fakeName');
})->throws(InvalidArgumentException::class, 'No class, trait or method named "fakeName" has been found.');

View File

@ -0,0 +1,12 @@
<?php
use PHPUnit\Framework\Attributes\CoversFunction;
function testCoversFunction() {}
it('uses the correct PHPUnit attribute for function', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[1]->getName())->toBe(CoversFunction::class);
expect($attributes[1]->getArguments()[0])->toBe('testCoversFunction');
})->coversFunction('testCoversFunction');

View File

@ -0,0 +1,17 @@
<?php
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversFunction;
use Tests\Fixtures\Covers\CoversClass3;
function testCoversFunction2() {}
it('guesses if the given argument is a class or function', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[1]->getName())->toBe(CoversClass::class);
expect($attributes[1]->getArguments()[0])->toBe(CoversClass3::class);
expect($attributes[2]->getName())->toBe(CoversFunction::class);
expect($attributes[2]->getArguments()[0])->toBe('testCoversFunction2');
})->covers(CoversClass3::class, 'testCoversFunction2');

View File

@ -0,0 +1,11 @@
<?php
use PHPUnit\Framework\Attributes\CoversTrait as PHPUnitCoversTrait;
use Tests\Fixtures\Covers\CoversTrait;
it('uses the correct PHPUnit attribute for trait', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[1]->getName())->toBe(PHPUnitCoversTrait::class);
expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait');
})->coversTrait(CoversTrait::class);

View File

@ -0,0 +1,24 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect('This is a Test String!')->toBeSlug()
->and('Another Test String')->toBeSlug();
});
test('failures', function () {
expect('')->toBeSlug();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
expect('')->toBeSlug('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('failures with default message', function () {
expect('')->toBeSlug();
})->throws(ExpectationFailedException::class, 'Failed asserting that can be converted to a slug.');
test('not failures', function () {
expect('This is a Test String!')->not->toBeSlug();
})->throws(ExpectationFailedException::class);

View File

@ -0,0 +1,12 @@
<?php
it('may return a file path', function () {
$file = fixture('phpunit-in-isolation.xml');
expect($file)->toBeString()
->toBeFile();
});
it('may throw an exception if the file does not exist', function () {
fixture('file-that-does-not-exist.php');
})->throws(InvalidArgumentException::class);

View File

@ -0,0 +1,11 @@
<?php
use Pest\Panic;
it('can reference a specific class', function () {
expect(Panic::class)->toBeString();
})->references(Panic::class);
it('can reference a specific class method', function () {
expect(Panic::with(...))->toBeCallable();
})->references([Panic::class, 'with']);

11
tests/Features/See.php Normal file
View File

@ -0,0 +1,11 @@
<?php
use Pest\Panic;
it('can reference a specific class', function () {
expect(Panic::class)->toBeString();
})->see(Panic::class);
it('can reference a specific class method', function () {
expect(Panic::with(...))->toBeCallable();
})->see([Panic::class, 'with']);

View File

@ -0,0 +1,16 @@
<?php
pest()->beforeAll(function () {
expect($_SERVER['globalHook']->calls->beforeAll)
->toBe(0);
$_SERVER['globalHook']->calls->beforeAll++;
});
it('gets called before all tests 1', function () {
expect($_SERVER['globalHook']->calls->beforeAll)->toBe(1);
})->repeat(2);
it('gets called before all tests 2', function () {
expect($_SERVER['globalHook']->calls->beforeAll)->toBe(1);
});

View File

@ -29,7 +29,6 @@ pest()
}) })
->beforeAll(function () { ->beforeAll(function () {
$_SERVER['globalHook']->beforeAll = 0; $_SERVER['globalHook']->beforeAll = 0;
$_SERVER['globalHook']->calls->beforeAll++;
}) })
->afterEach(function () { ->afterEach(function () {
if (! isset($this->ith)) { if (! isset($this->ith)) {

View File

@ -16,7 +16,7 @@ $run = function () {
test('parallel', function () use ($run) { test('parallel', function () use ($run) {
expect($run('--exclude-group=integration')) expect($run('--exclude-group=integration'))
->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 24 skipped, 1134 passed (2712 assertions)') ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 24 skipped, 1147 passed (2742 assertions)')
->toContain('Parallel: 3 processes'); ->toContain('Parallel: 3 processes');
})->skipOnWindows(); })->skipOnWindows();