mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 07:47:22 +01:00
Merge branch '4.x' into 3.x
This commit is contained in:
10
.github/workflows/static.yml
vendored
10
.github/workflows/static.yml
vendored
@ -24,15 +24,19 @@ jobs:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.2
|
||||
php-version: 8.3
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
extensions: sockets
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer update --prefer-stable --no-interaction --no-progress --ansi
|
||||
|
||||
# - name: Type Check
|
||||
# run: composer test:type:check
|
||||
- name: Profanity Check
|
||||
run: composer test:profanity
|
||||
|
||||
- name: Type Check
|
||||
run: composer test:type:check
|
||||
|
||||
- name: Type Coverage
|
||||
run: composer test:type:coverage
|
||||
|
||||
7
.github/workflows/tests.yml
vendored
7
.github/workflows/tests.yml
vendored
@ -13,9 +13,9 @@ jobs:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
symfony: ['7.1']
|
||||
php: ['8.2', '8.3', '8.4']
|
||||
dependency_version: [prefer-lowest, prefer-stable]
|
||||
symfony: ['7.3']
|
||||
php: ['8.3', '8.4']
|
||||
dependency_version: [prefer-stable]
|
||||
|
||||
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
|
||||
|
||||
@ -29,6 +29,7 @@ jobs:
|
||||
php-version: ${{ matrix.php }}
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
extensions: sockets
|
||||
|
||||
- name: Setup Problem Matches
|
||||
run: |
|
||||
|
||||
16
README.md
16
README.md
@ -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.
|
||||
|
||||
- Explore our docs at **[pestphp.com »](https://pestphp.com)**
|
||||
- Follow us on Twitter at **[@pestphp »](https://twitter.com/pestphp)**
|
||||
- Join us at **[discord.gg/kaHY6p54JH »](https://discord.gg/kaHY6p54JH)** or **[t.me/+kYH5G4d5MV83ODk0 »](https://t.me/+kYH5G4d5MV83ODk0)**
|
||||
- Follow the creator Nuno Maduro:
|
||||
- YouTube: **[youtube.com/@nunomaduro](https://www.youtube.com/@nunomaduro)** — Videos every weekday
|
||||
- Twitch: **[twitch.tv/enunomaduro](https://www.twitch.tv/enunomaduro)** — Streams (almost) every weekday
|
||||
- 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
|
||||
|
||||
@ -29,17 +34,16 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
|
||||
|
||||
### Gold Sponsors
|
||||
|
||||
- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
|
||||
- **[LaraJobs](https://larajobs.com/?ref=pestphp)**
|
||||
- **[Brokerchooser](https://brokerchooser.com/?ref=pestphp)**
|
||||
- **[Forge](https://forge.laravel.com/?ref=pestphp)**
|
||||
- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
|
||||
- **[NativePHP](https://nativephp.com/mobile?ref=pestphp.com)**
|
||||
|
||||
### Premium Sponsors
|
||||
|
||||
- [Akaunting](https://akaunting.com/?ref=pestphp)
|
||||
- [Codecourse](https://codecourse.com/?ref=pestphp)
|
||||
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
|
||||
- [Localazy](https://localazy.com/?ref=pestphp)
|
||||
- [Forge](https://forge.laravel.com/?ref=pestphp)
|
||||
- [Route4Me](https://www.route4me.com/?ref=pestphp)
|
||||
- [Spatie](https://spatie.be/?ref=pestphp)
|
||||
- [Worksome](https://www.worksome.com/?ref=pestphp)
|
||||
|
||||
@ -17,19 +17,21 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2.0",
|
||||
"brianium/paratest": "^7.7.0",
|
||||
"nunomaduro/collision": "^8.6.1",
|
||||
"nunomaduro/termwind": "^2.3.0",
|
||||
"pestphp/pest-plugin": "^3.0.0",
|
||||
"pestphp/pest-plugin-arch": "^3.0.0",
|
||||
"pestphp/pest-plugin-mutate": "^3.0.5",
|
||||
"phpunit/phpunit": "^11.5.6"
|
||||
"php": "^8.3.0",
|
||||
"brianium/paratest": "^7.11.0",
|
||||
"nunomaduro/collision": "^8.8.2",
|
||||
"nunomaduro/termwind": "^2.3.1",
|
||||
"pestphp/pest-plugin": "^4.0.0",
|
||||
"pestphp/pest-plugin-arch": "^4.0.0",
|
||||
"pestphp/pest-plugin-mutate": "^4.0.0",
|
||||
"pestphp/pest-plugin-profanity": "^4.0.0",
|
||||
"phpunit/phpunit": "^12.2.7",
|
||||
"symfony/process": "^7.3.0"
|
||||
},
|
||||
"conflict": {
|
||||
"filp/whoops": "<2.16.0",
|
||||
"phpunit/phpunit": ">11.5.6",
|
||||
"sebastian/exporter": "<6.0.0",
|
||||
"filp/whoops": "<2.18.3",
|
||||
"phpunit/phpunit": ">12.2.7",
|
||||
"sebastian/exporter": "<7.0.0",
|
||||
"webmozart/assert": "<1.11.0"
|
||||
},
|
||||
"autoload": {
|
||||
@ -53,9 +55,10 @@
|
||||
]
|
||||
},
|
||||
"require-dev": {
|
||||
"pestphp/pest-dev-tools": "^3.3.0",
|
||||
"pestphp/pest-plugin-type-coverage": "^3.2.3",
|
||||
"symfony/process": "^7.2.0"
|
||||
"pestphp/pest-dev-tools": "^4.0.0",
|
||||
"pestphp/pest-plugin-browser": "^4.0.0",
|
||||
"pestphp/pest-plugin-type-coverage": "^4.0.0",
|
||||
"psy/psysh": "^0.12.9"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
@ -71,16 +74,17 @@
|
||||
],
|
||||
"scripts": {
|
||||
"refacto": "rector",
|
||||
"lint": "pint",
|
||||
"lint": "pint --parallel",
|
||||
"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:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100",
|
||||
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
|
||||
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
|
||||
"test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3",
|
||||
"test:integration": "php bin/pest --colors=always --group=integration -v",
|
||||
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
|
||||
"test:unit": "php bin/pest --exclude-group=integration --compact",
|
||||
"test:inline": "php bin/pest --configuration=phpunit.inline.xml",
|
||||
"test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3",
|
||||
"test:integration": "php bin/pest --group=integration -v",
|
||||
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots",
|
||||
"test": [
|
||||
"@test:refacto",
|
||||
"@test:lint",
|
||||
@ -111,6 +115,7 @@
|
||||
"Pest\\Plugins\\Snapshot",
|
||||
"Pest\\Plugins\\Verbose",
|
||||
"Pest\\Plugins\\Version",
|
||||
"Pest\\Plugins\\Shard",
|
||||
"Pest\\Plugins\\Parallel"
|
||||
]
|
||||
},
|
||||
|
||||
@ -52,6 +52,8 @@ use PHPUnit\Util\Filter;
|
||||
use PHPUnit\Util\ThrowableToStringMapper;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final readonly class ThrowableBuilder
|
||||
@ -82,7 +84,7 @@ final readonly class ThrowableBuilder
|
||||
$t->getMessage(),
|
||||
ThrowableToStringMapper::map($t),
|
||||
$trace,
|
||||
$previous
|
||||
$previous,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ use PHPUnit\Event\Test\Finished;
|
||||
use PHPUnit\Event\Test\MarkedIncomplete;
|
||||
use PHPUnit\Event\Test\PreparationStarted;
|
||||
use PHPUnit\Event\Test\Prepared;
|
||||
use PHPUnit\Event\Test\PrintedUnexpectedOutput;
|
||||
use PHPUnit\Event\Test\Skipped;
|
||||
use PHPUnit\Event\TestSuite\Started;
|
||||
use PHPUnit\Event\UnknownSubscriberTypeException;
|
||||
@ -41,6 +42,8 @@ use function str_replace;
|
||||
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
|
||||
*/
|
||||
final class JunitXmlLogger
|
||||
@ -59,32 +62,32 @@ final class JunitXmlLogger
|
||||
private array $testSuites = [];
|
||||
|
||||
/**
|
||||
* @psalm-var array<int,int>
|
||||
* @var array<int,int>
|
||||
*/
|
||||
private array $testSuiteTests = [0];
|
||||
|
||||
/**
|
||||
* @psalm-var array<int,int>
|
||||
* @var array<int,int>
|
||||
*/
|
||||
private array $testSuiteAssertions = [0];
|
||||
|
||||
/**
|
||||
* @psalm-var array<int,int>
|
||||
* @var array<int,int>
|
||||
*/
|
||||
private array $testSuiteErrors = [0];
|
||||
|
||||
/**
|
||||
* @psalm-var array<int,int>
|
||||
* @var array<int,int>
|
||||
*/
|
||||
private array $testSuiteFailures = [0];
|
||||
|
||||
/**
|
||||
* @psalm-var array<int,int>
|
||||
* @var array<int,int>
|
||||
*/
|
||||
private array $testSuiteSkipped = [0];
|
||||
|
||||
/**
|
||||
* @psalm-var array<int,int>
|
||||
* @var array<int,int>
|
||||
*/
|
||||
private array $testSuiteTimes = [0];
|
||||
|
||||
@ -113,7 +116,7 @@ final class JunitXmlLogger
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->printer->print($this->document->saveXML());
|
||||
$this->printer->print($this->document->saveXML() ?: '');
|
||||
|
||||
$this->printer->flush();
|
||||
}
|
||||
@ -195,28 +198,34 @@ final class JunitXmlLogger
|
||||
$this->createTestCase($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function testPreparationFailed(): void
|
||||
{
|
||||
$this->preparationFailed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function testPrepared(): void
|
||||
{
|
||||
$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
|
||||
*/
|
||||
public function testFinished(Finished $event): void
|
||||
{
|
||||
if ($this->preparationFailed) {
|
||||
if (! $this->prepared || $this->preparationFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -305,9 +314,11 @@ final class JunitXmlLogger
|
||||
new TestPreparationStartedSubscriber($this),
|
||||
new TestPreparationFailedSubscriber($this),
|
||||
new TestPreparedSubscriber($this),
|
||||
new TestPrintedUnexpectedOutputSubscriber($this),
|
||||
new TestFinishedSubscriber($this),
|
||||
new TestErroredSubscriber($this),
|
||||
new TestFailedSubscriber($this),
|
||||
new TestMarkedIncompleteSubscriber($this),
|
||||
new TestSkippedSubscriber($this),
|
||||
new TestRunnerExecutionFinishedSubscriber($this),
|
||||
);
|
||||
@ -431,7 +442,7 @@ final class JunitXmlLogger
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @psalm-assert !null $this->currentTestCase
|
||||
* @phpstan-assert !null $this->currentTestCase
|
||||
*/
|
||||
private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void
|
||||
{
|
||||
|
||||
98
overrides/Report/PHP.php
Normal file
98
overrides/Report/PHP.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -46,9 +46,10 @@ declare(strict_types=1);
|
||||
namespace PHPUnit\Runner\ResultCache;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const LOCK_EX;
|
||||
|
||||
use PHPUnit\Framework\TestStatus\TestStatus;
|
||||
use PHPUnit\Runner\DirectoryCannotBeCreatedException;
|
||||
use PHPUnit\Runner\DirectoryDoesNotExistException;
|
||||
use PHPUnit\Runner\Exception;
|
||||
use PHPUnit\Util\Filesystem;
|
||||
|
||||
@ -65,24 +66,23 @@ use function json_encode;
|
||||
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
|
||||
*/
|
||||
final class DefaultResultCache implements ResultCache
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
|
||||
private const string DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
|
||||
|
||||
private readonly string $cacheFilename;
|
||||
|
||||
/**
|
||||
* @psalm-var array<string, TestStatus>
|
||||
* @var array<string, TestStatus>
|
||||
*/
|
||||
private array $defects = [];
|
||||
|
||||
/**
|
||||
* @psalm-var array<string, float>
|
||||
* @var array<string, float>
|
||||
*/
|
||||
private array $times = [];
|
||||
|
||||
@ -95,28 +95,39 @@ final class DefaultResultCache implements ResultCache
|
||||
$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()) {
|
||||
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
|
||||
@ -165,7 +176,7 @@ final class DefaultResultCache implements ResultCache
|
||||
public function persist(): void
|
||||
{
|
||||
if (! Filesystem::createDirectory(dirname($this->cacheFilename))) {
|
||||
throw new DirectoryCannotBeCreatedException($this->cacheFilename);
|
||||
throw new DirectoryDoesNotExistException(dirname($this->cacheFilename));
|
||||
}
|
||||
|
||||
$data = [
|
||||
|
||||
@ -45,6 +45,7 @@ declare(strict_types=1);
|
||||
namespace PHPUnit\TextUI;
|
||||
|
||||
use Pest\Plugins\Only;
|
||||
use Pest\Runner\Filter\EnsureTestCaseIsInitiatedFilter;
|
||||
use PHPUnit\Event;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Runner\Filter\Factory;
|
||||
@ -66,6 +67,12 @@ final readonly class TestSuiteFilterProcessor
|
||||
{
|
||||
$factory = new Factory;
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
(fn () => $this->filters[] = [
|
||||
'className' => EnsureTestCaseIsInitiatedFilter::class,
|
||||
'argument' => '',
|
||||
])->call($factory);
|
||||
|
||||
if (! $configuration->hasFilter() &&
|
||||
! $configuration->hasGroups() &&
|
||||
! $configuration->hasExcludeGroups() &&
|
||||
@ -73,6 +80,8 @@ final readonly class TestSuiteFilterProcessor
|
||||
! $configuration->hasTestsCovering() &&
|
||||
! $configuration->hasTestsUsing() &&
|
||||
! Only::isEnabled()) {
|
||||
$suite->injectFilter($factory);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
199
phpstan-baseline.neon
Normal file
199
phpstan-baseline.neon
Normal 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
|
||||
@ -1,14 +1,12 @@
|
||||
includes:
|
||||
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
||||
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
|
||||
- phpstan-baseline.neon
|
||||
|
||||
parameters:
|
||||
level: max
|
||||
level: 7
|
||||
paths:
|
||||
- src
|
||||
|
||||
checkMissingIterableValueType: true
|
||||
reportUnmatchedIgnoredErrors: true
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
|
||||
ignoreErrors:
|
||||
- "#type mixed is not subtype of native#"
|
||||
|
||||
22
resources/views/installers/plugin-browser.php
Normal file
22
resources/views/installers/plugin-browser.php
Normal 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>
|
||||
@ -17,7 +17,7 @@ final class BootExcludeList implements Bootstrapper
|
||||
*
|
||||
* @var array<int, non-empty-string>
|
||||
*/
|
||||
private const EXCLUDE_LIST = [
|
||||
private const array EXCLUDE_LIST = [
|
||||
'bin',
|
||||
'overrides',
|
||||
'resources',
|
||||
|
||||
@ -24,7 +24,7 @@ final class BootFiles implements Bootstrapper
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const STRUCTURE = [
|
||||
private const array STRUCTURE = [
|
||||
'Expectations',
|
||||
'Expectations.php',
|
||||
'Helpers',
|
||||
|
||||
@ -15,17 +15,18 @@ final class BootOverrides implements Bootstrapper
|
||||
/**
|
||||
* The list of files to be overridden.
|
||||
*
|
||||
* @var array<string, string>
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public const FILES = [
|
||||
'53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php',
|
||||
'77ffb7647b583bd82e37962c6fbdc4b04d3344d8a2c1ed103e625ed1ff7cb5c2' => 'Runner/ResultCache/DefaultResultCache.php',
|
||||
'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php',
|
||||
'3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
|
||||
'8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
|
||||
'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php',
|
||||
'8cfcb4999af79463eca51a42058e502ea4ddc776cba5677bf2f8eb6093e21a5c' => 'Event/Value/ThrowableBuilder.php',
|
||||
'86cd9bcaa53cdd59c5b13e58f30064a015c549501e7629d93b96893d4dee1eb1' => 'Logging/JUnit/JunitXmlLogger.php',
|
||||
public const array FILES = [
|
||||
'Runner/Filter/NameFilterIterator.php',
|
||||
'Runner/ResultCache/DefaultResultCache.php',
|
||||
'Runner/TestSuiteLoader.php',
|
||||
'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
|
||||
'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
|
||||
'TextUI/TestSuiteFilterProcessor.php',
|
||||
'Event/Value/ThrowableBuilder.php',
|
||||
'Logging/JUnit/JunitXmlLogger.php',
|
||||
'Report/PHP.php',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -20,7 +20,7 @@ final readonly class BootSubscribers implements Bootstrapper
|
||||
*
|
||||
* @var array<int, class-string<Subscriber>>
|
||||
*/
|
||||
private const SUBSCRIBERS = [
|
||||
private const array SUBSCRIBERS = [
|
||||
Subscribers\EnsureConfigurationIsAvailable::class,
|
||||
Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
|
||||
Subscribers\EnsureKernelDumpIsFlushed::class,
|
||||
|
||||
@ -6,10 +6,12 @@ namespace Pest\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Pest\Exceptions\DatasetArgumentsMismatch;
|
||||
use Pest\Panic;
|
||||
use Pest\Preset;
|
||||
use Pest\Support\ChainableClosure;
|
||||
use Pest\Support\ExceptionTrace;
|
||||
use Pest\Support\Reflection;
|
||||
use Pest\Support\Shell;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\Attributes\PostCondition;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@ -101,27 +103,6 @@ trait Testable
|
||||
*/
|
||||
private array $__snapshotChanges = [];
|
||||
|
||||
/**
|
||||
* Creates a new Test Case instance.
|
||||
*/
|
||||
public function __construct(string $name)
|
||||
{
|
||||
parent::__construct($name);
|
||||
|
||||
$test = TestSuite::getInstance()->tests->get(self::$__filename);
|
||||
|
||||
if ($test->hasMethod($name)) {
|
||||
$method = $test->getMethod($name);
|
||||
$this->__description = self::$__latestDescription = $method->description;
|
||||
self::$__latestAssignees = $method->assignees;
|
||||
self::$__latestNotes = $method->notes;
|
||||
self::$__latestIssues = $method->issues;
|
||||
self::$__latestPrs = $method->prs;
|
||||
$this->__describing = $method->describing;
|
||||
$this->__test = $method->getClosure();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the test case static properties.
|
||||
*/
|
||||
@ -214,7 +195,11 @@ trait Testable
|
||||
$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->setUp($this);
|
||||
|
||||
$description = $method->description;
|
||||
if ($this->dataName()) {
|
||||
$description = str_contains((string) $description, ':dataset')
|
||||
@ -285,6 +268,33 @@ trait Testable
|
||||
$this->__callClosure($beforeEach, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize test case properties from TestSuite.
|
||||
*/
|
||||
public function __initializeTestCase(): void
|
||||
{
|
||||
// Return if the test case has already been initialized
|
||||
if (isset($this->__test)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $this->name();
|
||||
$test = TestSuite::getInstance()->tests->get(self::$__filename);
|
||||
|
||||
if ($test->hasMethod($name)) {
|
||||
$method = $test->getMethod($name);
|
||||
$this->__description = self::$__latestDescription = $method->description;
|
||||
self::$__latestAssignees = $method->assignees;
|
||||
self::$__latestNotes = $method->notes;
|
||||
self::$__latestIssues = $method->issues;
|
||||
self::$__latestPrs = $method->prs;
|
||||
$this->__describing = $method->describing;
|
||||
$this->__test = $method->getClosure();
|
||||
|
||||
$method->setUp($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets executed after the Test Case.
|
||||
*/
|
||||
@ -434,15 +444,7 @@ trait Testable
|
||||
return;
|
||||
}
|
||||
|
||||
if (count($this->__snapshotChanges) === 1) {
|
||||
$this->markTestIncomplete($this->__snapshotChanges[0]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$messages = implode(PHP_EOL, array_map(static fn (string $message): string => '- $message', $this->__snapshotChanges));
|
||||
|
||||
$this->markTestIncomplete($messages);
|
||||
$this->markTestIncomplete(implode('. ', $this->__snapshotChanges));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -466,7 +468,7 @@ trait Testable
|
||||
*/
|
||||
public static function getLatestPrintableTestCaseMethodName(): string
|
||||
{
|
||||
return self::$__latestDescription;
|
||||
return self::$__latestDescription ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -481,4 +483,12 @@ trait Testable
|
||||
'notes' => self::$__latestNotes,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a shell for the test case.
|
||||
*/
|
||||
public function shell(): void
|
||||
{
|
||||
Shell::open();
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +102,14 @@ final readonly class Configuration
|
||||
return Configuration\Project::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the browser configuration.
|
||||
*/
|
||||
public function browser(): Browser\Configuration
|
||||
{
|
||||
return new Browser\Configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies calls to the uses method.
|
||||
*
|
||||
|
||||
@ -16,7 +16,7 @@ final readonly class Help
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const HELP_MESSAGES = [
|
||||
private const array HELP_MESSAGES = [
|
||||
'<comment>Pest Options:</comment>',
|
||||
' <info>--init</info> Initialise a standard Pest configuration',
|
||||
' <info>--coverage</info> Enable coverage and output to standard output',
|
||||
|
||||
@ -22,10 +22,14 @@ final readonly class Thanks
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private const FUNDING_MESSAGES = [
|
||||
private const array FUNDING_MESSAGES = [
|
||||
'Star' => 'https://github.com/pestphp/pest',
|
||||
'News' => 'https://twitter.com/pestphp',
|
||||
'Videos' => 'https://youtube.com/@nunomaduro',
|
||||
'YouTube' => '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',
|
||||
];
|
||||
|
||||
|
||||
@ -535,7 +535,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$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',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -548,7 +548,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$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',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -561,7 +561,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$this,
|
||||
fn (ObjectDescription $object): bool => $object->reflectionClass->isTrait(),
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isTrait(),
|
||||
'to be trait',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -582,7 +582,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$this,
|
||||
fn (ObjectDescription $object): bool => $object->reflectionClass->isAbstract(),
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isAbstract(),
|
||||
'to be abstract',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -599,7 +599,7 @@ final class Expectation
|
||||
|
||||
return Targeted::make(
|
||||
$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)),
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -670,7 +670,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$this,
|
||||
fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum(),
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isEnum(),
|
||||
'to be enum',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -712,7 +712,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$this,
|
||||
fn (ObjectDescription $object): bool => $object->reflectionClass->isInterface(),
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isInterface(),
|
||||
'to be interface',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -733,7 +733,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$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),
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -773,6 +773,10 @@ final class Expectation
|
||||
$this,
|
||||
function (ObjectDescription $object) use ($traits): bool {
|
||||
foreach ($traits as $trait) {
|
||||
if (isset($object->reflectionClass) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
|
||||
return false;
|
||||
}
|
||||
@ -792,7 +796,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$this,
|
||||
fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() === [],
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getInterfaceNames() === [],
|
||||
'to implement nothing',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -809,7 +813,8 @@ final class Expectation
|
||||
|
||||
return Targeted::make(
|
||||
$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()) === [],
|
||||
"to only implement '".implode("', '", $interfaces)."'",
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
@ -823,7 +828,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$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}'",
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -836,7 +841,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$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}'",
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -855,7 +860,7 @@ final class Expectation
|
||||
$this,
|
||||
function (ObjectDescription $object) use ($interfaces): bool {
|
||||
foreach ($interfaces as $interface) {
|
||||
if (! $object->reflectionClass->implementsInterface($interface)) {
|
||||
if (! isset($object->reflectionClass) || ! $object->reflectionClass->implementsInterface($interface)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -928,7 +933,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$this,
|
||||
fn (ObjectDescription $object): bool => $object->reflectionClass->hasMethod('__invoke'),
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->hasMethod('__invoke'),
|
||||
'to be invokable',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
|
||||
);
|
||||
@ -1037,7 +1042,7 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$this,
|
||||
fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) !== [],
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getAttributes($attribute) !== [],
|
||||
"to have attribute '{$attribute}'",
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -1066,7 +1071,8 @@ final class Expectation
|
||||
{
|
||||
return Targeted::make(
|
||||
$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
|
||||
&& (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line
|
||||
'to be '.$backingType.' backed enum',
|
||||
|
||||
@ -74,7 +74,10 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
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),
|
||||
), is_string($targets) ? [$targets] : $targets));
|
||||
}
|
||||
@ -84,8 +87,11 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toHaveFileSystemPermissions(string $permissions): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) !== $permissions,
|
||||
sprintf('permissions not to be [%s]', $permissions),
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
|
||||
@ -105,8 +111,11 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toHaveMethodsDocumented(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|
||||
|| array_filter(
|
||||
Reflection::getMethodsFromReflectionClass($object->reflectionClass),
|
||||
@ -124,8 +133,11 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toHavePropertiesDocumented(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|
||||
|| array_filter(
|
||||
Reflection::getPropertiesFromReflectionClass($object->reflectionClass),
|
||||
@ -144,8 +156,11 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toUseStrictTypes(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
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)),
|
||||
'not to use strict types',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
|
||||
@ -157,8 +172,11 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toUseStrictEquality(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
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), ' !== '),
|
||||
'to use strict equality',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' === ') || str_contains($line, ' !== ')),
|
||||
@ -170,9 +188,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toBeFinal(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isFinal(),
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && (isset($object->reflectionClass) === false || ! $object->reflectionClass->isFinal()),
|
||||
'not to be final',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -183,9 +204,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toBeReadonly(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && (isset($object->reflectionClass) === false || ! $object->reflectionClass->isReadOnly()) && assert(true), // @phpstan-ignore-line
|
||||
'not to be readonly',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -196,9 +220,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toBeTrait(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isTrait(),
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isTrait(),
|
||||
'not to be trait',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -217,9 +244,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toBeAbstract(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isAbstract(),
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isAbstract(),
|
||||
'not to be abstract',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -234,11 +264,14 @@ final readonly class OppositeExpectation
|
||||
{
|
||||
$methods = is_array($method) ? $method : [$method];
|
||||
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => array_filter(
|
||||
$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),
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
@ -266,8 +299,11 @@ final readonly class OppositeExpectation
|
||||
|
||||
$state = new stdClass;
|
||||
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
$original,
|
||||
function (ObjectDescription $object) use ($methods, &$state): bool {
|
||||
$reflectionMethods = isset($object->reflectionClass)
|
||||
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PUBLIC)
|
||||
@ -309,8 +345,11 @@ final readonly class OppositeExpectation
|
||||
|
||||
$state = new stdClass;
|
||||
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
$original,
|
||||
function (ObjectDescription $object) use ($methods, &$state): bool {
|
||||
$reflectionMethods = isset($object->reflectionClass)
|
||||
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PROTECTED)
|
||||
@ -352,8 +391,11 @@ final readonly class OppositeExpectation
|
||||
|
||||
$state = new stdClass;
|
||||
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
$original,
|
||||
function (ObjectDescription $object) use ($methods, &$state): bool {
|
||||
$reflectionMethods = isset($object->reflectionClass)
|
||||
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PRIVATE)
|
||||
@ -389,9 +431,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toBeEnum(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum(),
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isEnum(),
|
||||
'not to be enum',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -410,8 +455,11 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toBeClass(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => ! class_exists($object->name),
|
||||
'not to be class',
|
||||
FileLineFinder::where(fn (string $line): bool => true),
|
||||
@ -431,9 +479,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toBeInterface(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isInterface(),
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isInterface(),
|
||||
'not to be interface',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -452,9 +503,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toExtend(string $class): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isSubclassOf($class),
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isSubclassOf($class),
|
||||
sprintf("not to extend '%s'", $class),
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -465,9 +519,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toExtendNothing(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() !== false,
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getParentClass() !== false,
|
||||
'to extend a 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];
|
||||
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
$original,
|
||||
function (ObjectDescription $object) use ($traits): bool {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -515,11 +575,14 @@ final readonly class OppositeExpectation
|
||||
{
|
||||
$interfaces = is_array($interfaces) ? $interfaces : [$interfaces];
|
||||
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
$original,
|
||||
function (ObjectDescription $object) use ($interfaces): bool {
|
||||
foreach ($interfaces as $interface) {
|
||||
if ($object->reflectionClass->implementsInterface($interface)) {
|
||||
if (isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -536,9 +599,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toImplementNothing(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() !== [],
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getInterfaceNames() !== [],
|
||||
'to implement an interface',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -557,9 +623,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toHavePrefix(string $prefix): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => ! str_starts_with($object->reflectionClass->getShortName(), $prefix),
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_starts_with($object->reflectionClass->getShortName(), $prefix),
|
||||
"not to have prefix '{$prefix}'",
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -570,9 +639,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toHaveSuffix(string $suffix): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => ! str_ends_with($object->reflectionClass->getName(), $suffix),
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_ends_with($object->reflectionClass->getName(), $suffix),
|
||||
"not to have suffix '{$suffix}'",
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||
);
|
||||
@ -599,7 +671,10 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
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
|
||||
{
|
||||
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),
|
||||
), is_string($targets) ? [$targets] : $targets));
|
||||
}
|
||||
@ -632,9 +710,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toBeInvokable(): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod('__invoke'),
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->hasMethod('__invoke'),
|
||||
'to not be invokable',
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
|
||||
);
|
||||
@ -645,9 +726,12 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
public function toHaveAttribute(string $attribute): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) === [],
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getAttributes($attribute) === [],
|
||||
"to not have attribute '{$attribute}'",
|
||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
|
||||
);
|
||||
@ -737,9 +821,13 @@ final readonly class OppositeExpectation
|
||||
*/
|
||||
private function toBeBackedEnum(string $backingType): ArchExpectation
|
||||
{
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$this->original,
|
||||
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum()
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|
||||
|| ! $object->reflectionClass->isEnum()
|
||||
|| ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
|
||||
|| (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line
|
||||
'not to be '.$backingType.' backed enum',
|
||||
|
||||
@ -155,7 +155,7 @@ final class TestCaseMethodFactory
|
||||
assert($testCase instanceof TestCaseFactory);
|
||||
$method = $this;
|
||||
|
||||
return function (...$arguments) use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line
|
||||
return function (...$arguments) use ($testCase, $method, $closure): mixed {
|
||||
/* @var TestCase $this */
|
||||
$testCase->proxies->proxy($this);
|
||||
$method->proxies->proxy($this);
|
||||
|
||||
@ -2,11 +2,14 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Pest\Browser\Api\ArrayablePendingAwaitablePage;
|
||||
use Pest\Browser\Api\PendingAwaitablePage;
|
||||
use Pest\Concerns\Expectable;
|
||||
use Pest\Configuration;
|
||||
use Pest\Exceptions\AfterAllWithinDescribe;
|
||||
use Pest\Exceptions\BeforeAllWithinDescribe;
|
||||
use Pest\Expectation;
|
||||
use Pest\Installers\PluginBrowser;
|
||||
use Pest\Mutate\Contracts\MutationTestRunner;
|
||||
use Pest\Mutate\Repositories\ConfigurationRepository;
|
||||
use Pest\PendingCalls\AfterEachCall;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
15
src/Installers/PluginBrowser.php
Normal file
15
src/Installers/PluginBrowser.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,7 @@ final readonly class Kernel
|
||||
*
|
||||
* @var array<int, class-string>
|
||||
*/
|
||||
private const BOOTSTRAPPERS = [
|
||||
private const array BOOTSTRAPPERS = [
|
||||
Bootstrappers\BootOverrides::class,
|
||||
Bootstrappers\BootSubscribers::class,
|
||||
Bootstrappers\BootFiles::class,
|
||||
|
||||
@ -31,7 +31,7 @@ final readonly class Converter
|
||||
/**
|
||||
* The prefix for the test suite name.
|
||||
*/
|
||||
private const PREFIX = 'P\\';
|
||||
private const string PREFIX = 'P\\';
|
||||
|
||||
/**
|
||||
* The state generator.
|
||||
|
||||
@ -183,7 +183,6 @@ final class Expectation
|
||||
{
|
||||
foreach ($needles as $needle) {
|
||||
if (is_string($this->value)) {
|
||||
// @phpstan-ignore-next-line
|
||||
Assert::assertStringContainsString((string) $needle, $this->value);
|
||||
} else {
|
||||
if (! is_iterable($this->value)) {
|
||||
@ -1159,4 +1158,21 @@ final class Expectation
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value can be converted to a slug
|
||||
*
|
||||
* @return self<TValue>
|
||||
*/
|
||||
public function toBeSlug(string $message = ''): self
|
||||
{
|
||||
if ($message === '') {
|
||||
$message = "Failed asserting that {$this->value} can be converted to a slug.";
|
||||
}
|
||||
|
||||
$slug = Str::slugify((string) $this->value);
|
||||
Assert::assertNotEmpty($slug, $message);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ final readonly class Panic
|
||||
{
|
||||
try {
|
||||
$output = Container::getInstance()->get(OutputInterface::class);
|
||||
} catch (Throwable) { // @phpstan-ignore-line
|
||||
} catch (Throwable) {
|
||||
$output = new ConsoleOutput;
|
||||
}
|
||||
|
||||
|
||||
@ -78,7 +78,7 @@ final class DescribeCall
|
||||
$this->currentBeforeEachCall->describing[] = $this->description;
|
||||
}
|
||||
|
||||
$this->currentBeforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line
|
||||
$this->currentBeforeEachCall->{$name}(...$arguments);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ use Pest\Factories\Attribute;
|
||||
use Pest\Factories\TestCaseMethodFactory;
|
||||
use Pest\Mutate\Repositories\ConfigurationRepository;
|
||||
use Pest\PendingCalls\Concerns\Describable;
|
||||
use Pest\Plugins\Environment;
|
||||
use Pest\Plugins\Only;
|
||||
use Pest\Support\Backtrace;
|
||||
use Pest\Support\Container;
|
||||
@ -178,10 +179,9 @@ final class TestCall // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the current test multiple times with
|
||||
* each item of the given `iterable`.
|
||||
* Runs the current test multiple times with each item of the given `iterable`.
|
||||
*
|
||||
* @param array<\Closure|iterable<int|string, mixed>|string> $data
|
||||
* @param Closure|iterable<array-key, mixed>|string $data
|
||||
*/
|
||||
public function with(Closure|iterable|string ...$data): self
|
||||
{
|
||||
@ -224,7 +224,7 @@ final class TestCall // @phpstan-ignore-line
|
||||
*/
|
||||
public function only(): self
|
||||
{
|
||||
Only::enable($this, ...func_get_args()); // @phpstan-ignore-line
|
||||
Only::enable($this, ...func_get_args());
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -315,6 +315,61 @@ final class TestCall // @phpstan-ignore-line
|
||||
: $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Weather the current test is running on a CI environment.
|
||||
*/
|
||||
private function runningOnCI(): bool
|
||||
{
|
||||
foreach ([
|
||||
'CI',
|
||||
'GITHUB_ACTIONS',
|
||||
'GITLAB_CI',
|
||||
'CIRCLECI',
|
||||
'TRAVIS',
|
||||
'APPVEYOR',
|
||||
'BITBUCKET_BUILD_NUMBER',
|
||||
'BUILDKITE',
|
||||
'TEAMCITY_VERSION',
|
||||
'JENKINS_URL',
|
||||
'SYSTEM_COLLECTIONURI',
|
||||
'CI_NAME',
|
||||
'TASKCLUSTER_ROOT_URL',
|
||||
'DRONE',
|
||||
'WERCKER',
|
||||
'NEVERCODE',
|
||||
'SEMAPHORE',
|
||||
'NETLIFY',
|
||||
'NOW_BUILDER',
|
||||
] as $env) {
|
||||
if (getenv($env) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return Environment::name() === Environment::CI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the current test when running on a CI environments.
|
||||
*/
|
||||
public function skipOnCI(): self
|
||||
{
|
||||
if ($this->runningOnCI()) {
|
||||
return $this->skip('This test is skipped on [CI].');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function skipLocally(): self
|
||||
{
|
||||
if ($this->runningOnCI() === false) {
|
||||
return $this->skip('This test is skipped [locally].');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the current test unless the given test is running on Windows.
|
||||
*/
|
||||
@ -616,6 +671,30 @@ final class TestCall // @phpstan-ignore-line
|
||||
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,
|
||||
* and its purpose is simply to check whether the given code can
|
||||
|
||||
@ -6,7 +6,7 @@ namespace Pest;
|
||||
|
||||
function version(): string
|
||||
{
|
||||
return '3.7.4';
|
||||
return '4.0.0-alpha.6';
|
||||
}
|
||||
|
||||
function testDirectory(string $file = ''): string
|
||||
|
||||
@ -21,7 +21,7 @@ final class Cache implements HandlesArguments
|
||||
/**
|
||||
* The temporary folder.
|
||||
*/
|
||||
private const TEMPORARY_FOLDER = __DIR__
|
||||
private const string TEMPORARY_FOLDER = __DIR__
|
||||
.DIRECTORY_SEPARATOR
|
||||
.'..'
|
||||
.DIRECTORY_SEPARATOR
|
||||
|
||||
@ -21,7 +21,7 @@ final class Configuration implements HandlesArguments, Terminable
|
||||
/**
|
||||
* The base PHPUnit file.
|
||||
*/
|
||||
public const BASE_PHPUNIT_FILE = __DIR__
|
||||
public const string BASE_PHPUNIT_FILE = __DIR__
|
||||
.DIRECTORY_SEPARATOR
|
||||
.'..'
|
||||
.DIRECTORY_SEPARATOR
|
||||
@ -34,7 +34,7 @@ final class Configuration implements HandlesArguments, Terminable
|
||||
*/
|
||||
public function handleArguments(array $arguments): array
|
||||
{
|
||||
if ($this->hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) {
|
||||
if ($this->hasArgument('--configuration', $arguments) || $this->hasArgument('-c', $arguments) || $this->hasCustomConfigurationFile()) {
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
|
||||
@ -17,20 +17,11 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
*/
|
||||
final class Coverage implements AddsOutput, HandlesArguments
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const COVERAGE_OPTION = 'coverage';
|
||||
private const string COVERAGE_OPTION = 'coverage';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const MIN_OPTION = 'min';
|
||||
private const string MIN_OPTION = 'min';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const EXACTLY_OPTION = 'exactly';
|
||||
private const string EXACTLY_OPTION = 'exactly';
|
||||
|
||||
/**
|
||||
* Whether it should show the coverage or not.
|
||||
|
||||
@ -14,12 +14,12 @@ final class Environment implements HandlesArguments
|
||||
/**
|
||||
* The continuous integration environment.
|
||||
*/
|
||||
public const CI = 'ci';
|
||||
public const string CI = 'ci';
|
||||
|
||||
/**
|
||||
* The local environment.
|
||||
*/
|
||||
public const LOCAL = 'local';
|
||||
public const string LOCAL = 'local';
|
||||
|
||||
/**
|
||||
* The current environment.
|
||||
|
||||
@ -20,12 +20,12 @@ final readonly class Init implements HandlesArguments
|
||||
/**
|
||||
* The option the triggers the init job.
|
||||
*/
|
||||
private const INIT_OPTION = '--init';
|
||||
private const string INIT_OPTION = '--init';
|
||||
|
||||
/**
|
||||
* The files that will be created.
|
||||
*/
|
||||
private const STUBS = [
|
||||
private const array STUBS = [
|
||||
'phpunit.xml.stub' => 'phpunit.xml',
|
||||
'Pest.php.stub' => 'tests/Pest.php',
|
||||
'TestCase.php.stub' => 'tests/TestCase.php',
|
||||
@ -119,6 +119,6 @@ final readonly class Init implements HandlesArguments
|
||||
*/
|
||||
private function isLaravelInstalled(): bool
|
||||
{
|
||||
return InstalledVersions::isInstalled('laravel/laravel');
|
||||
return InstalledVersions::isInstalled('laravel/framework');
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,10 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins;
|
||||
|
||||
use Pest\Contracts\Plugins\Terminable;
|
||||
use Pest\Factories\Attribute;
|
||||
use Pest\Factories\TestCaseMethodFactory;
|
||||
use Pest\PendingCalls\TestCall;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -15,7 +18,7 @@ final class Only implements Terminable
|
||||
/**
|
||||
* The temporary folder.
|
||||
*/
|
||||
private const TEMPORARY_FOLDER = __DIR__
|
||||
private const string TEMPORARY_FOLDER = __DIR__
|
||||
.DIRECTORY_SEPARATOR
|
||||
.'..'
|
||||
.DIRECTORY_SEPARATOR
|
||||
@ -23,28 +26,19 @@ final class Only implements Terminable
|
||||
.DIRECTORY_SEPARATOR
|
||||
.'.temp';
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function terminate(): void
|
||||
{
|
||||
if (Parallel::isWorker()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
|
||||
|
||||
if (file_exists($lockFile)) {
|
||||
unlink($lockFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the lock file.
|
||||
*/
|
||||
public static function enable(TestCall $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()) {
|
||||
return;
|
||||
@ -88,4 +82,20 @@ final class Only implements Terminable
|
||||
|
||||
return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function terminate(): void
|
||||
{
|
||||
if (Parallel::isWorker()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
|
||||
|
||||
if (file_exists($lockFile)) {
|
||||
unlink($lockFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,9 +23,9 @@ final class Parallel implements HandlesArguments
|
||||
{
|
||||
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\Pest::class,
|
||||
Parallel\Handlers\Laravel::class,
|
||||
@ -34,7 +34,7 @@ final class Parallel implements HandlesArguments
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request'];
|
||||
private const 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.
|
||||
|
||||
@ -18,7 +18,7 @@ final class Parallel implements HandlesArguments
|
||||
/**
|
||||
* The list of arguments to remove.
|
||||
*/
|
||||
private const ARGS_TO_REMOVE = [
|
||||
private const array ARGS_TO_REMOVE = [
|
||||
'--parallel',
|
||||
'-p',
|
||||
'--no-output',
|
||||
|
||||
@ -11,6 +11,7 @@ final class CleanConsoleOutput extends ConsoleOutput
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
#[\Override]
|
||||
protected function doWrite(string $message, bool $newline): void // @pest-arch-ignore-line
|
||||
{
|
||||
if ($this->isOpeningHeadline($message)) {
|
||||
|
||||
@ -59,10 +59,10 @@ final class ResultPrinter
|
||||
private readonly OutputInterface $output,
|
||||
private readonly Options $options
|
||||
) {
|
||||
$this->printer = new class($this->output) implements Printer
|
||||
$this->printer = new readonly class($this->output) implements Printer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OutputInterface $output,
|
||||
private OutputInterface $output,
|
||||
) {}
|
||||
|
||||
public function print(string $buffer): void
|
||||
|
||||
@ -17,6 +17,7 @@ use ParaTest\WrapperRunner\WrapperWorker;
|
||||
use Pest\Result;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Event\Facade as EventFacade;
|
||||
use PHPUnit\Event\Test\AfterLastTestMethodFailed;
|
||||
use PHPUnit\Event\TestRunner\WarningTriggered;
|
||||
use PHPUnit\Runner\CodeCoverage;
|
||||
use PHPUnit\Runner\ResultCache\DefaultResultCache;
|
||||
@ -50,7 +51,7 @@ final class WrapperRunner implements RunnerInterface
|
||||
/**
|
||||
* The time to sleep between cycles.
|
||||
*/
|
||||
private const CYCLE_SLEEP = 10000;
|
||||
private const int CYCLE_SLEEP = 10000;
|
||||
|
||||
/**
|
||||
* The result printer.
|
||||
@ -313,27 +314,42 @@ final class WrapperRunner implements RunnerInterface
|
||||
$testResult = unserialize($contents);
|
||||
assert($testResult instanceof TestResult);
|
||||
|
||||
/** @var list<AfterLastTestMethodFailed> $failedEvents */
|
||||
$failedEvents = array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents());
|
||||
|
||||
$testResultSum = new TestResult(
|
||||
(int) $testResultSum->hasTests() + (int) $testResult->hasTests(),
|
||||
$testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
|
||||
$testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(),
|
||||
array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()),
|
||||
array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()),
|
||||
$failedEvents,
|
||||
array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()),
|
||||
array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()),
|
||||
array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()),
|
||||
array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->testRunnerTriggeredNoticeEvents(), $testResult->testRunnerTriggeredNoticeEvents()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->errors(), $testResult->errors()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->notices(), $testResult->notices()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->warnings(), $testResult->warnings()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()),
|
||||
$testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
|
||||
);
|
||||
@ -351,8 +367,10 @@ final class WrapperRunner implements RunnerInterface
|
||||
$testResultSum->testMarkedIncompleteEvents(),
|
||||
$testResultSum->testTriggeredPhpunitDeprecationEvents(),
|
||||
$testResultSum->testTriggeredPhpunitErrorEvents(),
|
||||
$testResultSum->testTriggeredPhpunitNoticeEvents(),
|
||||
$testResultSum->testTriggeredPhpunitWarningEvents(),
|
||||
$testResultSum->testRunnerTriggeredDeprecationEvents(),
|
||||
$testResultSum->testRunnerTriggeredNoticeEvents(),
|
||||
array_values(array_filter(
|
||||
$testResultSum->testRunnerTriggeredWarningEvents(),
|
||||
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')
|
||||
|
||||
@ -34,7 +34,7 @@ final class CompactPrinter
|
||||
/**
|
||||
* @var array<string, array<int, string>>
|
||||
*/
|
||||
private const LOOKUP_TABLE = [
|
||||
private const array LOOKUP_TABLE = [
|
||||
'.' => ['gray', '.'],
|
||||
'S' => ['yellow', 's'],
|
||||
'T' => ['cyan', 't'],
|
||||
@ -131,14 +131,14 @@ final class CompactPrinter
|
||||
$status['collected'],
|
||||
$status['threshold'],
|
||||
$status['roots'],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0.00,
|
||||
0.00,
|
||||
0.00,
|
||||
0.00,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
);
|
||||
|
||||
$telemetry = new Info(
|
||||
|
||||
177
src/Plugins/Shard.php
Normal file
177
src/Plugins/Shard.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,7 @@ final class Verbose implements HandlesArguments
|
||||
/**
|
||||
* The list of verbosity levels.
|
||||
*/
|
||||
private const VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
|
||||
private const array VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
|
||||
@ -19,7 +19,7 @@ use function sprintf;
|
||||
*/
|
||||
final class DatasetsRepository
|
||||
{
|
||||
private const SEPARATOR = '>>';
|
||||
private const string SEPARATOR = '>>';
|
||||
|
||||
/**
|
||||
* Holds the datasets.
|
||||
@ -71,7 +71,7 @@ final class DatasetsRepository
|
||||
*
|
||||
* @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];
|
||||
|
||||
@ -110,7 +110,6 @@ final class DatasetsRepository
|
||||
foreach ($datasetCombination as $datasetCombinationElement) {
|
||||
$partialDescriptions[] = $datasetCombinationElement['label'];
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$values = array_merge($values, $datasetCombinationElement['values']);
|
||||
}
|
||||
|
||||
@ -221,7 +220,6 @@ final class DatasetsRepository
|
||||
$result = $tmp;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
@ -19,9 +19,9 @@ final class SnapshotRepository
|
||||
* Creates a snapshot repository instance.
|
||||
*/
|
||||
public function __construct(
|
||||
readonly private string $rootPath,
|
||||
readonly private string $testsPath,
|
||||
readonly private string $snapshotsPath,
|
||||
private readonly string $rootPath,
|
||||
private readonly string $testsPath,
|
||||
private readonly string $snapshotsPath,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@ -4,20 +4,16 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest;
|
||||
|
||||
use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection;
|
||||
use PHPUnit\TestRunner\TestResult\TestResult;
|
||||
use PHPUnit\TextUI\Configuration\Configuration;
|
||||
use PHPUnit\TextUI\ShellExitCodeCalculator;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Result
|
||||
{
|
||||
private const SUCCESS_EXIT = 0;
|
||||
|
||||
private const FAILURE_EXIT = 1;
|
||||
|
||||
private const EXCEPTION_EXIT = 2;
|
||||
private const int SUCCESS_EXIT = 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
|
||||
{
|
||||
if ($result->wasSuccessfulIgnoringPhpunitWarnings()) {
|
||||
if ($configuration->failOnWarning()) {
|
||||
$warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents()
|
||||
+ count($result->warnings())
|
||||
+ count($result->phpWarnings());
|
||||
$shell = new ShellExitCodeCalculator;
|
||||
|
||||
if ($warnings > 0) {
|
||||
return self::FAILURE_EXIT;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $result->hasTestTriggeredPhpunitWarningEvents()) {
|
||||
return self::SUCCESS_EXIT;
|
||||
}
|
||||
}
|
||||
|
||||
if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) {
|
||||
return self::FAILURE_EXIT;
|
||||
}
|
||||
|
||||
if ($result->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;
|
||||
return $shell->calculate($configuration, $result);
|
||||
}
|
||||
}
|
||||
|
||||
39
src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php
Normal file
39
src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -35,7 +35,7 @@ final class EnsureIgnorableTestCasesAreIgnored implements StartedSubscriber
|
||||
/** @var array<int, WarningTriggered> $testRunnerTriggeredWarningEvents */
|
||||
$testRunnerTriggeredWarningEvents = $property->getValue($collector);
|
||||
|
||||
$testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => $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);
|
||||
}
|
||||
|
||||
@ -11,12 +11,9 @@ use Pest\Exceptions\ShouldNotHappen;
|
||||
*/
|
||||
final class Backtrace
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const FILE = 'file';
|
||||
private const string FILE = 'file';
|
||||
|
||||
private const BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS;
|
||||
private const int BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS;
|
||||
|
||||
/**
|
||||
* Returns the current test file.
|
||||
|
||||
@ -15,7 +15,6 @@ final class Closure
|
||||
/**
|
||||
* Binds the given closure to the given "this".
|
||||
*
|
||||
*
|
||||
* @throws ShouldNotHappen
|
||||
*/
|
||||
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.');
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$closure = BaseClosure::bind($closure, $newThis, $newScope);
|
||||
|
||||
if (! $closure instanceof \Closure) {
|
||||
|
||||
@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Pest\Support;
|
||||
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Node\Directory;
|
||||
use SebastianBergmann\CodeCoverage\Node\File;
|
||||
use SebastianBergmann\Environment\Runtime;
|
||||
@ -88,10 +87,20 @@ final class Coverage
|
||||
throw ShouldNotHappen::fromMessage(sprintf('Coverage not found in path: %s.', $reportPath));
|
||||
}
|
||||
|
||||
/** @var CodeCoverage $codeCoverage */
|
||||
$codeCoverage = require $reportPath;
|
||||
$handle = fopen($reportPath, 'r');
|
||||
$code = '';
|
||||
while (is_resource($handle) && ! feof($handle)) {
|
||||
$code .= fread($handle, 8192);
|
||||
}
|
||||
|
||||
if (is_resource($handle)) {
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
unlink($reportPath);
|
||||
|
||||
$codeCoverage = eval(substr($code, 5));
|
||||
|
||||
$totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines();
|
||||
|
||||
/** @var Directory<File|Directory> $report */
|
||||
|
||||
@ -11,9 +11,9 @@ use function Pest\testDirectory;
|
||||
*/
|
||||
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
|
||||
{
|
||||
|
||||
@ -13,7 +13,7 @@ use Throwable;
|
||||
*/
|
||||
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.
|
||||
|
||||
@ -15,7 +15,7 @@ final readonly class Exporter
|
||||
/**
|
||||
* 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.
|
||||
@ -66,6 +66,7 @@ final readonly class Exporter
|
||||
|
||||
$result[] = $context->contains($data[$key]) !== false
|
||||
? '*RECURSION*'
|
||||
// @phpstan-ignore-next-line
|
||||
: sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context));
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ use Throwable;
|
||||
*/
|
||||
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.
|
||||
@ -50,14 +50,13 @@ final class HigherOrderMessage
|
||||
}
|
||||
|
||||
if ($this->hasHigherOrderCallable()) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments);
|
||||
}
|
||||
|
||||
try {
|
||||
return is_array($this->arguments)
|
||||
? Reflection::call($target, $this->name, $this->arguments)
|
||||
: $target->{$this->name}; /* @phpstan-ignore-line */
|
||||
: $target->{$this->name};
|
||||
} catch (Throwable $throwable) {
|
||||
Reflection::setPropertyValue($throwable, 'file', $this->filename);
|
||||
Reflection::setPropertyValue($throwable, 'line', $this->line);
|
||||
@ -65,7 +64,6 @@ final class HigherOrderMessage
|
||||
if ($throwable->getMessage() === $this->getUndefinedMethodMessage($target, $this->name)) {
|
||||
/** @var ReflectionClass<TValue> $reflection */
|
||||
$reflection = new ReflectionClass($target);
|
||||
/* @phpstan-ignore-next-line */
|
||||
$reflection = $reflection->getParentClass() ?: $reflection;
|
||||
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
|
||||
{
|
||||
if (\PHP_MAJOR_VERSION >= 8) {
|
||||
return sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName));
|
||||
}
|
||||
|
||||
return sprintf(self::UNDEFINED_METHOD, $methodName);
|
||||
return sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName));
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,7 +40,6 @@ final class HigherOrderMessageCollection
|
||||
public function chain(object $target): void
|
||||
{
|
||||
foreach ($this->messages as $message) {
|
||||
// @phpstan-ignore-next-line
|
||||
$target = $message->call($target) ?? $target;
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ final class HigherOrderTapProxy
|
||||
*/
|
||||
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)
|
||||
{
|
||||
if (property_exists($this->target, $property)) {
|
||||
return $this->target->{$property}; // @phpstan-ignore-line
|
||||
return $this->target->{$property};
|
||||
}
|
||||
|
||||
$className = (new ReflectionClass($this->target))->getName();
|
||||
|
||||
101
src/Support/Shell.php
Normal file
101
src/Support/Shell.php
Normal 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', [])
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -13,12 +13,9 @@ final class Str
|
||||
* Pool of alpha-numeric characters for generating (unsafe) random strings
|
||||
* from.
|
||||
*/
|
||||
private const POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
private const string POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const PREFIX = '__pest_evaluable_';
|
||||
private const string PREFIX = '__pest_evaluable_';
|
||||
|
||||
/**
|
||||
* Create a (unsecure & non-cryptographically safe) random alpha-numeric
|
||||
@ -120,4 +117,14 @@ final class Str
|
||||
{
|
||||
return (bool) filter_var($value, FILTER_VALIDATE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given `$target` to a URL-friendly "slug".
|
||||
*/
|
||||
public static function slugify(string $target): string
|
||||
{
|
||||
$target = preg_replace('/[^a-zA-Z0-9]+/', '-', $target);
|
||||
|
||||
return strtolower(trim((string) $target, '-'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
pest()->extend(Tests\TestCase::class)
|
||||
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
// ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
->in('Feature');
|
||||
|
||||
/*
|
||||
|
||||
@ -1,31 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests/Unit</directory>
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory suffix="Test.php">./tests/Feature</directory>
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
|
||||
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
|
||||
*/
|
||||
|
||||
// pest()->extend(Tests\TestCase::class)->in('Feature');
|
||||
pest()->extend(Tests\TestCase::class)->in('Feature');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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"
|
||||
colors="true"
|
||||
>
|
||||
@ -11,8 +11,8 @@
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
<directory suffix=".php">./src</directory>
|
||||
<directory>app</directory>
|
||||
<directory>src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
|
||||
Pest Testing Framework 3.7.4.
|
||||
Pest Testing Framework 4.0.0-alpha.6.
|
||||
|
||||
USAGE: pest <file> [options]
|
||||
|
||||
@ -53,7 +53,7 @@
|
||||
--disallow-test-output ................. Be strict about output during tests
|
||||
--enforce-time-limit ................. Enforce time limit based on test 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-error ..................................... Stop after first error
|
||||
--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-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-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-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-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
|
||||
--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
|
||||
@ -88,18 +101,23 @@
|
||||
--display-skipped ........................ Display details for skipped tests
|
||||
--display-deprecations . Display details for deprecations triggered by tests
|
||||
--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-notices ........... Display details for notices 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
|
||||
--teamcity . Replace default progress and result output with TeamCity format
|
||||
--testdox ................ Replace default result output with TestDox format
|
||||
--testdox-summary Repeat TestDox output for tests with errors, failures, or issues
|
||||
--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
|
||||
|
||||
LOGGING OPTIONS:
|
||||
--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
|
||||
--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
|
||||
@ -111,6 +129,7 @@
|
||||
--coverage ..... Generate code coverage report and output to standard output
|
||||
--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-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-crap4j [file] Write code coverage report in Crap4J XML format to file
|
||||
--coverage-html [dir] Write code coverage report in HTML format to directory
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
|
||||
Pest Testing Framework 3.7.4.
|
||||
Pest Testing Framework 4.0.0-alpha.6.
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
✓ preset → strict → ignoring ['usleep']
|
||||
✓ preset → security → ignoring ['eval', 'str_shuffle', 'exec', …]
|
||||
✓ globals
|
||||
✓ dependencies
|
||||
✓ contracts
|
||||
|
||||
PASS Tests\Environments\Windows
|
||||
@ -68,14 +67,24 @@
|
||||
✓ it adds coverage if --min exist
|
||||
✓ 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 function
|
||||
✓ it guesses if the given argument is a class or function
|
||||
✓ it uses the correct PHPUnit attribute for trait
|
||||
|
||||
PASS Tests\Features\Covers\CoversNothing
|
||||
✓ 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
|
||||
|
||||
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
|
||||
✓ it throws exception if dataset does not exist
|
||||
✓ it throws exception if dataset already exist
|
||||
@ -619,6 +628,13 @@
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ failures with custom message
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeSlug
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ failures with custom message
|
||||
✓ failures with default message
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeSnakeCase
|
||||
@ -1030,6 +1046,10 @@
|
||||
✓ it may fail
|
||||
✓ 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
|
||||
✓ it can set/get properties on $this
|
||||
! 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
|
||||
// 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
|
||||
✓ once
|
||||
✓ multiple times @ repetition 1 of 5
|
||||
@ -1295,6 +1319,10 @@
|
||||
✓ it can see datasets defined in Pest.php file with ('B')
|
||||
✓ 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
|
||||
✓ it do not skips
|
||||
- it skips with truthy → 1
|
||||
@ -1425,6 +1453,11 @@
|
||||
✓ nested → nested 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
|
||||
✓ global beforeEach execution order
|
||||
|
||||
@ -1698,4 +1731,4 @@
|
||||
WARN Tests\Visual\Version
|
||||
- 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)
|
||||
@ -27,26 +27,6 @@ arch('globals')
|
||||
->not->toBeUsed()
|
||||
->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')
|
||||
->expect('Pest\Contracts')
|
||||
->toOnlyUse([
|
||||
|
||||
@ -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.');
|
||||
13
tests/Features/Covers/ClassCoverage.php
Normal file
13
tests/Features/Covers/ClassCoverage.php
Normal 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');
|
||||
});
|
||||
10
tests/Features/Covers/CoversNothing.php
Normal file
10
tests/Features/Covers/CoversNothing.php
Normal 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();
|
||||
10
tests/Features/Covers/ExceptionHandling.php
Normal file
10
tests/Features/Covers/ExceptionHandling.php
Normal 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.');
|
||||
12
tests/Features/Covers/FunctionCoverage.php
Normal file
12
tests/Features/Covers/FunctionCoverage.php
Normal 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');
|
||||
17
tests/Features/Covers/GuessCoverage.php
Normal file
17
tests/Features/Covers/GuessCoverage.php
Normal 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');
|
||||
11
tests/Features/Covers/TraitCoverage.php
Normal file
11
tests/Features/Covers/TraitCoverage.php
Normal 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);
|
||||
24
tests/Features/Expect/toBeSlug.php
Normal file
24
tests/Features/Expect/toBeSlug.php
Normal 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);
|
||||
12
tests/Features/Fixture.php
Normal file
12
tests/Features/Fixture.php
Normal 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);
|
||||
11
tests/Features/References.php
Normal file
11
tests/Features/References.php
Normal 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
11
tests/Features/See.php
Normal 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']);
|
||||
16
tests/Hooks/BeforeAllTest.php
Normal file
16
tests/Hooks/BeforeAllTest.php
Normal 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);
|
||||
});
|
||||
@ -29,7 +29,6 @@ pest()
|
||||
})
|
||||
->beforeAll(function () {
|
||||
$_SERVER['globalHook']->beforeAll = 0;
|
||||
$_SERVER['globalHook']->calls->beforeAll++;
|
||||
})
|
||||
->afterEach(function () {
|
||||
if (! isset($this->ith)) {
|
||||
|
||||
@ -16,7 +16,7 @@ $run = function () {
|
||||
|
||||
test('parallel', function () use ($run) {
|
||||
expect($run('--exclude-group=integration'))
|
||||
->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 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');
|
||||
})->skipOnWindows();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user