mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 15:57:21 +01:00
Compare commits
134 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e3444e1db | |||
| fc7a4182b5 | |||
| b7406938ac | |||
| 314caabd1d | |||
| 65cabf91b1 | |||
| f91c6c1e1e | |||
| 843dbbf18a | |||
| 47fb1d7763 | |||
| 639df4cb43 | |||
| e54e4a0178 | |||
| 7749775f50 | |||
| f11f3aa0a4 | |||
| 33817013fe | |||
| 7f11ace329 | |||
| 3d776f1f20 | |||
| d5ced0a5ca | |||
| af1e214be4 | |||
| 7f9b50974a | |||
| cd5272d8cc | |||
| a7b2039175 | |||
| 50960a96e9 | |||
| 507df757a1 | |||
| 8722b3fc3c | |||
| 19eca6e338 | |||
| 6b523d6963 | |||
| a350545803 | |||
| 71c2e97c9f | |||
| 98a12012bf | |||
| 92523a6f39 | |||
| ed38fb644f | |||
| 39b66bf01d | |||
| 1ee36f584d | |||
| e3e518747f | |||
| 0b96b8f630 | |||
| 711a60c2db | |||
| e7132fa012 | |||
| 3b72bbd7fe | |||
| 273edb864c | |||
| fcb60f3c4a | |||
| 91bb7589e2 | |||
| e524bf5f73 | |||
| 27414ce19f | |||
| fbc9e704e2 | |||
| ee6b3ed062 | |||
| 4c88590b89 | |||
| 66e59efec6 | |||
| da04ba62a8 | |||
| d187566e63 | |||
| 3e86e158b2 | |||
| d6c6489e93 | |||
| ee70a3cfea | |||
| 7a6f33f139 | |||
| e29302300f | |||
| 222ed174bc | |||
| 2aa32569f0 | |||
| 1d8d1a046f | |||
| 3d9ceb1cf2 | |||
| 520a5fe29d | |||
| de4409e368 | |||
| 6d6e4e040f | |||
| aac08629f7 | |||
| fe27012bbc | |||
| f9901245f1 | |||
| 21e22decf3 | |||
| e513f76ea9 | |||
| be9c95e3bc | |||
| 9172721ce8 | |||
| 924dc016cc | |||
| f49b91ec0d | |||
| 516ace85b4 | |||
| f9814793dd | |||
| 00572f5f8e | |||
| fb282b184e | |||
| e0695a13cb | |||
| 8f810bf2a2 | |||
| 84636cee96 | |||
| 0355119afc | |||
| 9d0410ee0b | |||
| 0d148c2a67 | |||
| 0f1e87c726 | |||
| 73bf579da3 | |||
| 5def62018b | |||
| d8e1b27491 | |||
| 2ff4713968 | |||
| 3f27352560 | |||
| af3fdceddb | |||
| 3faeede1ef | |||
| 0bc3219a2b | |||
| a22013a7d3 | |||
| 7fc69033f8 | |||
| ef76c04dbe | |||
| 163479ae60 | |||
| c3bfdf130e | |||
| 8c403a57c2 | |||
| 49bf00024f | |||
| dd44ac4195 | |||
| 5d2aafd2a3 | |||
| 0fc9d4dfe0 | |||
| 02b1ffb334 | |||
| c62cc3fef0 | |||
| 909d778da3 | |||
| 7711a52fe9 | |||
| 99c9f4e5d8 | |||
| a310796165 | |||
| db9243ca2e | |||
| 635e3b4c41 | |||
| 791734a29c | |||
| 8cfb0acf46 | |||
| bf67407ba5 | |||
| efdc84e115 | |||
| d1608bf33d | |||
| 4f6140fdb1 | |||
| 442a58d07f | |||
| 19e9267021 | |||
| e46d499384 | |||
| 490f321a0d | |||
| 174645caa2 | |||
| 2c3a53f6cd | |||
| 0bdaef29e9 | |||
| 1ad30a97b3 | |||
| 50ff347b59 | |||
| b5b8fab09b | |||
| 5331b44a18 | |||
| 53c94600cb | |||
| dd7d150caa | |||
| 92bc1decd9 | |||
| e3bfcbe5f1 | |||
| ba7eb70a5d | |||
| 74ff3b8cd9 | |||
| ab0b4a1b4e | |||
| 169b76458e | |||
| 668685498f | |||
| bab193e7e1 | |||
| f720be862e |
10
.github/workflows/static.yml
vendored
10
.github/workflows/static.yml
vendored
@ -24,15 +24,19 @@ jobs:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.2
|
||||
php-version: 8.3
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
extensions: sockets
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer update --prefer-stable --no-interaction --no-progress --ansi
|
||||
|
||||
# - name: Type Check
|
||||
# run: composer test:type:check
|
||||
- name: Profanity Check
|
||||
run: composer test:profanity
|
||||
|
||||
- name: Type Check
|
||||
run: composer test:type:check
|
||||
|
||||
- name: Type Coverage
|
||||
run: composer test:type:coverage
|
||||
|
||||
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@ -12,10 +12,10 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
symfony: ['7.1']
|
||||
php: ['8.2', '8.3', '8.4']
|
||||
dependency_version: [prefer-lowest, prefer-stable]
|
||||
os: [ubuntu-latest, macos-latest] # windows-latest
|
||||
symfony: ['7.3']
|
||||
php: ['8.3', '8.4']
|
||||
dependency_version: [prefer-stable]
|
||||
|
||||
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
|
||||
|
||||
@ -29,6 +29,7 @@ jobs:
|
||||
php-version: ${{ matrix.php }}
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
extensions: sockets
|
||||
|
||||
- name: Setup Problem Matches
|
||||
run: |
|
||||
|
||||
26
README.md
26
README.md
@ -1,7 +1,7 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/pestphp/art/master/v3/banner.png" width="600" alt="PEST">
|
||||
<img src="https://raw.githubusercontent.com/pestphp/art/master/v4/social.png" width="600" alt="PEST">
|
||||
<p align="center">
|
||||
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (master)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=3.x&label=Tests%203.x"></a>
|
||||
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (master)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=4.x&label=Tests%204.x"></a>
|
||||
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
|
||||
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a>
|
||||
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
|
||||
@ -10,14 +10,14 @@
|
||||
|
||||
------
|
||||
|
||||
> Pest v3 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest3-now-available)**.
|
||||
> Pest v4 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest-v4-is-here-now-with-browser-testing)**.
|
||||
|
||||
**Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP.
|
||||
|
||||
- Explore our docs at **[pestphp.com »](https://pestphp.com)**
|
||||
- Follow the creator Nuno Maduro:
|
||||
- YouTube: **[youtube.com/@nunomaduro](https://www.youtube.com/@nunomaduro)** — Videos every weekday
|
||||
- Twitch: **[twitch.tv/enunomaduro](https://www.twitch.tv/enunomaduro)** — Streams (almost) every weekday
|
||||
- YouTube: **[youtube.com/@nunomaduro](https://youtube.com/@nunomaduro)** — Videos every week
|
||||
- Twitch: **[twitch.tv/nunomaduro](https://twitch.tv/nunomaduro)** — Live coding on Mondays, Wednesdays, and Fridays at 9PM UTC
|
||||
- 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)**
|
||||
@ -31,22 +31,24 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
|
||||
### Platinum Sponsors
|
||||
|
||||
- **[Laracasts](https://laracasts.com/?ref=pestphp)**
|
||||
- **[NativePHP](https://nativephp.com/mobile?ref=pestphp.com)**
|
||||
|
||||
### Gold Sponsors
|
||||
|
||||
- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
|
||||
- **[NativePHP](https://nativephp.com/mobile?ref=pestphp.com)**
|
||||
- **[CMS Max](https://cmsmax.com/?ref=pestphp)**
|
||||
|
||||
### Premium Sponsors
|
||||
|
||||
- [Akaunting](https://akaunting.com/?ref=pestphp)
|
||||
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
|
||||
- [Localazy](https://localazy.com/?ref=pestphp)
|
||||
- [Forge](https://forge.laravel.com/?ref=pestphp)
|
||||
- [Route4Me](https://www.route4me.com/?ref=pestphp)
|
||||
- [Spatie](https://spatie.be/?ref=pestphp)
|
||||
- [Worksome](https://www.worksome.com/?ref=pestphp)
|
||||
- [Zapiet](https://www.zapiet.com/?ref=pestphp)
|
||||
- [Localazy](https://localazy.com/?ref=pestphp)
|
||||
- [Load Forge](https://loadforge.com/?ref=pestphp)
|
||||
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
|
||||
- [Route4Me](https://www.route4me.com/?ref=pestphp)
|
||||
- [Devtools for Livewire](https://devtools-for-livewire.com/?ref=pestphp)
|
||||
- [Nerdify](https://www.getnerdify.com/?ref=pestphp)
|
||||
- [Akaunting](https://akaunting.com/?ref=pestphp)
|
||||
- [LambdaTest](https://lambdatest.com/?ref=pestphp)
|
||||
|
||||
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
|
||||
When releasing a new version of Pest there are some checks and updates that need to be done:
|
||||
|
||||
> **For Pest v2 you should use the `2.x` branch instead.**
|
||||
> **For Pest v3 you should use the `3.x` branch instead.**
|
||||
|
||||
- Clear your local repository with: `git add . && git reset --hard && git checkout 3.x`
|
||||
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...3.x](https://github.com/pestphp/pest/compare/{latest_version}...3.x)
|
||||
- Clear your local repository with: `git add . && git reset --hard && git checkout 4.x`
|
||||
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...4.x](https://github.com/pestphp/pest/compare/{latest_version}...4.x)
|
||||
- Update the version number in [src/Pest.php](src/Pest.php)
|
||||
- Run the tests locally using: `composer test`
|
||||
- Commit the Pest file with the message: `git commit -m "release: vX.X.X"`
|
||||
|
||||
@ -86,7 +86,7 @@ $bootPest = (static function (): void {
|
||||
$getopt['teamcity-file'] ?? null,
|
||||
$getopt['testdox-file'] ?? null,
|
||||
isset($getopt['testdox-color']),
|
||||
$getopt['testdox-columns'] ?? null,
|
||||
(int) $getopt['testdox-columns'] ?? null,
|
||||
);
|
||||
|
||||
while (true) {
|
||||
|
||||
@ -17,19 +17,21 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2.0",
|
||||
"brianium/paratest": "^7.8.4",
|
||||
"php": "^8.3.0",
|
||||
"brianium/paratest": "^7.14.0",
|
||||
"nunomaduro/collision": "^8.8.2",
|
||||
"nunomaduro/termwind": "^2.3.1",
|
||||
"pestphp/pest-plugin": "^3.0.0",
|
||||
"pestphp/pest-plugin-arch": "^3.1.1",
|
||||
"pestphp/pest-plugin-mutate": "^3.0.5",
|
||||
"phpunit/phpunit": "^11.5.33"
|
||||
"pestphp/pest-plugin": "^4.0.0",
|
||||
"pestphp/pest-plugin-arch": "^4.0.0",
|
||||
"pestphp/pest-plugin-mutate": "^4.0.1",
|
||||
"pestphp/pest-plugin-profanity": "^4.1.0",
|
||||
"phpunit/phpunit": "^12.3.15",
|
||||
"symfony/process": "^7.3.3"
|
||||
},
|
||||
"conflict": {
|
||||
"filp/whoops": "<2.16.0",
|
||||
"phpunit/phpunit": ">11.5.33",
|
||||
"sebastian/exporter": "<6.0.0",
|
||||
"filp/whoops": "<2.18.3",
|
||||
"phpunit/phpunit": ">12.3.15",
|
||||
"sebastian/exporter": "<7.0.0",
|
||||
"webmozart/assert": "<1.11.0"
|
||||
},
|
||||
"autoload": {
|
||||
@ -53,9 +55,10 @@
|
||||
]
|
||||
},
|
||||
"require-dev": {
|
||||
"pestphp/pest-dev-tools": "^3.4.0",
|
||||
"pestphp/pest-plugin-type-coverage": "^3.6.1",
|
||||
"symfony/process": "^7.3.0"
|
||||
"pestphp/pest-dev-tools": "^4.0.0",
|
||||
"pestphp/pest-plugin-browser": "^4.1.0",
|
||||
"pestphp/pest-plugin-type-coverage": "^4.0.2",
|
||||
"psy/psysh": "^0.12.10"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
@ -71,16 +74,17 @@
|
||||
],
|
||||
"scripts": {
|
||||
"refacto": "rector",
|
||||
"lint": "pint",
|
||||
"lint": "pint --parallel",
|
||||
"test:refacto": "rector --dry-run",
|
||||
"test:lint": "pint --test",
|
||||
"test:lint": "pint --parallel --test",
|
||||
"test:profanity": "php bin/pest --profanity --compact",
|
||||
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
|
||||
"test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100",
|
||||
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
|
||||
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
|
||||
"test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3",
|
||||
"test:integration": "php bin/pest --colors=always --group=integration -v",
|
||||
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
|
||||
"test:unit": "php bin/pest --exclude-group=integration --compact",
|
||||
"test:inline": "php bin/pest --configuration=phpunit.inline.xml",
|
||||
"test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3",
|
||||
"test:integration": "php bin/pest --group=integration -v",
|
||||
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots",
|
||||
"test": [
|
||||
"@test:refacto",
|
||||
"@test:lint",
|
||||
@ -111,6 +115,7 @@
|
||||
"Pest\\Plugins\\Snapshot",
|
||||
"Pest\\Plugins\\Verbose",
|
||||
"Pest\\Plugins\\Version",
|
||||
"Pest\\Plugins\\Shard",
|
||||
"Pest\\Plugins\\Parallel"
|
||||
]
|
||||
},
|
||||
|
||||
@ -52,6 +52,8 @@ use PHPUnit\Util\Filter;
|
||||
use PHPUnit\Util\ThrowableToStringMapper;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final readonly class ThrowableBuilder
|
||||
@ -82,7 +84,7 @@ final readonly class ThrowableBuilder
|
||||
$t->getMessage(),
|
||||
ThrowableToStringMapper::map($t),
|
||||
$trace,
|
||||
$previous
|
||||
$previous,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +99,9 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
|
||||
}
|
||||
|
||||
if ($test instanceof HasPrintableTestCaseName) {
|
||||
$name = $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName();
|
||||
$name = trim(
|
||||
$test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName().$test->dataSetAsString()
|
||||
);
|
||||
} else {
|
||||
$name = $test::class.'::'.$test->nameWithDataSet();
|
||||
}
|
||||
|
||||
@ -72,10 +72,7 @@ use function Pest\version;
|
||||
*/
|
||||
final class DefaultResultCache implements ResultCache
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
|
||||
private const string DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
|
||||
|
||||
private readonly string $cacheFilename;
|
||||
|
||||
|
||||
@ -45,6 +45,7 @@ declare(strict_types=1);
|
||||
namespace PHPUnit\TextUI;
|
||||
|
||||
use Pest\Plugins\Only;
|
||||
use Pest\Runner\Filter\EnsureTestCaseIsInitiatedFilter;
|
||||
use PHPUnit\Event;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Runner\Filter\Factory;
|
||||
@ -66,6 +67,12 @@ final readonly class TestSuiteFilterProcessor
|
||||
{
|
||||
$factory = new Factory;
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
(fn () => $this->filters[] = [
|
||||
'className' => EnsureTestCaseIsInitiatedFilter::class,
|
||||
'argument' => '',
|
||||
])->call($factory);
|
||||
|
||||
if (! $configuration->hasFilter() &&
|
||||
! $configuration->hasGroups() &&
|
||||
! $configuration->hasExcludeGroups() &&
|
||||
@ -73,6 +80,8 @@ final readonly class TestSuiteFilterProcessor
|
||||
! $configuration->hasTestsCovering() &&
|
||||
! $configuration->hasTestsUsing() &&
|
||||
! Only::isEnabled()) {
|
||||
$suite->injectFilter($factory);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
<testsuites>
|
||||
<testsuite name="default">
|
||||
<directory suffix=".php">./tests</directory>
|
||||
<directory suffix=".php">./tests-external</directory>
|
||||
<exclude>./tests/.snapshots</exclude>
|
||||
<exclude>./tests/.tests</exclude>
|
||||
<exclude>./tests/Fixtures/Inheritance</exclude>
|
||||
|
||||
22
resources/views/installers/plugin-browser.php
Normal file
22
resources/views/installers/plugin-browser.php
Normal file
@ -0,0 +1,22 @@
|
||||
<div class="mx-2 mb-1">
|
||||
<p>
|
||||
<span>Using the <span class="text-yellow font-bold">visit()</span> function requires the Pest Plugin Browser to be installed.</span>
|
||||
|
||||
<span class="ml-1 text-yellow font-bold">Run:</span>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<span class="text-gray mr-1">- </span>
|
||||
<span>composer require pestphp/pest-plugin-browser:^4.0 --dev</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-gray mr-1">- </span>
|
||||
<span>npm install playwright@latest</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-gray mr-1">- </span>
|
||||
<span>npx playwright install</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\ArchPresets;
|
||||
|
||||
use Pest\Arch\Contracts\ArchExpectation;
|
||||
use Pest\Expectation;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -89,5 +92,9 @@ final class Php extends AbstractPreset
|
||||
'xdebug_var_dump',
|
||||
'trap',
|
||||
])->not->toBeUsed();
|
||||
|
||||
$this->eachUserNamespace(
|
||||
fn (Expectation $namespace): ArchExpectation => $namespace->not->toHaveSuspiciousCharacters(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,6 @@ final class Security extends AbstractPreset
|
||||
'create_function',
|
||||
'unserialize',
|
||||
'extract',
|
||||
'parse_str',
|
||||
'mb_parse_str',
|
||||
'dl',
|
||||
'assert',
|
||||
|
||||
@ -17,7 +17,7 @@ final class BootExcludeList implements Bootstrapper
|
||||
*
|
||||
* @var array<int, non-empty-string>
|
||||
*/
|
||||
private const EXCLUDE_LIST = [
|
||||
private const array EXCLUDE_LIST = [
|
||||
'bin',
|
||||
'overrides',
|
||||
'resources',
|
||||
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Bootstrappers;
|
||||
|
||||
use Pest\Contracts\Bootstrapper;
|
||||
use Pest\Exceptions\FatalException;
|
||||
use Pest\Support\DatasetInfo;
|
||||
use Pest\Support\Str;
|
||||
use Pest\TestSuite;
|
||||
@ -24,7 +25,7 @@ final class BootFiles implements Bootstrapper
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const STRUCTURE = [
|
||||
private const array STRUCTURE = [
|
||||
'Expectations',
|
||||
'Expectations.php',
|
||||
'Helpers',
|
||||
@ -40,6 +41,10 @@ final class BootFiles implements Bootstrapper
|
||||
$rootPath = TestSuite::getInstance()->rootPath;
|
||||
$testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory();
|
||||
|
||||
if (! is_dir($testsPath)) {
|
||||
throw new FatalException(sprintf('The test directory [%s] does not exist.', $testsPath));
|
||||
}
|
||||
|
||||
foreach (self::STRUCTURE as $filename) {
|
||||
$filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename);
|
||||
|
||||
|
||||
@ -15,17 +15,17 @@ final class BootOverrides implements Bootstrapper
|
||||
/**
|
||||
* The list of files to be overridden.
|
||||
*
|
||||
* @var array<string, string>
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public const FILES = [
|
||||
'53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php',
|
||||
'77ffb7647b583bd82e37962c6fbdc4b04d3344d8a2c1ed103e625ed1ff7cb5c2' => 'Runner/ResultCache/DefaultResultCache.php',
|
||||
'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php',
|
||||
'3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
|
||||
'8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
|
||||
'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php',
|
||||
'8cfcb4999af79463eca51a42058e502ea4ddc776cba5677bf2f8eb6093e21a5c' => 'Event/Value/ThrowableBuilder.php',
|
||||
'86cd9bcaa53cdd59c5b13e58f30064a015c549501e7629d93b96893d4dee1eb1' => 'Logging/JUnit/JunitXmlLogger.php',
|
||||
public const array FILES = [
|
||||
'Runner/Filter/NameFilterIterator.php',
|
||||
'Runner/ResultCache/DefaultResultCache.php',
|
||||
'Runner/TestSuiteLoader.php',
|
||||
'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
|
||||
'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
|
||||
'TextUI/TestSuiteFilterProcessor.php',
|
||||
'Event/Value/ThrowableBuilder.php',
|
||||
'Logging/JUnit/JunitXmlLogger.php',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -20,7 +20,7 @@ final readonly class BootSubscribers implements Bootstrapper
|
||||
*
|
||||
* @var array<int, class-string<Subscriber>>
|
||||
*/
|
||||
private const SUBSCRIBERS = [
|
||||
private const array SUBSCRIBERS = [
|
||||
Subscribers\EnsureConfigurationIsAvailable::class,
|
||||
Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
|
||||
Subscribers\EnsureKernelDumpIsFlushed::class,
|
||||
|
||||
@ -6,10 +6,12 @@ namespace Pest\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Pest\Exceptions\DatasetArgumentsMismatch;
|
||||
use Pest\Panic;
|
||||
use Pest\Preset;
|
||||
use Pest\Support\ChainableClosure;
|
||||
use Pest\Support\ExceptionTrace;
|
||||
use Pest\Support\Reflection;
|
||||
use Pest\Support\Shell;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\Attributes\PostCondition;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@ -101,27 +103,6 @@ trait Testable
|
||||
*/
|
||||
private array $__snapshotChanges = [];
|
||||
|
||||
/**
|
||||
* Creates a new Test Case instance.
|
||||
*/
|
||||
public function __construct(string $name)
|
||||
{
|
||||
parent::__construct($name);
|
||||
|
||||
$test = TestSuite::getInstance()->tests->get(self::$__filename);
|
||||
|
||||
if ($test->hasMethod($name)) {
|
||||
$method = $test->getMethod($name);
|
||||
$this->__description = self::$__latestDescription = $method->description;
|
||||
self::$__latestAssignees = $method->assignees;
|
||||
self::$__latestNotes = $method->notes;
|
||||
self::$__latestIssues = $method->issues;
|
||||
self::$__latestPrs = $method->prs;
|
||||
$this->__describing = $method->describing;
|
||||
$this->__test = $method->getClosure();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the test case static properties.
|
||||
*/
|
||||
@ -214,7 +195,11 @@ trait Testable
|
||||
$beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll);
|
||||
}
|
||||
|
||||
call_user_func(Closure::bind($beforeAll, null, self::class));
|
||||
try {
|
||||
call_user_func(Closure::bind($beforeAll, null, self::class));
|
||||
} catch (Throwable $e) {
|
||||
Panic::with($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -242,8 +227,6 @@ trait Testable
|
||||
|
||||
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
||||
|
||||
$method->setUp($this);
|
||||
|
||||
$description = $method->description;
|
||||
if ($this->dataName()) {
|
||||
$description = str_contains((string) $description, ':dataset')
|
||||
@ -285,6 +268,33 @@ trait Testable
|
||||
$this->__callClosure($beforeEach, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize test case properties from TestSuite.
|
||||
*/
|
||||
public function __initializeTestCase(): void
|
||||
{
|
||||
// Return if the test case has already been initialized
|
||||
if (isset($this->__test)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $this->name();
|
||||
$test = TestSuite::getInstance()->tests->get(self::$__filename);
|
||||
|
||||
if ($test->hasMethod($name)) {
|
||||
$method = $test->getMethod($name);
|
||||
$this->__description = self::$__latestDescription = $method->description;
|
||||
self::$__latestAssignees = $method->assignees;
|
||||
self::$__latestNotes = $method->notes;
|
||||
self::$__latestIssues = $method->issues;
|
||||
self::$__latestPrs = $method->prs;
|
||||
$this->__describing = $method->describing;
|
||||
$this->__test = $method->getClosure();
|
||||
|
||||
$method->setUp($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets executed after the Test Case.
|
||||
*/
|
||||
@ -434,15 +444,7 @@ trait Testable
|
||||
return;
|
||||
}
|
||||
|
||||
if (count($this->__snapshotChanges) === 1) {
|
||||
$this->markTestIncomplete($this->__snapshotChanges[0]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$messages = implode(PHP_EOL, array_map(static fn (string $message): string => '- $message', $this->__snapshotChanges));
|
||||
|
||||
$this->markTestIncomplete($messages);
|
||||
$this->markTestIncomplete(implode('. ', $this->__snapshotChanges));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -466,7 +468,7 @@ trait Testable
|
||||
*/
|
||||
public static function getLatestPrintableTestCaseMethodName(): string
|
||||
{
|
||||
return self::$__latestDescription;
|
||||
return self::$__latestDescription ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -481,4 +483,12 @@ trait Testable
|
||||
'notes' => self::$__latestNotes,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a shell for the test case.
|
||||
*/
|
||||
public function shell(): void
|
||||
{
|
||||
Shell::open();
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +102,14 @@ final readonly class Configuration
|
||||
return Configuration\Project::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the browser configuration.
|
||||
*/
|
||||
public function browser(): Browser\Configuration
|
||||
{
|
||||
return new Browser\Configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies calls to the uses method.
|
||||
*
|
||||
|
||||
@ -16,7 +16,7 @@ final readonly class Help
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const HELP_MESSAGES = [
|
||||
private const array HELP_MESSAGES = [
|
||||
'<comment>Pest Options:</comment>',
|
||||
' <info>--init</info> Initialise a standard Pest configuration',
|
||||
' <info>--coverage</info> Enable coverage and output to standard output',
|
||||
|
||||
@ -22,11 +22,11 @@ final readonly class Thanks
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private const FUNDING_MESSAGES = [
|
||||
private const array FUNDING_MESSAGES = [
|
||||
'Star' => 'https://github.com/pestphp/pest',
|
||||
'YouTube' => 'https://youtube.com/@nunomaduro',
|
||||
'TikTok' => 'https://tiktok.com/@nunomaduro',
|
||||
'Twitch' => 'https://twitch.tv/enunomaduro',
|
||||
'TikTok' => 'https://tiktok.com/@enunomaduro',
|
||||
'Twitch' => 'https://twitch.tv/nunomaduro',
|
||||
'LinkedIn' => 'https://linkedin.com/in/nunomaduro',
|
||||
'Instagram' => 'https://instagram.com/enunomaduro',
|
||||
'X' => 'https://x.com/enunomaduro',
|
||||
|
||||
@ -22,7 +22,7 @@ final class DatasetMissing extends BadFunctionCallException implements Exception
|
||||
public function __construct(string $file, string $name, array $arguments)
|
||||
{
|
||||
parent::__construct(sprintf(
|
||||
"A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s",
|
||||
'A test with the description [%s] has [%d] argument(s) ([%s]) and no dataset(s) provided in [%s]',
|
||||
$name,
|
||||
count($arguments),
|
||||
implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)),
|
||||
|
||||
@ -19,7 +19,11 @@ final class TestCaseAlreadyInUse extends InvalidArgumentException implements Exc
|
||||
*/
|
||||
public function __construct(string $inUse, string $newOne, string $folder)
|
||||
{
|
||||
parent::__construct(sprintf('Test case `%s` can not be used. The folder `%s` already uses the test case `%s`',
|
||||
$newOne, $folder, $inUse));
|
||||
parent::__construct(sprintf(
|
||||
'Test case [%s] can not be used. The folder [%s] already uses the test case [%s].',
|
||||
$newOne,
|
||||
$folder,
|
||||
$inUse,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ final class TestClosureMustNotBeStatic extends InvalidArgumentException implemen
|
||||
{
|
||||
parent::__construct(
|
||||
sprintf(
|
||||
'Test closure must not be static. Please remove the `static` keyword from the `%s` method in `%s`.',
|
||||
'Test closure must not be static. Please remove the [static] keyword from the [%s] method in [%s].',
|
||||
$method->description,
|
||||
$method->filename
|
||||
)
|
||||
|
||||
@ -330,7 +330,7 @@ final class Expectation
|
||||
* @param array<int, mixed> $parameters
|
||||
* @return Expectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue>
|
||||
*/
|
||||
public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation
|
||||
public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation|ArchExpectation
|
||||
{
|
||||
if (! self::hasMethod($method)) {
|
||||
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $method)) {
|
||||
@ -355,6 +355,10 @@ final class Expectation
|
||||
$reflectionClosure = new \ReflectionFunction($closure);
|
||||
$expectation = $reflectionClosure->getClosureThis();
|
||||
|
||||
if ($reflectionClosure->getReturnType()?->__toString() === ArchExpectation::class) {
|
||||
return $closure(...$parameters);
|
||||
}
|
||||
|
||||
assert(is_object($expectation));
|
||||
|
||||
ExpectationPipeline::for($closure)
|
||||
@ -890,6 +894,14 @@ final class Expectation
|
||||
return ToUseNothing::make($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the source code of the given expectation target does not include suspicious characters.
|
||||
*/
|
||||
public function toHaveSuspiciousCharacters(): ArchExpectation
|
||||
{
|
||||
throw InvalidExpectation::fromMethods(['toHaveSuspiciousCharacters']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported.
|
||||
*/
|
||||
|
||||
@ -24,6 +24,7 @@ use PHPUnit\Framework\AssertionFailedError;
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
use ReflectionMethod;
|
||||
use ReflectionProperty;
|
||||
use Spoofchecker;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
@ -278,6 +279,24 @@ final readonly class OppositeExpectation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the given expectation target does not have suspicious characters.
|
||||
*/
|
||||
public function toHaveSuspiciousCharacters(): ArchExpectation
|
||||
{
|
||||
$checker = new Spoofchecker;
|
||||
|
||||
/** @var Expectation<array<int, string>|string> $original */
|
||||
$original = $this->original;
|
||||
|
||||
return Targeted::make(
|
||||
$original,
|
||||
fn (ObjectDescription $object): bool => ! $checker->isSuspicious((string) file_get_contents($object->path)),
|
||||
'to not include suspicious characters',
|
||||
FileLineFinder::where(fn (string $line): bool => $checker->isSuspicious($line)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the given expectation target does not have the given methods.
|
||||
*
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Factories\Covers;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class CoversNothing {}
|
||||
@ -32,7 +32,7 @@ final class TestCaseMethodFactory
|
||||
/**
|
||||
* The test's describing, if any.
|
||||
*
|
||||
* @var array<int, string>
|
||||
* @var array<int, \Pest\Support\Description>
|
||||
*/
|
||||
public array $describing = [];
|
||||
|
||||
|
||||
@ -2,11 +2,14 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Pest\Browser\Api\ArrayablePendingAwaitablePage;
|
||||
use Pest\Browser\Api\PendingAwaitablePage;
|
||||
use Pest\Concerns\Expectable;
|
||||
use Pest\Configuration;
|
||||
use Pest\Exceptions\AfterAllWithinDescribe;
|
||||
use Pest\Exceptions\BeforeAllWithinDescribe;
|
||||
use Pest\Expectation;
|
||||
use Pest\Installers\PluginBrowser;
|
||||
use Pest\Mutate\Contracts\MutationTestRunner;
|
||||
use Pest\Mutate\Repositories\ConfigurationRepository;
|
||||
use Pest\PendingCalls\AfterEachCall;
|
||||
@ -18,6 +21,7 @@ use Pest\Repositories\DatasetsRepository;
|
||||
use Pest\Support\Backtrace;
|
||||
use Pest\Support\Container;
|
||||
use Pest\Support\DatasetInfo;
|
||||
use Pest\Support\Description;
|
||||
use Pest\Support\HigherOrderTapProxy;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@ -95,7 +99,7 @@ if (! function_exists('describe')) {
|
||||
{
|
||||
$filename = Backtrace::testFile();
|
||||
|
||||
return new DescribeCall(TestSuite::getInstance(), $filename, $description, $tests);
|
||||
return new DescribeCall(TestSuite::getInstance(), $filename, new Description($description), $tests);
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,3 +282,51 @@ if (! function_exists('mutates')) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('fixture')) {
|
||||
/**
|
||||
* Returns the absolute path to a fixture file.
|
||||
*/
|
||||
function fixture(string $file): string
|
||||
{
|
||||
$file = implode(DIRECTORY_SEPARATOR, [
|
||||
TestSuite::getInstance()->rootPath,
|
||||
TestSuite::getInstance()->testPath,
|
||||
'Fixtures',
|
||||
str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file),
|
||||
]);
|
||||
|
||||
$fileRealPath = realpath($file);
|
||||
|
||||
if ($fileRealPath === false) {
|
||||
throw new InvalidArgumentException(
|
||||
'The fixture file ['.$file.'] does not exist.',
|
||||
);
|
||||
}
|
||||
|
||||
return $fileRealPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('visit')) {
|
||||
/**
|
||||
* Browse to the given URL.
|
||||
*
|
||||
* @template TUrl of array<int, string>|string
|
||||
*
|
||||
* @param TUrl $url
|
||||
* @param array<string, mixed> $options
|
||||
* @return (TUrl is array<int, string> ? ArrayablePendingAwaitablePage : PendingAwaitablePage)
|
||||
*/
|
||||
function visit(array|string $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage
|
||||
{
|
||||
if (! class_exists(\Pest\Browser\Configuration::class)) {
|
||||
PluginBrowser::install();
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
return test()->visit($url, $options);
|
||||
}
|
||||
}
|
||||
|
||||
15
src/Installers/PluginBrowser.php
Normal file
15
src/Installers/PluginBrowser.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Installers;
|
||||
|
||||
use Pest\Support\View;
|
||||
|
||||
final readonly class PluginBrowser
|
||||
{
|
||||
public static function install(): void
|
||||
{
|
||||
View::render('installers/plugin-browser');
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,7 @@ final readonly class Kernel
|
||||
*
|
||||
* @var array<int, class-string>
|
||||
*/
|
||||
private const BOOTSTRAPPERS = [
|
||||
private const array BOOTSTRAPPERS = [
|
||||
Bootstrappers\BootOverrides::class,
|
||||
Bootstrappers\BootSubscribers::class,
|
||||
Bootstrappers\BootFiles::class,
|
||||
|
||||
@ -31,7 +31,7 @@ final readonly class Converter
|
||||
/**
|
||||
* The prefix for the test suite name.
|
||||
*/
|
||||
private const PREFIX = 'P\\';
|
||||
private const string PREFIX = 'P\\';
|
||||
|
||||
/**
|
||||
* The state generator.
|
||||
|
||||
@ -781,15 +781,13 @@ final class Expectation
|
||||
foreach ($array as $key => $value) {
|
||||
Assert::assertArrayHasKey($key, $valueAsArray, $message);
|
||||
|
||||
if ($message === '') {
|
||||
$message = sprintf(
|
||||
'Failed asserting that an array has a key %s with the value %s.',
|
||||
$this->export($key),
|
||||
$this->export($valueAsArray[$key]),
|
||||
);
|
||||
}
|
||||
$assertMessage = $message !== '' ? $message : sprintf(
|
||||
'Failed asserting that an array has a key %s with the value %s.',
|
||||
$this->export($key),
|
||||
$this->export($valueAsArray[$key]),
|
||||
);
|
||||
|
||||
Assert::assertEquals($value, $valueAsArray[$key], $message);
|
||||
Assert::assertEquals($value, $valueAsArray[$key], $assertMessage);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@ -802,7 +800,7 @@ final class Expectation
|
||||
* @param iterable<string, mixed> $object
|
||||
* @return self<TValue>
|
||||
*/
|
||||
public function toMatchObject(iterable $object, string $message = ''): self
|
||||
public function toMatchObject(object|iterable $object, string $message = ''): self
|
||||
{
|
||||
foreach ((array) $object as $property => $value) {
|
||||
if (! is_object($this->value) && ! is_string($this->value)) {
|
||||
@ -814,15 +812,13 @@ final class Expectation
|
||||
/* @phpstan-ignore-next-line */
|
||||
$propertyValue = $this->value->{$property};
|
||||
|
||||
if ($message === '') {
|
||||
$message = sprintf(
|
||||
'Failed asserting that an object has a property %s with the value %s.',
|
||||
$this->export($property),
|
||||
$this->export($propertyValue),
|
||||
);
|
||||
}
|
||||
$assertMessage = $message !== '' ? $message : sprintf(
|
||||
'Failed asserting that an object has a property %s with the value %s.',
|
||||
$this->export($property),
|
||||
$this->export($propertyValue),
|
||||
);
|
||||
|
||||
Assert::assertEquals($value, $propertyValue, $message);
|
||||
Assert::assertEquals($value, $propertyValue, $assertMessage);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@ -1158,4 +1154,21 @@ final class Expectation
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value can be converted to a slug
|
||||
*
|
||||
* @return self<TValue>
|
||||
*/
|
||||
public function toBeSlug(string $message = ''): self
|
||||
{
|
||||
if ($message === '') {
|
||||
$message = "Failed asserting that {$this->value} can be converted to a slug.";
|
||||
}
|
||||
|
||||
$slug = Str::slugify((string) $this->value);
|
||||
Assert::assertNotEmpty($slug, $message);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,14 +12,14 @@ trait Describable
|
||||
/**
|
||||
* Note: this is property is not used; however, it gets added automatically by rector php.
|
||||
*
|
||||
* @var array<int, string>
|
||||
* @var array<int, \Pest\Support\Description>
|
||||
*/
|
||||
public array $__describing;
|
||||
|
||||
/**
|
||||
* The describing of the test case.
|
||||
*
|
||||
* @var array<int, string>
|
||||
* @var array<int, \Pest\Support\Description>
|
||||
*/
|
||||
public array $describing = [];
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ namespace Pest\PendingCalls;
|
||||
|
||||
use Closure;
|
||||
use Pest\Support\Backtrace;
|
||||
use Pest\Support\Description;
|
||||
use Pest\TestSuite;
|
||||
|
||||
/**
|
||||
@ -16,7 +17,7 @@ final class DescribeCall
|
||||
/**
|
||||
* The current describe call.
|
||||
*
|
||||
* @var array<int, string>
|
||||
* @var array<int, Description>
|
||||
*/
|
||||
private static array $describing = [];
|
||||
|
||||
@ -31,7 +32,7 @@ final class DescribeCall
|
||||
public function __construct(
|
||||
public readonly TestSuite $testSuite,
|
||||
public readonly string $filename,
|
||||
public readonly string $description,
|
||||
public readonly Description $description,
|
||||
public readonly Closure $tests
|
||||
) {
|
||||
//
|
||||
@ -40,7 +41,7 @@ final class DescribeCall
|
||||
/**
|
||||
* What is the current describing.
|
||||
*
|
||||
* @return array<int, string>
|
||||
* @return array<int, Description>
|
||||
*/
|
||||
public static function describing(): array
|
||||
{
|
||||
|
||||
@ -12,6 +12,7 @@ use Pest\Factories\Attribute;
|
||||
use Pest\Factories\TestCaseMethodFactory;
|
||||
use Pest\Mutate\Repositories\ConfigurationRepository;
|
||||
use Pest\PendingCalls\Concerns\Describable;
|
||||
use Pest\Plugins\Environment;
|
||||
use Pest\Plugins\Only;
|
||||
use Pest\Support\Backtrace;
|
||||
use Pest\Support\Container;
|
||||
@ -178,10 +179,9 @@ final class TestCall // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the current test multiple times with
|
||||
* each item of the given `iterable`.
|
||||
* Runs the current test multiple times with each item of the given `iterable`.
|
||||
*
|
||||
* @param array<\Closure|iterable<int|string, mixed>|string> $data
|
||||
* @param Closure|iterable<array-key, mixed>|string $data
|
||||
*/
|
||||
public function with(Closure|iterable|string ...$data): self
|
||||
{
|
||||
@ -315,6 +315,61 @@ final class TestCall // @phpstan-ignore-line
|
||||
: $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Weather the current test is running on a CI environment.
|
||||
*/
|
||||
private function runningOnCI(): bool
|
||||
{
|
||||
foreach ([
|
||||
'CI',
|
||||
'GITHUB_ACTIONS',
|
||||
'GITLAB_CI',
|
||||
'CIRCLECI',
|
||||
'TRAVIS',
|
||||
'APPVEYOR',
|
||||
'BITBUCKET_BUILD_NUMBER',
|
||||
'BUILDKITE',
|
||||
'TEAMCITY_VERSION',
|
||||
'JENKINS_URL',
|
||||
'SYSTEM_COLLECTIONURI',
|
||||
'CI_NAME',
|
||||
'TASKCLUSTER_ROOT_URL',
|
||||
'DRONE',
|
||||
'WERCKER',
|
||||
'NEVERCODE',
|
||||
'SEMAPHORE',
|
||||
'NETLIFY',
|
||||
'NOW_BUILDER',
|
||||
] as $env) {
|
||||
if (getenv($env) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return Environment::name() === Environment::CI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the current test when running on a CI environments.
|
||||
*/
|
||||
public function skipOnCI(): self
|
||||
{
|
||||
if ($this->runningOnCI()) {
|
||||
return $this->skip('This test is skipped on [CI].');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function skipLocally(): self
|
||||
{
|
||||
if ($this->runningOnCI() === false) {
|
||||
return $this->skip('This test is skipped [locally].');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the current test unless the given test is running on Windows.
|
||||
*/
|
||||
@ -604,18 +659,29 @@ final class TestCall // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets that the current test covers nothing.
|
||||
* Adds one or more references to the tested method or class. This helps
|
||||
* to link test cases to the source code for easier navigation.
|
||||
*
|
||||
* @param array<class-string|string>|class-string ...$classes
|
||||
*/
|
||||
public function coversNothing(): self
|
||||
public function references(string|array ...$classes): self
|
||||
{
|
||||
$this->testCaseMethod->attributes[] = new Attribute(
|
||||
\PHPUnit\Framework\Attributes\CoversNothing::class,
|
||||
[],
|
||||
);
|
||||
assert($classes !== []);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one or more references to the tested method or class. This helps
|
||||
* to link test cases to the source code for easier navigation.
|
||||
*
|
||||
* @param array<class-string|string>|class-string ...$classes
|
||||
*/
|
||||
public function see(string|array ...$classes): self
|
||||
{
|
||||
return $this->references(...$classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the test runner that no expectations happen in this test,
|
||||
* and its purpose is simply to check whether the given code can
|
||||
@ -693,7 +759,12 @@ final class TestCall // @phpstan-ignore-line
|
||||
$this->testSuite->tests->set($this->testCaseMethod);
|
||||
|
||||
if (! is_null($testCase = $this->testSuite->tests->get($this->filename))) {
|
||||
$testCase->attributes = array_merge($testCase->attributes, $this->testCaseFactoryAttributes);
|
||||
$attributesToMerge = array_filter(
|
||||
$this->testCaseFactoryAttributes,
|
||||
fn (Attribute $attributeToMerge): bool => array_filter($testCase->attributes, fn (Attribute $attribute): bool => serialize($attributeToMerge) === serialize($attribute)) === []
|
||||
);
|
||||
|
||||
$testCase->attributes = array_merge($testCase->attributes, $attributesToMerge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ namespace Pest;
|
||||
|
||||
function version(): string
|
||||
{
|
||||
return '3.8.4';
|
||||
return '4.1.1';
|
||||
}
|
||||
|
||||
function testDirectory(string $file = ''): string
|
||||
|
||||
@ -21,7 +21,7 @@ final class Cache implements HandlesArguments
|
||||
/**
|
||||
* The temporary folder.
|
||||
*/
|
||||
private const TEMPORARY_FOLDER = __DIR__
|
||||
private const string TEMPORARY_FOLDER = __DIR__
|
||||
.DIRECTORY_SEPARATOR
|
||||
.'..'
|
||||
.DIRECTORY_SEPARATOR
|
||||
|
||||
@ -21,7 +21,7 @@ final class Configuration implements HandlesArguments, Terminable
|
||||
/**
|
||||
* The base PHPUnit file.
|
||||
*/
|
||||
public const BASE_PHPUNIT_FILE = __DIR__
|
||||
public const string BASE_PHPUNIT_FILE = __DIR__
|
||||
.DIRECTORY_SEPARATOR
|
||||
.'..'
|
||||
.DIRECTORY_SEPARATOR
|
||||
@ -34,7 +34,7 @@ final class Configuration implements HandlesArguments, Terminable
|
||||
*/
|
||||
public function handleArguments(array $arguments): array
|
||||
{
|
||||
if ($this->hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) {
|
||||
if ($this->hasArgument('--configuration', $arguments) || $this->hasArgument('-c', $arguments) || $this->hasCustomConfigurationFile()) {
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
|
||||
@ -17,20 +17,11 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
*/
|
||||
final class Coverage implements AddsOutput, HandlesArguments
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const COVERAGE_OPTION = 'coverage';
|
||||
private const string COVERAGE_OPTION = 'coverage';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const MIN_OPTION = 'min';
|
||||
private const string MIN_OPTION = 'min';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const EXACTLY_OPTION = 'exactly';
|
||||
private const string EXACTLY_OPTION = 'exactly';
|
||||
|
||||
/**
|
||||
* Whether it should show the coverage or not.
|
||||
|
||||
@ -14,12 +14,12 @@ final class Environment implements HandlesArguments
|
||||
/**
|
||||
* The continuous integration environment.
|
||||
*/
|
||||
public const CI = 'ci';
|
||||
public const string CI = 'ci';
|
||||
|
||||
/**
|
||||
* The local environment.
|
||||
*/
|
||||
public const LOCAL = 'local';
|
||||
public const string LOCAL = 'local';
|
||||
|
||||
/**
|
||||
* The current environment.
|
||||
|
||||
@ -20,12 +20,12 @@ final readonly class Init implements HandlesArguments
|
||||
/**
|
||||
* The option the triggers the init job.
|
||||
*/
|
||||
private const INIT_OPTION = '--init';
|
||||
private const string INIT_OPTION = '--init';
|
||||
|
||||
/**
|
||||
* The files that will be created.
|
||||
*/
|
||||
private const STUBS = [
|
||||
private const array STUBS = [
|
||||
'phpunit.xml.stub' => 'phpunit.xml',
|
||||
'Pest.php.stub' => 'tests/Pest.php',
|
||||
'TestCase.php.stub' => 'tests/TestCase.php',
|
||||
|
||||
@ -5,7 +5,10 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins;
|
||||
|
||||
use Pest\Contracts\Plugins\Terminable;
|
||||
use Pest\Factories\Attribute;
|
||||
use Pest\Factories\TestCaseMethodFactory;
|
||||
use Pest\PendingCalls\TestCall;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -15,7 +18,7 @@ final class Only implements Terminable
|
||||
/**
|
||||
* The temporary folder.
|
||||
*/
|
||||
private const TEMPORARY_FOLDER = __DIR__
|
||||
private const string TEMPORARY_FOLDER = __DIR__
|
||||
.DIRECTORY_SEPARATOR
|
||||
.'..'
|
||||
.DIRECTORY_SEPARATOR
|
||||
@ -23,28 +26,19 @@ final class Only implements Terminable
|
||||
.DIRECTORY_SEPARATOR
|
||||
.'.temp';
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function terminate(): void
|
||||
{
|
||||
if (Parallel::isWorker()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
|
||||
|
||||
if (file_exists($lockFile)) {
|
||||
unlink($lockFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the lock file.
|
||||
*/
|
||||
public static function enable(TestCall $testCall, string $group = '__pest_only'): void
|
||||
public static function enable(TestCall|TestCaseMethodFactory $testCall, string $group = '__pest_only'): void
|
||||
{
|
||||
$testCall->group($group);
|
||||
if ($testCall instanceof TestCall) {
|
||||
$testCall->group($group);
|
||||
} else {
|
||||
$testCall->attributes[] = new Attribute(
|
||||
Group::class,
|
||||
[$group],
|
||||
);
|
||||
}
|
||||
|
||||
if (Environment::name() === Environment::CI || Parallel::isWorker()) {
|
||||
return;
|
||||
@ -88,4 +82,20 @@ final class Only implements Terminable
|
||||
|
||||
return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function terminate(): void
|
||||
{
|
||||
if (Parallel::isWorker()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
|
||||
|
||||
if (file_exists($lockFile)) {
|
||||
unlink($lockFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,9 +23,9 @@ final class Parallel implements HandlesArguments
|
||||
{
|
||||
use HandleArguments;
|
||||
|
||||
private const GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_';
|
||||
private const string GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_';
|
||||
|
||||
private const HANDLERS = [
|
||||
private const array HANDLERS = [
|
||||
Parallel\Handlers\Parallel::class,
|
||||
Parallel\Handlers\Pest::class,
|
||||
Parallel\Handlers\Laravel::class,
|
||||
@ -34,7 +34,7 @@ final class Parallel implements HandlesArguments
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request'];
|
||||
private const array UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request'];
|
||||
|
||||
/**
|
||||
* Whether the given command line arguments indicate that the test suite should be run in parallel.
|
||||
|
||||
@ -18,7 +18,7 @@ final class Parallel implements HandlesArguments
|
||||
/**
|
||||
* The list of arguments to remove.
|
||||
*/
|
||||
private const ARGS_TO_REMOVE = [
|
||||
private const array ARGS_TO_REMOVE = [
|
||||
'--parallel',
|
||||
'-p',
|
||||
'--no-output',
|
||||
|
||||
@ -11,6 +11,7 @@ final class CleanConsoleOutput extends ConsoleOutput
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
#[\Override]
|
||||
protected function doWrite(string $message, bool $newline): void // @pest-arch-ignore-line
|
||||
{
|
||||
if ($this->isOpeningHeadline($message)) {
|
||||
|
||||
@ -59,10 +59,10 @@ final class ResultPrinter
|
||||
private readonly OutputInterface $output,
|
||||
private readonly Options $options
|
||||
) {
|
||||
$this->printer = new class($this->output) implements Printer
|
||||
$this->printer = new readonly class($this->output) implements Printer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OutputInterface $output,
|
||||
private OutputInterface $output,
|
||||
) {}
|
||||
|
||||
public function print(string $buffer): void
|
||||
|
||||
@ -17,6 +17,7 @@ use ParaTest\WrapperRunner\WrapperWorker;
|
||||
use Pest\Result;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Event\Facade as EventFacade;
|
||||
use PHPUnit\Event\Test\AfterLastTestMethodFailed;
|
||||
use PHPUnit\Event\TestRunner\WarningTriggered;
|
||||
use PHPUnit\Runner\CodeCoverage;
|
||||
use PHPUnit\Runner\ResultCache\DefaultResultCache;
|
||||
@ -50,7 +51,7 @@ final class WrapperRunner implements RunnerInterface
|
||||
/**
|
||||
* The time to sleep between cycles.
|
||||
*/
|
||||
private const CYCLE_SLEEP = 10000;
|
||||
private const int CYCLE_SLEEP = 10000;
|
||||
|
||||
/**
|
||||
* The result printer.
|
||||
@ -313,27 +314,42 @@ final class WrapperRunner implements RunnerInterface
|
||||
$testResult = unserialize($contents);
|
||||
assert($testResult instanceof TestResult);
|
||||
|
||||
/** @var list<AfterLastTestMethodFailed> $failedEvents */
|
||||
$failedEvents = array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents());
|
||||
|
||||
$testResultSum = new TestResult(
|
||||
(int) $testResultSum->hasTests() + (int) $testResult->hasTests(),
|
||||
$testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
|
||||
$testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(),
|
||||
array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()),
|
||||
array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()),
|
||||
$failedEvents,
|
||||
array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()),
|
||||
array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()),
|
||||
array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()),
|
||||
array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->testRunnerTriggeredNoticeEvents(), $testResult->testRunnerTriggeredNoticeEvents()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->errors(), $testResult->errors()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->notices(), $testResult->notices()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->warnings(), $testResult->warnings()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()),
|
||||
// @phpstan-ignore-next-line
|
||||
array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()),
|
||||
$testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
|
||||
);
|
||||
@ -351,8 +367,10 @@ final class WrapperRunner implements RunnerInterface
|
||||
$testResultSum->testMarkedIncompleteEvents(),
|
||||
$testResultSum->testTriggeredPhpunitDeprecationEvents(),
|
||||
$testResultSum->testTriggeredPhpunitErrorEvents(),
|
||||
$testResultSum->testTriggeredPhpunitNoticeEvents(),
|
||||
$testResultSum->testTriggeredPhpunitWarningEvents(),
|
||||
$testResultSum->testRunnerTriggeredDeprecationEvents(),
|
||||
$testResultSum->testRunnerTriggeredNoticeEvents(),
|
||||
array_values(array_filter(
|
||||
$testResultSum->testRunnerTriggeredWarningEvents(),
|
||||
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')
|
||||
|
||||
@ -34,7 +34,7 @@ final class CompactPrinter
|
||||
/**
|
||||
* @var array<string, array<int, string>>
|
||||
*/
|
||||
private const LOOKUP_TABLE = [
|
||||
private const array LOOKUP_TABLE = [
|
||||
'.' => ['gray', '.'],
|
||||
'S' => ['yellow', 's'],
|
||||
'T' => ['cyan', 't'],
|
||||
@ -131,14 +131,14 @@ final class CompactPrinter
|
||||
$status['collected'],
|
||||
$status['threshold'],
|
||||
$status['roots'],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0.00,
|
||||
0.00,
|
||||
0.00,
|
||||
0.00,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
);
|
||||
|
||||
$telemetry = new Info(
|
||||
|
||||
177
src/Plugins/Shard.php
Normal file
177
src/Plugins/Shard.php
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins;
|
||||
|
||||
use Pest\Contracts\Plugins\AddsOutput;
|
||||
use Pest\Contracts\Plugins\HandlesArguments;
|
||||
use Pest\Exceptions\InvalidOption;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Shard implements AddsOutput, HandlesArguments
|
||||
{
|
||||
use Concerns\HandleArguments;
|
||||
|
||||
private const string SHARD_OPTION = 'shard';
|
||||
|
||||
/**
|
||||
* The shard index and total number of shards.
|
||||
*
|
||||
* @var array{
|
||||
* index: int,
|
||||
* total: int,
|
||||
* testsRan: int,
|
||||
* testsCount: int
|
||||
* }|null
|
||||
*/
|
||||
private static ?array $shard = null;
|
||||
|
||||
/**
|
||||
* Creates a new Plugin instance.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly OutputInterface $output,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function handleArguments(array $arguments): array
|
||||
{
|
||||
if (! $this->hasArgument('--shard', $arguments)) {
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$input = new ArgvInput($arguments);
|
||||
|
||||
['index' => $index, 'total' => $total] = self::getShard($input);
|
||||
|
||||
$arguments = $this->popArgument("--shard=$index/$total", $this->popArgument('--shard', $this->popArgument(
|
||||
"$index/$total",
|
||||
$arguments,
|
||||
)));
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
$tests = $this->allTests($arguments);
|
||||
$testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? [];
|
||||
|
||||
self::$shard = [
|
||||
'index' => $index,
|
||||
'total' => $total,
|
||||
'testsRan' => count($testsToRun),
|
||||
'testsCount' => count($tests),
|
||||
];
|
||||
|
||||
return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all tests that the test suite would run.
|
||||
*
|
||||
* @param list<string> $arguments
|
||||
* @return list<string>
|
||||
*/
|
||||
private function allTests(array $arguments): array
|
||||
{
|
||||
$output = (new Process([
|
||||
'php',
|
||||
...$this->removeParallelArguments($arguments),
|
||||
'--list-tests',
|
||||
]))->mustRun()->getOutput();
|
||||
|
||||
preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches);
|
||||
|
||||
return array_values(array_unique($matches[1]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $arguments
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function removeParallelArguments(array $arguments): array
|
||||
{
|
||||
return array_filter($arguments, fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the filter argument for the given tests to run.
|
||||
*/
|
||||
private function buildFilterArgument(mixed $testsToRun): string
|
||||
{
|
||||
return addslashes(implode('|', $testsToRun));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds output after the Test Suite execution.
|
||||
*/
|
||||
public function addOutput(int $exitCode): int
|
||||
{
|
||||
if (self::$shard === null) {
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
[
|
||||
'index' => $index,
|
||||
'total' => $total,
|
||||
'testsRan' => $testsRan,
|
||||
'testsCount' => $testsCount,
|
||||
] = self::$shard;
|
||||
|
||||
$this->output->writeln(sprintf(
|
||||
' <fg=gray>Shard:</> <fg=default>%d of %d</> — %d file%s ran, out of %d.',
|
||||
$index,
|
||||
$total,
|
||||
$testsRan,
|
||||
$testsRan === 1 ? '' : 's',
|
||||
$testsCount,
|
||||
));
|
||||
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shard information.
|
||||
*
|
||||
* @return array{index: int, total: int}
|
||||
*/
|
||||
public static function getShard(InputInterface $input): array
|
||||
{
|
||||
if ($input->hasParameterOption('--'.self::SHARD_OPTION)) {
|
||||
$shard = $input->getParameterOption('--'.self::SHARD_OPTION);
|
||||
} else {
|
||||
$shard = null;
|
||||
}
|
||||
|
||||
if (! is_string($shard) || ! preg_match('/^\d+\/\d+$/', $shard)) {
|
||||
throw new InvalidOption('The [--shard] option must be in the format "index/total".');
|
||||
}
|
||||
|
||||
[$index, $total] = explode('/', $shard);
|
||||
|
||||
if (! is_numeric($index) || ! is_numeric($total)) {
|
||||
throw new InvalidOption('The [--shard] option must be in the format "index/total".');
|
||||
}
|
||||
|
||||
if ($index <= 0 || $total <= 0 || $index > $total) {
|
||||
throw new InvalidOption('The [--shard] option index must be a non-negative integer less than the total number of shards.');
|
||||
}
|
||||
|
||||
$index = (int) $index;
|
||||
$total = (int) $total;
|
||||
|
||||
return [
|
||||
'index' => $index,
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,7 @@ final class Verbose implements HandlesArguments
|
||||
/**
|
||||
* The list of verbosity levels.
|
||||
*/
|
||||
private const VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
|
||||
private const array VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
|
||||
@ -19,7 +19,7 @@ use function sprintf;
|
||||
*/
|
||||
final class DatasetsRepository
|
||||
{
|
||||
private const SEPARATOR = '>>';
|
||||
private const string SEPARATOR = '>>';
|
||||
|
||||
/**
|
||||
* Holds the datasets.
|
||||
@ -67,11 +67,11 @@ final class DatasetsRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Closure|array<int|string, mixed>
|
||||
* @return array<int|string, mixed>
|
||||
*
|
||||
* @throws ShouldNotHappen
|
||||
*/
|
||||
public static function get(string $filename, string $description): Closure|array // @phpstan-ignore-line
|
||||
public static function get(string $filename, string $description): array // @phpstan-ignore-line
|
||||
{
|
||||
$dataset = self::$withs[$filename.self::SEPARATOR.$description];
|
||||
|
||||
@ -191,6 +191,7 @@ final class DatasetsRepository
|
||||
return str_starts_with($currentTestFile, $datasetScope);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
/** @var string|null $closestScopeDatasetKey */
|
||||
$closestScopeDatasetKey = array_reduce(
|
||||
array_keys($matchingDatasets),
|
||||
fn (string|int|null $keyA, string|int|null $keyB): string|int|null => $keyA !== null && strlen((string) $keyA) > strlen((string) $keyB) ? $keyA : $keyB
|
||||
|
||||
@ -19,6 +19,7 @@ final class SnapshotRepository
|
||||
* Creates a snapshot repository instance.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $rootPath,
|
||||
private readonly string $testsPath,
|
||||
private readonly string $snapshotsPath,
|
||||
) {}
|
||||
@ -103,7 +104,19 @@ final class SnapshotRepository
|
||||
*/
|
||||
private function getSnapshotFilename(): string
|
||||
{
|
||||
$relativePath = str_replace($this->testsPath, '', TestSuite::getInstance()->getFilename());
|
||||
$testFile = TestSuite::getInstance()->getFilename();
|
||||
|
||||
if (str_starts_with($testFile, $this->testsPath)) {
|
||||
// if the test file is in the tests directory
|
||||
$startPath = $this->testsPath;
|
||||
} else {
|
||||
// if the test file is in the app, src, etc. directory
|
||||
$startPath = $this->rootPath;
|
||||
}
|
||||
|
||||
// relative path: we use substr() and not str_replace() to remove the start path
|
||||
// for instance, if the $startPath is /app/ and the $testFile is /app/app/tests/Unit/ExampleTest.php, we should only remove the first /app/ from the path
|
||||
$relativePath = substr($testFile, strlen($startPath));
|
||||
|
||||
// remove extension from filename
|
||||
$relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.'));
|
||||
|
||||
@ -4,20 +4,16 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest;
|
||||
|
||||
use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection;
|
||||
use PHPUnit\TestRunner\TestResult\TestResult;
|
||||
use PHPUnit\TextUI\Configuration\Configuration;
|
||||
use PHPUnit\TextUI\ShellExitCodeCalculator;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Result
|
||||
{
|
||||
private const SUCCESS_EXIT = 0;
|
||||
|
||||
private const FAILURE_EXIT = 1;
|
||||
|
||||
private const EXCEPTION_EXIT = 2;
|
||||
private const int SUCCESS_EXIT = 0;
|
||||
|
||||
/**
|
||||
* If the exit code is different from 0.
|
||||
@ -40,44 +36,8 @@ final class Result
|
||||
*/
|
||||
public static function exitCode(Configuration $configuration, TestResult $result): int
|
||||
{
|
||||
if ($result->wasSuccessful()) {
|
||||
if ($configuration->failOnWarning()) {
|
||||
$warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents()
|
||||
+ count($result->warnings())
|
||||
+ count($result->phpWarnings());
|
||||
$shell = new ShellExitCodeCalculator;
|
||||
|
||||
if ($warnings > 0) {
|
||||
return self::FAILURE_EXIT;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $result->hasTestTriggeredPhpunitWarningEvents()) {
|
||||
return self::SUCCESS_EXIT;
|
||||
}
|
||||
}
|
||||
|
||||
if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) {
|
||||
return self::FAILURE_EXIT;
|
||||
}
|
||||
|
||||
if ($result->wasSuccessful()) {
|
||||
if ($configuration->failOnRisky() && $result->hasTestConsideredRiskyEvents()) {
|
||||
$returnCode = self::FAILURE_EXIT;
|
||||
}
|
||||
|
||||
if ($configuration->failOnIncomplete() && $result->hasTestMarkedIncompleteEvents()) {
|
||||
$returnCode = self::FAILURE_EXIT;
|
||||
}
|
||||
|
||||
if ($configuration->failOnSkipped() && $result->hasTestSkippedEvents()) {
|
||||
$returnCode = self::FAILURE_EXIT;
|
||||
}
|
||||
}
|
||||
|
||||
if ($result->hasTestErroredEvents()) {
|
||||
return self::EXCEPTION_EXIT;
|
||||
}
|
||||
|
||||
return self::FAILURE_EXIT;
|
||||
return $shell->calculate($configuration, $result);
|
||||
}
|
||||
}
|
||||
|
||||
39
src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php
Normal file
39
src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Runner\Filter;
|
||||
|
||||
use Pest\Contracts\HasPrintableTestCaseName;
|
||||
use PHPUnit\Framework\Test;
|
||||
use RecursiveFilterIterator;
|
||||
use RecursiveIterator;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EnsureTestCaseIsInitiatedFilter extends RecursiveFilterIterator
|
||||
{
|
||||
/**
|
||||
* @param RecursiveIterator<int, Test> $iterator
|
||||
*/
|
||||
public function __construct(RecursiveIterator $iterator)
|
||||
{
|
||||
parent::__construct($iterator);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function accept(): bool
|
||||
{
|
||||
$test = $this->getInnerIterator()->current();
|
||||
|
||||
if ($test instanceof HasPrintableTestCaseName) {
|
||||
/** @phpstan-ignore-next-line */
|
||||
$test->__initializeTestCase();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -33,7 +33,7 @@ final class EnsureIgnorableTestCasesAreIgnored implements StartedSubscriber
|
||||
/** @var array<int, WarningTriggered> $testRunnerTriggeredWarningEvents */
|
||||
$testRunnerTriggeredWarningEvents = $property->getValue($collector);
|
||||
|
||||
$testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => $event->message() !== 'No tests found in class "Pest\TestCases\IgnorableTestCase".'));
|
||||
$testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => str_contains($event->message(), 'No tests found in class') === false));
|
||||
|
||||
$property->setValue($collector, $testRunnerTriggeredWarningEvents);
|
||||
}
|
||||
|
||||
@ -11,12 +11,9 @@ use Pest\Exceptions\ShouldNotHappen;
|
||||
*/
|
||||
final class Backtrace
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const FILE = 'file';
|
||||
private const string FILE = 'file';
|
||||
|
||||
private const BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS;
|
||||
private const int BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS;
|
||||
|
||||
/**
|
||||
* Returns the current test file.
|
||||
|
||||
@ -11,9 +11,9 @@ use function Pest\testDirectory;
|
||||
*/
|
||||
final class DatasetInfo
|
||||
{
|
||||
public const DATASETS_DIR_NAME = 'Datasets';
|
||||
public const string DATASETS_DIR_NAME = 'Datasets';
|
||||
|
||||
public const DATASETS_FILE_NAME = 'Datasets.php';
|
||||
public const string DATASETS_FILE_NAME = 'Datasets.php';
|
||||
|
||||
public static function isInsideADatasetsDirectory(string $file): bool
|
||||
{
|
||||
|
||||
21
src/Support/Description.php
Normal file
21
src/Support/Description.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Support;
|
||||
|
||||
final readonly class Description implements \Stringable
|
||||
{
|
||||
/**
|
||||
* Creates a new Description instance.
|
||||
*/
|
||||
public function __construct(private string $description) {}
|
||||
|
||||
/**
|
||||
* Returns the description as a string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@ use Throwable;
|
||||
*/
|
||||
final class ExceptionTrace
|
||||
{
|
||||
private const UNDEFINED_METHOD = 'Call to undefined method P\\';
|
||||
private const string UNDEFINED_METHOD = 'Call to undefined method P\\';
|
||||
|
||||
/**
|
||||
* Ensures the given closure reports the good execution context.
|
||||
|
||||
@ -15,7 +15,7 @@ final readonly class Exporter
|
||||
/**
|
||||
* The maximum number of items in an array to export.
|
||||
*/
|
||||
private const MAX_ARRAY_ITEMS = 3;
|
||||
private const int MAX_ARRAY_ITEMS = 3;
|
||||
|
||||
/**
|
||||
* Creates a new Exporter instance.
|
||||
|
||||
@ -13,7 +13,7 @@ use Throwable;
|
||||
*/
|
||||
final class HigherOrderMessage
|
||||
{
|
||||
public const UNDEFINED_METHOD = 'Method %s does not exist';
|
||||
public const string UNDEFINED_METHOD = 'Method %s does not exist';
|
||||
|
||||
/**
|
||||
* An optional condition that will determine if the message will be executed.
|
||||
|
||||
101
src/Support/Shell.php
Normal file
101
src/Support/Shell.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Support;
|
||||
|
||||
use Illuminate\Support\Env;
|
||||
use Laravel\Tinker\ClassAliasAutoloader;
|
||||
use Pest\TestSuite;
|
||||
use Psy\Configuration;
|
||||
use Psy\Shell as PsyShell;
|
||||
use Psy\VersionUpdater\Checker;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Shell
|
||||
{
|
||||
/**
|
||||
* Creates a new interactive shell.
|
||||
*/
|
||||
public static function open(): void
|
||||
{
|
||||
$config = new Configuration;
|
||||
|
||||
$config->setUpdateCheck(Checker::NEVER);
|
||||
|
||||
$config->getPresenter()->addCasters(self::casters());
|
||||
|
||||
$shell = new PsyShell($config);
|
||||
|
||||
$loader = self::tinkered($shell);
|
||||
|
||||
try {
|
||||
$shell->run();
|
||||
} finally {
|
||||
$loader?->unregister(); // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the casters for the Psy Shell.
|
||||
*
|
||||
* @return array<string, callable>
|
||||
*/
|
||||
private static function casters(): array
|
||||
{
|
||||
$casters = [
|
||||
'Illuminate\Support\Collection' => 'Laravel\Tinker\TinkerCaster::castCollection',
|
||||
'Illuminate\Support\HtmlString' => 'Laravel\Tinker\TinkerCaster::castHtmlString',
|
||||
'Illuminate\Support\Stringable' => 'Laravel\Tinker\TinkerCaster::castStringable',
|
||||
];
|
||||
|
||||
if (class_exists('Illuminate\Database\Eloquent\Model')) {
|
||||
$casters['Illuminate\Database\Eloquent\Model'] = 'Laravel\Tinker\TinkerCaster::castModel';
|
||||
}
|
||||
|
||||
if (class_exists('Illuminate\Process\ProcessResult')) {
|
||||
$casters['Illuminate\Process\ProcessResult'] = 'Laravel\Tinker\TinkerCaster::castProcessResult';
|
||||
}
|
||||
|
||||
if (class_exists('Illuminate\Foundation\Application')) {
|
||||
$casters['Illuminate\Foundation\Application'] = 'Laravel\Tinker\TinkerCaster::castApplication';
|
||||
}
|
||||
|
||||
if (function_exists('app') === false) {
|
||||
return $casters; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
$config = app()->make('config');
|
||||
|
||||
return array_merge($casters, (array) $config->get('tinker.casters', []));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tinkers the current shell, if the Tinker package is available.
|
||||
*/
|
||||
private static function tinkered(PsyShell $shell): ?object
|
||||
{
|
||||
if (function_exists('app') === false
|
||||
|| ! class_exists(Env::class)
|
||||
|| ! class_exists(ClassAliasAutoloader::class)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = Env::get('COMPOSER_VENDOR_DIR', app()->basePath().DIRECTORY_SEPARATOR.'vendor');
|
||||
|
||||
$path .= '/composer/autoload_classmap.php';
|
||||
|
||||
if (! file_exists($path)) {
|
||||
$path = TestSuite::getInstance()->rootPath.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'composer'.DIRECTORY_SEPARATOR.'autoload_classmap.php';
|
||||
}
|
||||
|
||||
$config = app()->make('config');
|
||||
|
||||
return ClassAliasAutoloader::register(
|
||||
$shell, $path, $config->get('tinker.alias', []), $config->get('tinker.dont_alias', [])
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -13,12 +13,9 @@ final class Str
|
||||
* Pool of alpha-numeric characters for generating (unsafe) random strings
|
||||
* from.
|
||||
*/
|
||||
private const POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
private const string POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const PREFIX = '__pest_evaluable_';
|
||||
private const string PREFIX = '__pest_evaluable_';
|
||||
|
||||
/**
|
||||
* Create a (unsecure & non-cryptographically safe) random alpha-numeric
|
||||
@ -104,7 +101,7 @@ final class Str
|
||||
/**
|
||||
* Creates a describe block as `$describeDescription` → `$testDescription` format.
|
||||
*
|
||||
* @param array<int, string> $describeDescriptions
|
||||
* @param array<int, Description> $describeDescriptions
|
||||
*/
|
||||
public static function describe(array $describeDescriptions, string $testDescription): string
|
||||
{
|
||||
@ -120,4 +117,14 @@ final class Str
|
||||
{
|
||||
return (bool) filter_var($value, FILTER_VALIDATE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given `$target` to a URL-friendly "slug".
|
||||
*/
|
||||
public static function slugify(string $target): string
|
||||
{
|
||||
$target = preg_replace('/[^a-zA-Z0-9]+/', '-', $target);
|
||||
|
||||
return strtolower(trim((string) $target, '-'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,6 +78,7 @@ final class TestSuite
|
||||
$this->afterAll = new AfterAllRepository;
|
||||
$this->rootPath = (string) realpath($rootPath);
|
||||
$this->snapshots = new SnapshotRepository(
|
||||
$this->rootPath,
|
||||
implode(DIRECTORY_SEPARATOR, [$this->rootPath, $this->testPath]),
|
||||
implode(DIRECTORY_SEPARATOR, ['.pest', 'snapshots']),
|
||||
);
|
||||
@ -101,7 +102,7 @@ final class TestSuite
|
||||
}
|
||||
|
||||
if (! self::$instance instanceof self) {
|
||||
Panic::with(new InvalidPestCommand);
|
||||
throw new InvalidPestCommand;
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
@ -119,7 +120,7 @@ final class TestSuite
|
||||
assert($this->test instanceof TestCase);
|
||||
|
||||
$description = str_replace('__pest_evaluable_', '', $this->test->name());
|
||||
$datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($this->test->dataSetAsStringWithData()));
|
||||
$datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($this->test->dataSetAsString()));
|
||||
|
||||
return str_replace(' ', '_', $description.$datasetAsString);
|
||||
}
|
||||
|
||||
35
tests-external/Features/Expect/toMatchSnapshot.php
Normal file
35
tests-external/Features/Expect/toMatchSnapshot.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Pest\TestSuite;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->snapshotable = <<<'HTML'
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>Snapshot</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
});
|
||||
|
||||
test('pass with dataset', function ($data) {
|
||||
TestSuite::getInstance()->snapshots->save($this->snapshotable);
|
||||
[$filename] = TestSuite::getInstance()->snapshots->get();
|
||||
|
||||
expect($filename)->toStartWith('tests/.pest/snapshots-external/')
|
||||
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap')
|
||||
->and($this->snapshotable)->toMatchSnapshot();
|
||||
})->with(['my-datas-set-value']);
|
||||
|
||||
describe('within describe', function () {
|
||||
test('pass with dataset', function ($data) {
|
||||
TestSuite::getInstance()->snapshots->save($this->snapshotable);
|
||||
[$filename] = TestSuite::getInstance()->snapshots->get();
|
||||
|
||||
expect($filename)->toStartWith('tests/.pest/snapshots-external/')
|
||||
->toEndWith('pass_with_dataset_with_data_set_____my_datas_set_value___.snap')
|
||||
->and($this->snapshotable)->toMatchSnapshot();
|
||||
});
|
||||
})->with(['my-datas-set-value']);
|
||||
@ -0,0 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>Snapshot</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>Snapshot</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>Snapshot</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user