Compare commits

...

54 Commits
v3.5.2 ... 3.x

Author SHA1 Message Date
7796630eaf chore: only runs CI against stable 2026-01-28 01:33:45 +00:00
3aec2b26ef chore: runs only unit tests 2026-01-28 01:30:45 +00:00
7a7c35292c release: 3.8.5 2026-01-28 01:23:37 +00:00
72cf695554 release: 3.8.4 2025-08-20 20:12:42 +01:00
027f4e4832 chore: bumps dependencies 2025-08-20 14:21:14 +01:00
165c879fe6 release: 3.8.3 2025-08-19 11:11:21 +01:00
4c8bf4b2fd chore: uses phpunit v11.5.33 2025-08-19 11:11:10 +01:00
1b0a846a81 Update README.md 2025-08-15 17:11:53 +01:00
f692be3637 chore: bumps dependencies 2025-07-26 07:34:25 -06:00
127ad618d3 chore: style 2025-07-26 07:34:19 -06:00
55218bcf78 Merge pull request #1324 from bibrokhim/add-attributes-to-laravel-preset
Add Attributes to Laravel preset
2025-07-26 04:19:54 +01:00
2a47b514ec Merge pull request #1351 from cndrsdrmn/patch-1
fix: add ignoring clause for `App\Features\Concerns` on Laravel Preset
2025-07-26 04:17:21 +01:00
7d77bbf1bb Merge pull request #1410 from JonPurvis/remove-period
Remove Period from `ShouldNotHappen` message
2025-06-23 19:29:01 +01:00
97c136cd94 link to issues page 2025-06-16 02:55:11 +01:00
d6cbd12d8b remove period from message 2025-06-16 02:51:48 +01:00
c6244a8712 Release 3.8.2 2025-04-17 11:53:02 +01:00
eed68f2840 Adjusts sponsors 2025-04-13 17:15:23 +01:00
6080f51a0b release: v3.8.1 2025-04-03 17:35:58 +01:00
e0f07be017 fix: init command detecting laravel 2025-04-03 17:23:39 +01:00
42e1b9f17f release: v3.8.0 2025-03-30 18:49:10 +01:00
0171617c1d chore: adjusts to new types on arch 2025-03-30 18:42:00 +01:00
2e11e9e65d docs: adjusts readme 2025-03-29 18:23:23 +00:00
4969526ef2 chore: bumps paratest 2025-03-29 17:57:53 +00:00
d7b1c36fdd Merge pull request #1341 from nuernbergerA/phpunit-overrides
chore: Sync overrides
2025-03-29 17:52:57 +00:00
003fc96e8f release: 3.7.5 2025-03-29 17:48:00 +00:00
f68d11ccae chore: bumps dependencies 2025-03-29 17:44:06 +00:00
ed70c9dc2b refactor: type adjustments 2025-03-14 22:40:39 +00:00
157a753d87 Update Pest.php.stub 2025-03-12 18:35:57 +00:00
a5317c5640 fix: add ignoring clause for App\Features\Concerns on Laravel Preset 2025-02-08 00:07:21 +07:00
66ceb64faa Updates tests 2025-02-03 13:36:47 +00:00
fa4098db8d Bumps dependencies 2025-02-03 13:30:45 +00:00
4a987d3d5c release: 3.7.4 2025-01-23 14:03:29 +00:00
4079a08f5f feat: adds --compact to coverage 2025-01-23 13:59:51 +00:00
e4aab77a34 release: 3.7.3 2025-01-23 12:51:02 +00:00
c4c9e915f4 cs 2025-01-20 09:50:36 +01:00
e834527db2 Update JunitXmlLogger.php
https://github.com/sebastianbergmann/phpunit/issues/6098
2025-01-20 09:39:10 +01:00
23f130b0f9 Update JunitXmlLogger.php
from https://github.com/sebastianbergmann/phpunit/issues/5771
c722fb2599
2025-01-20 09:38:24 +01:00
0cb8c42497 sync missing listener 2025-01-20 09:36:48 +01:00
fe4b5e5e1f sync change 2025-01-20 09:35:44 +01:00
8ee9d66d80 sync cs 2025-01-20 09:34:55 +01:00
7760d945bb sync latest changes 2025-01-20 09:34:23 +01:00
709ecb1ba2 chore: adjusts tests 2025-01-19 17:35:09 +00:00
6afb36519d release: 3.7.2 2025-01-19 17:16:25 +00:00
150bb9478d docs: adjusts sponsors 2025-01-08 01:09:20 +00:00
bf3178473d release: 3.7.1 2024-12-12 11:52:01 +00:00
d2eb94d723 chore: bumps phpunit and paratest 2024-12-12 11:50:43 +00:00
9688b83a3d release: 3.7.0 2024-12-10 11:54:49 +00:00
675372c794 chore: fixes types 2024-12-10 11:54:42 +00:00
c18636b3d5 chore: adds phpunit 11.5.0 support 2024-12-10 11:53:59 +00:00
1ac594bdf0 Add Attributes to Laravel preset 2024-12-06 16:07:59 +05:00
145294a4a3 chore: style 2024-12-01 23:55:15 +00:00
c2cabaeae6 chore: fixes test suite 2024-12-01 23:16:34 +00:00
918a8fc169 release: 3.6.0 2024-12-01 22:46:00 +00:00
5d32dd0641 feat: option to coverage 2024-12-01 22:45:31 +00:00
49 changed files with 635 additions and 277 deletions

View File

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

View File

@ -15,7 +15,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
symfony: ['7.1'] symfony: ['7.1']
php: ['8.2', '8.3', '8.4'] php: ['8.2', '8.3', '8.4']
dependency_version: [prefer-lowest, prefer-stable] dependency_version: [prefer-stable]
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }} name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}

View File

@ -15,30 +15,38 @@
**Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP. **Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP.
- Explore our docs at **[pestphp.com »](https://pestphp.com)** - Explore our docs at **[pestphp.com »](https://pestphp.com)**
- Follow us on Twitter at **[@pestphp »](https://twitter.com/pestphp)** - Follow the creator Nuno Maduro:
- Join us at **[discord.gg/kaHY6p54JH »](https://discord.gg/kaHY6p54JH)** or **[t.me/+kYH5G4d5MV83ODk0 »](https://t.me/+kYH5G4d5MV83ODk0)** - YouTube: **[youtube.com/@nunomaduro](https://www.youtube.com/@nunomaduro)** — Videos every weekday
- Twitch: **[twitch.tv/enunomaduro](https://www.twitch.tv/enunomaduro)** — Streams (almost) every weekday
- Twitter / X: **[x.com/enunomaduro](https://x.com/enunomaduro)**
- LinkedIn: **[linkedin.com/in/nunomaduro](https://www.linkedin.com/in/nunomaduro)**
- Instagram: **[instagram.com/enunomaduro](https://www.instagram.com/enunomaduro)**
- Tiktok: **[tiktok.com/@enunomaduro](https://www.tiktok.com/@enunomaduro)**
## Sponsors ## Sponsors
We cannot thank our sponsors enough for their incredible support in funding Pest's development. Their contributions have been instrumental in making Pest the best it can be. For those who are interested in becoming a sponsor, please visit Nuno Maduro's Sponsor page at **[github.com/sponsors/nunomaduro](https://github.com/sponsors/nunomaduro)**. We cannot thank our sponsors enough for their incredible support in funding Pest's development. Their contributions have been instrumental in making Pest the best it can be. For those who are interested in becoming a sponsor, please visit Nuno Maduro's Sponsor page at **[github.com/sponsors/nunomaduro](https://github.com/sponsors/nunomaduro)**.
### Platinum Sponsors ### Platinum Sponsors
- **[CodeRabbit](https://coderabbit.ai)** - **[Laracasts](https://laracasts.com/?ref=pestphp)**
- **[LaraJobs](https://larajobs.com)**
- **[Brokerchooser](https://brokerchooser.com)** ### Gold Sponsors
- **[Forge](https://forge.laravel.com)**
- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
- **[NativePHP](https://nativephp.com/mobile?ref=pestphp.com)**
- **[CMS Max](https://cmsmax.com/?ref=pestphp)**
### Premium Sponsors ### Premium Sponsors
- [Akaunting](https://akaunting.com/?ref=pestphp) - [Akaunting](https://akaunting.com/?ref=pestphp)
- [Codecourse](https://codecourse.com/?ref=pestphp)
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp) - [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
- [Laracasts](https://laracasts.com/?ref=pestphp)
- [Localazy](https://localazy.com/?ref=pestphp) - [Localazy](https://localazy.com/?ref=pestphp)
- [Forge](https://forge.laravel.com/?ref=pestphp)
- [Route4Me](https://www.route4me.com/?ref=pestphp) - [Route4Me](https://www.route4me.com/?ref=pestphp)
- [Spatie](https://spatie.be) - [Spatie](https://spatie.be/?ref=pestphp)
- [Worksome](https://www.worksome.com/) - [Worksome](https://www.worksome.com/?ref=pestphp)
- [Zapiet](https://www.zapiet.com/?ref=pestphp) - [Zapiet](https://www.zapiet.com/?ref=pestphp)
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.

View File

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

View File

@ -18,17 +18,17 @@
], ],
"require": { "require": {
"php": "^8.2.0", "php": "^8.2.0",
"brianium/paratest": "^7.6.0", "brianium/paratest": "^7.8.5",
"nunomaduro/collision": "^8.5.0", "nunomaduro/collision": "^8.8.3",
"nunomaduro/termwind": "^2.3.0", "nunomaduro/termwind": "^2.3.3",
"pestphp/pest-plugin": "^3.0.0", "pestphp/pest-plugin": "^3.0.0",
"pestphp/pest-plugin-arch": "^3.0.0", "pestphp/pest-plugin-arch": "^3.1.1",
"pestphp/pest-plugin-mutate": "^3.0.5", "pestphp/pest-plugin-mutate": "^3.0.5",
"phpunit/phpunit": "^11.4.4" "phpunit/phpunit": "^11.5.50"
}, },
"conflict": { "conflict": {
"filp/whoops": "<2.16.0", "filp/whoops": "<2.16.0",
"phpunit/phpunit": ">11.4.4", "phpunit/phpunit": ">11.5.50",
"sebastian/exporter": "<6.0.0", "sebastian/exporter": "<6.0.0",
"webmozart/assert": "<1.11.0" "webmozart/assert": "<1.11.0"
}, },
@ -53,9 +53,9 @@
] ]
}, },
"require-dev": { "require-dev": {
"pestphp/pest-dev-tools": "^3.3.0", "pestphp/pest-dev-tools": "^3.4.0",
"pestphp/pest-plugin-type-coverage": "^3.2.0", "pestphp/pest-plugin-type-coverage": "^3.6.1",
"symfony/process": "^7.1.8" "symfony/process": "^7.4.4"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,
@ -71,21 +71,12 @@
], ],
"scripts": { "scripts": {
"refacto": "rector", "refacto": "rector",
"lint": "pint",
"test:refacto": "rector --dry-run",
"test:lint": "pint --test",
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
"test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100",
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact", "test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml", "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:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3",
"test:integration": "php bin/pest --colors=always --group=integration -v", "test:integration": "php bin/pest --colors=always --group=integration -v",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
"test": [ "test": [
"@test:refacto",
"@test:lint",
"@test:type:check",
"@test:type:coverage",
"@test:unit", "@test:unit",
"@test:parallel", "@test:parallel",
"@test:integration" "@test:integration"

View File

@ -68,7 +68,7 @@ final readonly class ThrowableBuilder
$previous = self::from($previous); $previous = self::from($previous);
} }
$trace = Filter::getFilteredStacktrace($t); $trace = Filter::stackTraceFromThrowableAsString($t);
if ($t instanceof RenderableOnCollisionEditor && $frame = $t->toCollisionEditor()) { if ($t instanceof RenderableOnCollisionEditor && $frame = $t->toCollisionEditor()) {
$file = $frame->getFile(); $file = $frame->getFile();

View File

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

View File

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

199
phpstan-baseline.neon Normal file
View File

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

View File

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

View File

@ -35,7 +35,8 @@ final class Laravel extends AbstractPreset
->ignoring('App\Features\Concerns'); ->ignoring('App\Features\Concerns');
$this->expectations[] = expect('App\Features') $this->expectations[] = expect('App\Features')
->toHaveMethod('resolve'); ->toHaveMethod('resolve')
->ignoring('App\Features\Concerns');
$this->expectations[] = expect('App\Exceptions') $this->expectations[] = expect('App\Exceptions')
->classes() ->classes()
@ -166,5 +167,11 @@ final class Laravel extends AbstractPreset
$this->expectations[] = expect('App\Policies') $this->expectations[] = expect('App\Policies')
->classes() ->classes()
->toHaveSuffix('Policy'); ->toHaveSuffix('Policy');
$this->expectations[] = expect('App\Attributes')
->classes()
->toImplement('Illuminate\Contracts\Container\ContextualAttribute')
->toHaveAttribute('Attribute')
->toHaveMethod('resolve');
} }
} }

View File

@ -19,13 +19,13 @@ final class BootOverrides implements Bootstrapper
*/ */
public const FILES = [ public const FILES = [
'53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php', '53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php',
'a4a43de01f641c6944ee83d963795a46d32b5206b5ab3bbc6cce76e67190acbf' => 'Runner/ResultCache/DefaultResultCache.php', '77ffb7647b583bd82e37962c6fbdc4b04d3344d8a2c1ed103e625ed1ff7cb5c2' => 'Runner/ResultCache/DefaultResultCache.php',
'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php', 'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php',
'3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', '3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
'8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', '8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php', 'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php',
'357d5cd7007f8559b26e1b8cdf43bb6fb15b51b79db981779da6f31b7ec39dad' => 'Event/Value/ThrowableBuilder.php', '8cfcb4999af79463eca51a42058e502ea4ddc776cba5677bf2f8eb6093e21a5c' => 'Event/Value/ThrowableBuilder.php',
'ede161507d4c9c27805f55a05a32c3bb528e53b6e1fc092bfafdb8207e0019e9' => 'Logging/JUnit/JunitXmlLogger.php', '86cd9bcaa53cdd59c5b13e58f30064a015c549501e7629d93b96893d4dee1eb1' => 'Logging/JUnit/JunitXmlLogger.php',
]; ];
/** /**

View File

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

View File

@ -20,7 +20,7 @@ final class ShouldNotHappen extends RuntimeException
$message = $exception->getMessage(); $message = $exception->getMessage();
parent::__construct(sprintf(<<<'EOF' parent::__construct(sprintf(<<<'EOF'
This should not happen - please create an new issue here: https://github.com/pestphp/pest. This should not happen - please create an new issue here: https://github.com/pestphp/pest/issues
Issue: %s Issue: %s
PHP version: %s PHP version: %s

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ use Pest\Support\Str;
use PHPUnit\Event\Code\Test; use PHPUnit\Event\Code\Test;
use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Code\Throwable;
use PHPUnit\Event\Test\AfterLastTestMethodErrored;
use PHPUnit\Event\Test\BeforeFirstTestMethodErrored; use PHPUnit\Event\Test\BeforeFirstTestMethodErrored;
use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\ConsideredRisky;
use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\Errored;
@ -254,8 +255,9 @@ final readonly class Converter
$numberOfNotPassedTests = count( $numberOfNotPassedTests = count(
array_unique( array_unique(
array_map( array_map(
function (BeforeFirstTestMethodErrored|Errored|Failed|Skipped|ConsideredRisky|MarkedIncomplete $event): string { function (AfterLastTestMethodErrored|BeforeFirstTestMethodErrored|Errored|Failed|Skipped|ConsideredRisky|MarkedIncomplete $event): string {
if ($event instanceof BeforeFirstTestMethodErrored) { if ($event instanceof BeforeFirstTestMethodErrored
|| $event instanceof AfterLastTestMethodErrored) {
return $event->testClassName(); return $event->testClassName();
} }

View File

@ -232,7 +232,6 @@ final class TeamCityLogger
$reflector = new ReflectionClass($telemetry); $reflector = new ReflectionClass($telemetry);
$property = $reflector->getProperty('current'); $property = $reflector->getProperty('current');
$property->setAccessible(true);
$snapshot = $property->getValue($telemetry); $snapshot = $property->getValue($telemetry);
assert($snapshot instanceof Snapshot); assert($snapshot instanceof Snapshot);

View File

@ -183,7 +183,6 @@ final class Expectation
{ {
foreach ($needles as $needle) { foreach ($needles as $needle) {
if (is_string($this->value)) { if (is_string($this->value)) {
// @phpstan-ignore-next-line
Assert::assertStringContainsString((string) $needle, $this->value); Assert::assertStringContainsString((string) $needle, $this->value);
} else { } else {
if (! is_iterable($this->value)) { if (! is_iterable($this->value)) {

View File

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

View File

@ -56,7 +56,7 @@ final class AfterEachCall
$afterEachTestCase = ChainableClosure::boundWhen( $afterEachTestCase = ChainableClosure::boundWhen(
fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true), fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true),
ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class),
)->bindTo($this, self::class); )->bindTo($this, self::class);
assert($afterEachTestCase instanceof Closure); assert($afterEachTestCase instanceof Closure);

View File

@ -79,7 +79,7 @@ final class BeforeEachCall
$beforeEachTestCase = ChainableClosure::boundWhen( $beforeEachTestCase = ChainableClosure::boundWhen(
fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true), fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true),
ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class),
)->bindTo($this, self::class); )->bindTo($this, self::class);
assert($beforeEachTestCase instanceof Closure); assert($beforeEachTestCase instanceof Closure);

View File

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

View File

@ -224,7 +224,7 @@ final class TestCall // @phpstan-ignore-line
*/ */
public function only(): self public function only(): self
{ {
Only::enable($this, ...func_get_args()); // @phpstan-ignore-line Only::enable($this, ...func_get_args());
return $this; return $this;
} }

View File

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

View File

@ -27,16 +27,31 @@ final class Coverage implements AddsOutput, HandlesArguments
*/ */
private const MIN_OPTION = 'min'; private const MIN_OPTION = 'min';
/**
* @var string
*/
private const EXACTLY_OPTION = 'exactly';
/** /**
* Whether it should show the coverage or not. * Whether it should show the coverage or not.
*/ */
public bool $coverage = false; public bool $coverage = false;
/**
* Whether it should show the coverage or not.
*/
public bool $compact = false;
/** /**
* The minimum coverage. * The minimum coverage.
*/ */
public float $coverageMin = 0.0; public float $coverageMin = 0.0;
/**
* The exactly coverage.
*/
public ?float $coverageExactly = null;
/** /**
* Creates a new Plugin instance. * Creates a new Plugin instance.
*/ */
@ -51,7 +66,7 @@ final class Coverage implements AddsOutput, HandlesArguments
public function handleArguments(array $originals): array public function handleArguments(array $originals): array
{ {
$arguments = [...[''], ...array_values(array_filter($originals, function (string $original): bool { $arguments = [...[''], ...array_values(array_filter($originals, function (string $original): bool {
foreach ([self::COVERAGE_OPTION, self::MIN_OPTION] as $option) { foreach ([self::COVERAGE_OPTION, self::MIN_OPTION, self::EXACTLY_OPTION] as $option) {
if ($original === sprintf('--%s', $option)) { if ($original === sprintf('--%s', $option)) {
return true; return true;
} }
@ -73,6 +88,7 @@ final class Coverage implements AddsOutput, HandlesArguments
$inputs = []; $inputs = [];
$inputs[] = new InputOption(self::COVERAGE_OPTION, null, InputOption::VALUE_NONE); $inputs[] = new InputOption(self::COVERAGE_OPTION, null, InputOption::VALUE_NONE);
$inputs[] = new InputOption(self::MIN_OPTION, null, InputOption::VALUE_REQUIRED); $inputs[] = new InputOption(self::MIN_OPTION, null, InputOption::VALUE_REQUIRED);
$inputs[] = new InputOption(self::EXACTLY_OPTION, null, InputOption::VALUE_REQUIRED);
$input = new ArgvInput($arguments, new InputDefinition($inputs)); $input = new ArgvInput($arguments, new InputDefinition($inputs));
if ((bool) $input->getOption(self::COVERAGE_OPTION)) { if ((bool) $input->getOption(self::COVERAGE_OPTION)) {
@ -106,6 +122,17 @@ final class Coverage implements AddsOutput, HandlesArguments
$this->coverageMin = (float) $minOption; $this->coverageMin = (float) $minOption;
} }
if ($input->getOption(self::EXACTLY_OPTION) !== null) {
/** @var int|float $exactlyOption */
$exactlyOption = $input->getOption(self::EXACTLY_OPTION);
$this->coverageExactly = (float) $exactlyOption;
}
if ($_SERVER['COLLISION_PRINTER_COMPACT'] ?? false) {
$this->compact = true;
}
return $originals; return $originals;
} }
@ -126,11 +153,23 @@ final class Coverage implements AddsOutput, HandlesArguments
exit(1); exit(1);
} }
$coverage = \Pest\Support\Coverage::report($this->output); $coverage = \Pest\Support\Coverage::report($this->output, $this->compact);
$exitCode = (int) ($coverage < $this->coverageMin); $exitCode = (int) ($coverage < $this->coverageMin);
if ($exitCode === 1) { if ($exitCode === 0 && $this->coverageExactly !== null) {
$comparableCoverage = $this->computeComparableCoverage($coverage);
$comparableCoverageExactly = $this->computeComparableCoverage($this->coverageExactly);
$exitCode = $comparableCoverage === $comparableCoverageExactly ? 0 : 1;
if ($exitCode === 1) {
$this->output->writeln(sprintf(
"\n <fg=white;bg=red;options=bold> FAIL </> Code coverage not exactly <fg=white;options=bold> %s %%</>, currently <fg=red;options=bold> %s %%</>.",
number_format($this->coverageExactly, 1),
number_format(floor($coverage * 10) / 10, 1),
));
}
} elseif ($exitCode === 1) {
$this->output->writeln(sprintf( $this->output->writeln(sprintf(
"\n <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected <fg=white;options=bold> %s %%</>, currently <fg=red;options=bold> %s %%</>.", "\n <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected <fg=white;options=bold> %s %%</>, currently <fg=red;options=bold> %s %%</>.",
number_format($this->coverageMin, 1), number_format($this->coverageMin, 1),
@ -143,4 +182,12 @@ final class Coverage implements AddsOutput, HandlesArguments
return $exitCode; return $exitCode;
} }
/**
* Computes the comparable coverage to a percentage with one decimal.
*/
private function computeComparableCoverage(float $coverage): float
{
return floor($coverage * 10) / 10;
}
} }

View File

@ -119,6 +119,6 @@ final readonly class Init implements HandlesArguments
*/ */
private function isLaravelInstalled(): bool private function isLaravelInstalled(): bool
{ {
return InstalledVersions::isInstalled('laravel/laravel'); return InstalledVersions::isInstalled('laravel/framework');
} }
} }

View File

@ -19,6 +19,7 @@ use Pest\TestSuite;
use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Event\TestRunner\WarningTriggered; use PHPUnit\Event\TestRunner\WarningTriggered;
use PHPUnit\Runner\CodeCoverage; use PHPUnit\Runner\CodeCoverage;
use PHPUnit\Runner\ResultCache\DefaultResultCache;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade; use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry; use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
@ -79,7 +80,10 @@ final class WrapperRunner implements RunnerInterface
private array $unexpectedOutputFiles = []; private array $unexpectedOutputFiles = [];
/** @var list<SplFileInfo> */ /** @var list<SplFileInfo> */
private array $testresultFiles = []; private array $resultCacheFiles = [];
/** @var list<SplFileInfo> */
private array $testResultFiles = [];
/** @var list<SplFileInfo> */ /** @var list<SplFileInfo> */
private array $coverageFiles = []; private array $coverageFiles = [];
@ -264,7 +268,8 @@ final class WrapperRunner implements RunnerInterface
$this->batches[$token] = 0; $this->batches[$token] = 0;
$this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile; $this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile;
$this->testresultFiles[] = $worker->testresultFile; $this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile;
$this->testResultFiles[] = $worker->testResultFile;
if (isset($worker->junitFile)) { if (isset($worker->junitFile)) {
$this->junitFiles[] = $worker->junitFile; $this->junitFiles[] = $worker->junitFile;
@ -298,12 +303,12 @@ final class WrapperRunner implements RunnerInterface
private function complete(TestResult $testResultSum): int private function complete(TestResult $testResultSum): int
{ {
foreach ($this->testresultFiles as $testresultFile) { foreach ($this->testResultFiles as $testResultFile) {
if (! $testresultFile->isFile()) { if (! $testResultFile->isFile()) {
continue; continue;
} }
$contents = file_get_contents($testresultFile->getPathname()); $contents = file_get_contents($testResultFile->getPathname());
assert($contents !== false); assert($contents !== false);
$testResult = unserialize($contents); $testResult = unserialize($contents);
assert($testResult instanceof TestResult); assert($testResult instanceof TestResult);
@ -360,9 +365,20 @@ final class WrapperRunner implements RunnerInterface
$testResultSum->phpNotices(), $testResultSum->phpNotices(),
$testResultSum->phpWarnings(), $testResultSum->phpWarnings(),
$testResultSum->numberOfIssuesIgnoredByBaseline(), $testResultSum->numberOfIssuesIgnoredByBaseline(),
); );
if ($this->options->configuration->cacheResult()) {
$resultCacheSum = new DefaultResultCache($this->options->configuration->testResultCacheFile());
foreach ($this->resultCacheFiles as $resultCacheFile) {
$resultCache = new DefaultResultCache($resultCacheFile->getPathname());
$resultCache->load();
$resultCacheSum->mergeWith($resultCache);
}
$resultCacheSum->persist();
}
$this->printer->printResults( $this->printer->printResults(
$testResultSum, $testResultSum,
$this->teamcityFiles, $this->teamcityFiles,
@ -375,7 +391,7 @@ final class WrapperRunner implements RunnerInterface
$exitcode = Result::exitCode($this->options->configuration, $testResultSum); $exitcode = Result::exitCode($this->options->configuration, $testResultSum);
$this->clearFiles($this->unexpectedOutputFiles); $this->clearFiles($this->unexpectedOutputFiles);
$this->clearFiles($this->testresultFiles); $this->clearFiles($this->testResultFiles);
$this->clearFiles($this->coverageFiles); $this->clearFiles($this->coverageFiles);
$this->clearFiles($this->junitFiles); $this->clearFiles($this->junitFiles);
$this->clearFiles($this->teamcityFiles); $this->clearFiles($this->teamcityFiles);

View File

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

View File

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

View File

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

View File

@ -23,14 +23,12 @@ final class EnsureIgnorableTestCasesAreIgnored implements StartedSubscriber
{ {
$reflection = new ReflectionClass(Facade::class); $reflection = new ReflectionClass(Facade::class);
$property = $reflection->getProperty('collector'); $property = $reflection->getProperty('collector');
$property->setAccessible(true);
$collector = $property->getValue(); $collector = $property->getValue();
assert($collector instanceof Collector); assert($collector instanceof Collector);
$reflection = new ReflectionClass($collector); $reflection = new ReflectionClass($collector);
$property = $reflection->getProperty('testRunnerTriggeredWarningEvents'); $property = $reflection->getProperty('testRunnerTriggeredWarningEvents');
$property->setAccessible(true);
/** @var array<int, WarningTriggered> $testRunnerTriggeredWarningEvents */ /** @var array<int, WarningTriggered> $testRunnerTriggeredWarningEvents */
$testRunnerTriggeredWarningEvents = $property->getValue($collector); $testRunnerTriggeredWarningEvents = $property->getValue($collector);

View File

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

View File

@ -74,7 +74,7 @@ final class Coverage
* Reports the code coverage report to the * Reports the code coverage report to the
* console and returns the result in float. * console and returns the result in float.
*/ */
public static function report(OutputInterface $output): float public static function report(OutputInterface $output, bool $compact = false): float
{ {
if (! file_exists($reportPath = self::getPath())) { if (! file_exists($reportPath = self::getPath())) {
if (self::usingXdebug()) { if (self::usingXdebug()) {
@ -113,6 +113,10 @@ final class Coverage
? '100.0' ? '100.0'
: number_format($file->percentageOfExecutedLines()->asFloat(), 1, '.', ''); : number_format($file->percentageOfExecutedLines()->asFloat(), 1, '.', '');
if ($percentage === '100.0' && $compact) {
continue;
}
$uncoveredLines = ''; $uncoveredLines = '';
$percentageOfExecutedLinesAsString = $file->percentageOfExecutedLines()->asString(); $percentageOfExecutedLinesAsString = $file->percentageOfExecutedLines()->asString();

View File

@ -66,6 +66,7 @@ final readonly class Exporter
$result[] = $context->contains($data[$key]) !== false $result[] = $context->contains($data[$key]) !== false
? '*RECURSION*' ? '*RECURSION*'
// @phpstan-ignore-next-line
: sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context)); : sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context));
} }

View File

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

View File

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

View File

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

View File

@ -34,8 +34,6 @@ final class Reflection
try { try {
$reflectionMethod = $reflectionClass->getMethod($method); $reflectionMethod = $reflectionClass->getMethod($method);
$reflectionMethod->setAccessible(true);
return $reflectionMethod->invoke($object, ...$args); return $reflectionMethod->invoke($object, ...$args);
} catch (ReflectionException $exception) { } catch (ReflectionException $exception) {
if (method_exists($object, '__call')) { if (method_exists($object, '__call')) {
@ -113,8 +111,6 @@ final class Reflection
} }
} }
$reflectionProperty->setAccessible(true);
return $reflectionProperty->getValue($object); return $reflectionProperty->getValue($object);
} }
@ -144,8 +140,6 @@ final class Reflection
} }
} }
} }
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($object, $value); $reflectionProperty->setValue($object, $value);
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
Pest Testing Framework 3.5.2. Pest Testing Framework 3.8.5.
USAGE: pest <file> [options] USAGE: pest <file> [options]
@ -53,7 +53,7 @@
--disallow-test-output ................. Be strict about output during tests --disallow-test-output ................. Be strict about output during tests
--enforce-time-limit ................. Enforce time limit based on test size --enforce-time-limit ................. Enforce time limit based on test size
--default-time-limit [sec] Timeout in seconds for tests that have no declared size --default-time-limit [sec] Timeout in seconds for tests that have no declared size
--dont-report-useless-tests .. Do not report tests that do not test anything --do-not-report-useless-tests Do not report tests that do not test anything
--stop-on-defect ... Stop after first error, failure, warning, or risky test --stop-on-defect ... Stop after first error, failure, warning, or risky test
--stop-on-error ..................................... Stop after first error --stop-on-error ..................................... Stop after first error
--stop-on-failure ................................. Stop after first failure --stop-on-failure ................................. Stop after first failure
@ -68,9 +68,20 @@
--fail-on-risky Signal failure using shell exit code when a test was considered risky --fail-on-risky Signal failure using shell exit code when a test was considered risky
--fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered --fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered
--fail-on-phpunit-deprecation Signal failure using shell exit code when a PHPUnit deprecation was triggered --fail-on-phpunit-deprecation Signal failure using shell exit code when a PHPUnit deprecation was triggered
--fail-on-phpunit-warning Signal failure using shell exit code when a PHPUnit warning was triggered
--fail-on-notice Signal failure using shell exit code when a notice was triggered --fail-on-notice Signal failure using shell exit code when a notice was triggered
--fail-on-skipped Signal failure using shell exit code when a test was skipped --fail-on-skipped Signal failure using shell exit code when a test was skipped
--fail-on-incomplete Signal failure using shell exit code when a test was marked incomplete --fail-on-incomplete Signal failure using shell exit code when a test was marked incomplete
--fail-on-all-issues Signal failure using shell exit code when an issue is triggered
--do-not-fail-on-empty-test-suite Do not signal failure using shell exit code when no tests were run
--do-not-fail-on-warning Do not signal failure using shell exit code when a warning was triggered
--do-not-fail-on-risky Do not signal failure using shell exit code when a test was considered risky
--do-not-fail-on-deprecation Do not signal failure using shell exit code when a deprecation was triggered
--do-not-fail-on-phpunit-deprecation Do not signal failure using shell exit code when a PHPUnit deprecation was triggered
--do-not-fail-on-phpunit-warning Do not signal failure using shell exit code when a PHPUnit warning was triggered
--do-not-fail-on-notice Do not signal failure using shell exit code when a notice was triggered
--do-not-fail-on-skipped Do not signal failure using shell exit code when a test was skipped
--do-not-fail-on-incomplete Do not signal failure using shell exit code when a test was marked incomplete
--cache-result ............................ Write test results to cache file --cache-result ............................ Write test results to cache file
--do-not-cache-result .............. Do not write test results to cache file --do-not-cache-result .............. Do not write test results to cache file
--order-by [order] Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size --order-by [order] Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size
@ -91,6 +102,7 @@
--display-errors ............. Display details for errors triggered by tests --display-errors ............. Display details for errors triggered by tests
--display-notices ........... Display details for notices triggered by tests --display-notices ........... Display details for notices triggered by tests
--display-warnings ......... Display details for warnings triggered by tests --display-warnings ......... Display details for warnings triggered by tests
--display-all-issues ..... Display details for all issues that are triggered
--reverse-list .............................. Print defects in reverse order --reverse-list .............................. Print defects in reverse order
--teamcity . Replace default progress and result output with TeamCity format --teamcity . Replace default progress and result output with TeamCity format
--testdox ................ Replace default result output with TestDox format --testdox ................ Replace default result output with TestDox format

View File

@ -1,3 +1,3 @@
Pest Testing Framework 3.5.2. Pest Testing Framework 3.8.5.

View File

@ -1428,16 +1428,6 @@
PASS Tests\Hooks\BeforeEachTest PASS Tests\Hooks\BeforeEachTest
✓ global beforeEach execution order ✓ global beforeEach execution order
PASS Tests\Overrides\VersionsTest
✓ versions with dataset "Runner/Filter/NameFilterIterator.php"
✓ versions with dataset "Runner/ResultCache/DefaultResultCache.php"
✓ versions with dataset "Runner/TestSuiteLoader.php"
✓ versions with dataset "TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php"
✓ versions with dataset "TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php"
✓ versions with dataset "TextUI/TestSuiteFilterProcessor.php"
✓ versions with dataset "Event/Value/ThrowableBuilder.php"
✓ versions with dataset "Logging/JUnit/JunitXmlLogger.php"
PASS Tests\PHPUnit\CustomAffixes\InvalidTestName PASS Tests\PHPUnit\CustomAffixes\InvalidTestName
✓ it runs file names like @#$%^&()-_=+.php ✓ it runs file names like @#$%^&()-_=+.php
@ -1491,6 +1481,16 @@
PASS Tests\Playground PASS Tests\Playground
✓ basic ✓ basic
PASS Tests\Plugins\Coverage
✓ compute comparable coverage with (0, 0)
✓ compute comparable coverage with (0.5, 0.5)
✓ compute comparable coverage with (1.0, 1.0)
✓ compute comparable coverage with (32.51, 32.5)
✓ compute comparable coverage with (32.12312321312312, 32.1)
✓ compute comparable coverage with (32.53333333333333, 32.5)
✓ compute comparable coverage with (32.57777771232132, 32.5)
✓ compute comparable coverage with (100.0, 100.0)
PASS Tests\Plugins\Traits PASS Tests\Plugins\Traits
✓ it allows global uses ✓ it allows global uses
✓ it allows multiple global uses registered in the same path ✓ it allows multiple global uses registered in the same path

View File

@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
use Pest\Bootstrappers\BootOverrides;
test('versions', function (string $vendorPath, string $expectedHash) {
expect(hash_file('sha256', $vendorPath))->toBe($expectedHash);
})->with(function () {
foreach (BootOverrides::FILES as $hash => $file) {
$path = implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__, 2),
'vendor/phpunit/phpunit/src',
$file,
]);
yield $file => [$path, $hash];
}
});

View File

@ -0,0 +1,23 @@
<?php
use Pest\Plugins\Coverage;
use Symfony\Component\Console\Output\NullOutput;
test('compute comparable coverage', function (float $givenValue, float $expectedValue) {
$output = new NullOutput;
$plugin = new Coverage($output);
$comparableCoverage = (fn () => $this->computeComparableCoverage($givenValue))->call($plugin);
expect($comparableCoverage)->toBe($expectedValue);
})->with([
[0, 0],
[0.5, 0.5],
[1.0, 1.0],
[32.51, 32.5],
[32.12312321312312, 32.1],
[32.53333333333333, 32.5],
[32.57777771232132, 32.5],
[100.0, 100.0],
]);