mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 15:57:21 +01:00
Compare commits
293 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7015fe59c | |||
| 7281e0ded7 | |||
| 5de8693e3b | |||
| 7d80f1d20e | |||
| b3119cc120 | |||
| 4e294edf76 | |||
| f96a1b2786 | |||
| a49cf7edc5 | |||
| b0f6a74cb6 | |||
| aaa226f6a6 | |||
| 69cb752d02 | |||
| cf00e58b7d | |||
| 1f39b28e2c | |||
| 9fcbca69d4 | |||
| 3a4329ddc7 | |||
| dd01229d7b | |||
| c7e4efcea4 | |||
| df3205e814 | |||
| bc57a84e77 | |||
| bc39830d8a | |||
| 3a566b100e | |||
| 9fe61e0e56 | |||
| e86bec3e68 | |||
| 58b8f3cc5d | |||
| c157b661f2 | |||
| be90610f17 | |||
| 1701a306c3 | |||
| 064ab3fc2e | |||
| 44e315df98 | |||
| 62694c14b9 | |||
| 7c43c1c583 | |||
| 6a96aed654 | |||
| b1c997a869 | |||
| b4172e2c2e | |||
| ae419afd36 | |||
| 27aa305897 | |||
| f5820bd670 | |||
| 41fd831153 | |||
| 51340439e8 | |||
| 1a39826935 | |||
| ae1da79ac1 | |||
| 00990efc97 | |||
| 477d20a54f | |||
| b7b16096db | |||
| 4105e33c39 | |||
| 08b09f2e98 | |||
| b0fab7e437 | |||
| 8e3444e1db | |||
| fc7a4182b5 | |||
| b7406938ac | |||
| 314caabd1d | |||
| 65cabf91b1 | |||
| f91c6c1e1e | |||
| 843dbbf18a | |||
| 47fb1d7763 | |||
| 639df4cb43 | |||
| e54e4a0178 | |||
| 7749775f50 | |||
| f11f3aa0a4 | |||
| 33817013fe | |||
| 7f11ace329 | |||
| 3d776f1f20 | |||
| d5ced0a5ca | |||
| af1e214be4 | |||
| 7f9b50974a | |||
| cd5272d8cc | |||
| a7b2039175 | |||
| 72cf695554 | |||
| 50960a96e9 | |||
| 507df757a1 | |||
| 8722b3fc3c | |||
| 19eca6e338 | |||
| 6b523d6963 | |||
| a350545803 | |||
| 71c2e97c9f | |||
| 98a12012bf | |||
| 027f4e4832 | |||
| 92523a6f39 | |||
| ed38fb644f | |||
| 39b66bf01d | |||
| 165c879fe6 | |||
| 4c8bf4b2fd | |||
| 1ee36f584d | |||
| 1b0a846a81 | |||
| e3e518747f | |||
| 0b96b8f630 | |||
| 711a60c2db | |||
| e7132fa012 | |||
| 3b72bbd7fe | |||
| 273edb864c | |||
| fcb60f3c4a | |||
| 91bb7589e2 | |||
| e524bf5f73 | |||
| 27414ce19f | |||
| fbc9e704e2 | |||
| ee6b3ed062 | |||
| 4c88590b89 | |||
| 66e59efec6 | |||
| f692be3637 | |||
| 127ad618d3 | |||
| da04ba62a8 | |||
| d187566e63 | |||
| 3e86e158b2 | |||
| d6c6489e93 | |||
| ee70a3cfea | |||
| 7a6f33f139 | |||
| 55218bcf78 | |||
| e29302300f | |||
| 2a47b514ec | |||
| 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 | |||
| 7d77bbf1bb | |||
| 163479ae60 | |||
| c3bfdf130e | |||
| 8c403a57c2 | |||
| 97c136cd94 | |||
| d6cbd12d8b | |||
| 49bf00024f | |||
| dd44ac4195 | |||
| 5d2aafd2a3 | |||
| 0fc9d4dfe0 | |||
| 02b1ffb334 | |||
| c62cc3fef0 | |||
| 909d778da3 | |||
| 7711a52fe9 | |||
| 99c9f4e5d8 | |||
| a310796165 | |||
| db9243ca2e | |||
| 635e3b4c41 | |||
| 791734a29c | |||
| 8cfb0acf46 | |||
| bf67407ba5 | |||
| efdc84e115 | |||
| d1608bf33d | |||
| 4f6140fdb1 | |||
| 442a58d07f | |||
| 19e9267021 | |||
| c6244a8712 | |||
| eed68f2840 | |||
| 6080f51a0b | |||
| e0f07be017 | |||
| 42e1b9f17f | |||
| 0171617c1d | |||
| 2e11e9e65d | |||
| 4969526ef2 | |||
| d7b1c36fdd | |||
| 003fc96e8f | |||
| f68d11ccae | |||
| e46d499384 | |||
| 490f321a0d | |||
| 174645caa2 | |||
| ed70c9dc2b | |||
| 157a753d87 | |||
| 2c3a53f6cd | |||
| 0bdaef29e9 | |||
| 1ad30a97b3 | |||
| a5317c5640 | |||
| 66ceb64faa | |||
| fa4098db8d | |||
| 4a987d3d5c | |||
| 4079a08f5f | |||
| e4aab77a34 | |||
| 50ff347b59 | |||
| b5b8fab09b | |||
| c4c9e915f4 | |||
| e834527db2 | |||
| 23f130b0f9 | |||
| 0cb8c42497 | |||
| fe4b5e5e1f | |||
| 8ee9d66d80 | |||
| 7760d945bb | |||
| 709ecb1ba2 | |||
| 6afb36519d | |||
| 150bb9478d | |||
| bf3178473d | |||
| d2eb94d723 | |||
| 9688b83a3d | |||
| 675372c794 | |||
| c18636b3d5 | |||
| 1ac594bdf0 | |||
| 145294a4a3 | |||
| c2cabaeae6 | |||
| 918a8fc169 | |||
| 5d32dd0641 | |||
| 982353fb38 | |||
| 2eefa8b88d | |||
| 787d5492ac | |||
| 06a0bd9b0b | |||
| 5331b44a18 | |||
| 91afc81222 | |||
| 179d46ce97 | |||
| fa2bc1e536 | |||
| eaeb133c77 | |||
| cf57ea1f94 | |||
| 0b7f4f2384 | |||
| 2903a7e621 | |||
| b8964375c7 | |||
| bdcb883829 | |||
| 8a7e7f39ef | |||
| 53c94600cb | |||
| 67f217852c | |||
| 1bad148487 | |||
| e24f137b8e | |||
| 6d9189f3f5 | |||
| 6968094e2b | |||
| 9510d4a2f9 | |||
| cd2eb3504b | |||
| 7c639cdbbd | |||
| 1513ede73b | |||
| 8c65197881 | |||
| a6cd83665c | |||
| 0c57142c03 | |||
| 3f65af9fdf | |||
| 42d89814e3 | |||
| 1e3156a5b6 | |||
| 97713c0832 | |||
| 62b0e3c9df | |||
| 647de2f1cf | |||
| 0a7bff0d24 | |||
| 7618434580 | |||
| dd7d150caa | |||
| 1e0bb88b73 | |||
| 83b76d7c2e | |||
| 5a870b3940 | |||
| 1115c64186 | |||
| e38a271ca2 | |||
| 43703ab40a | |||
| 86452765a4 | |||
| b8a1b7e5cc | |||
| 5fe79d9c18 | |||
| 2744da4292 | |||
| 87f4e5e7b3 | |||
| bb3decf3cc | |||
| 4e2987d438 | |||
| a25158bce8 | |||
| 49e77b1d4c | |||
| 989e43d1a0 | |||
| 7cd42aafd8 | |||
| 48a1de273f | |||
| 970e16e949 | |||
| 432ff221c6 | |||
| a55da85dd2 | |||
| f291cd1603 | |||
| 5de0c2254a | |||
| b98ce0ced3 | |||
| 28772c2609 | |||
| 452ffaf8df | |||
| e8338405b5 | |||
| 92bc1decd9 | |||
| e3bfcbe5f1 | |||
| ba7eb70a5d | |||
| 74ff3b8cd9 | |||
| ab0b4a1b4e | |||
| 169b76458e | |||
| 668685498f | |||
| bab193e7e1 | |||
| f720be862e |
33
.github/workflows/static.yml
vendored
33
.github/workflows/static.yml
vendored
@ -2,10 +2,15 @@ name: Static Analysis
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches: [4.x]
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * *'
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: static-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
static:
|
static:
|
||||||
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
|
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
|
||||||
@ -19,20 +24,38 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: 8.2
|
php-version: 8.3
|
||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
coverage: none
|
coverage: none
|
||||||
|
extensions: sockets
|
||||||
|
|
||||||
|
- name: Get Composer cache directory
|
||||||
|
id: composer-cache
|
||||||
|
shell: bash
|
||||||
|
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: static-php-8.3-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
static-php-8.3-${{ matrix.dependency-version }}-composer-
|
||||||
|
static-php-8.3-composer-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: composer update --prefer-stable --no-interaction --no-progress --ansi
|
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
|
||||||
|
|
||||||
# - name: Type Check
|
- name: Profanity Check
|
||||||
# run: composer test:type:check
|
run: composer test:profanity
|
||||||
|
|
||||||
|
- name: Type Check
|
||||||
|
run: composer test:type:check
|
||||||
|
|
||||||
- name: Type Coverage
|
- name: Type Coverage
|
||||||
run: composer test:type:coverage
|
run: composer test:type:coverage
|
||||||
|
|||||||
36
.github/workflows/tests.yml
vendored
36
.github/workflows/tests.yml
vendored
@ -2,8 +2,13 @@ name: Tests
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches: [4.x]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
|
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
|
||||||
@ -12,16 +17,19 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest] # windows-latest
|
||||||
symfony: ['7.0']
|
symfony: ['7.4', '8.0']
|
||||||
php: ['8.2', '8.3']
|
php: ['8.3', '8.4', '8.5']
|
||||||
dependency_version: [prefer-lowest, prefer-stable]
|
dependency_version: [prefer-stable]
|
||||||
|
exclude:
|
||||||
|
- php: '8.3'
|
||||||
|
symfony: '8.0'
|
||||||
|
|
||||||
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
|
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
@ -29,6 +37,21 @@ jobs:
|
|||||||
php-version: ${{ matrix.php }}
|
php-version: ${{ matrix.php }}
|
||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
coverage: none
|
coverage: none
|
||||||
|
extensions: sockets
|
||||||
|
|
||||||
|
- name: Get Composer cache directory
|
||||||
|
id: composer-cache
|
||||||
|
shell: bash
|
||||||
|
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-
|
||||||
|
${{ matrix.os }}-php-${{ matrix.php }}-composer-
|
||||||
|
|
||||||
- name: Setup Problem Matches
|
- name: Setup Problem Matches
|
||||||
run: |
|
run: |
|
||||||
@ -36,7 +59,8 @@ jobs:
|
|||||||
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||||
|
|
||||||
- name: Install PHP dependencies
|
- name: Install PHP dependencies
|
||||||
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:~${{ matrix.symfony }}"
|
shell: bash
|
||||||
|
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
|
||||||
|
|
||||||
- name: Unit Tests
|
- name: Unit Tests
|
||||||
run: composer test:unit
|
run: composer test:unit
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,3 +12,5 @@ coverage.xml
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.STREAM.md
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@ composer test
|
|||||||
|
|
||||||
Check types:
|
Check types:
|
||||||
```bash
|
```bash
|
||||||
composer test:types
|
composer test:type:check
|
||||||
```
|
```
|
||||||
|
|
||||||
Unit tests:
|
Unit tests:
|
||||||
|
|||||||
14
Makefile
14
Makefile
@ -1,14 +0,0 @@
|
|||||||
# Well documented Makefiles
|
|
||||||
DEFAULT_GOAL := help
|
|
||||||
help:
|
|
||||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
|
||||||
|
|
||||||
build: ## Build all docker images. Specify the command e.g. via make build ARGS="--build-arg PHP=8.2"
|
|
||||||
docker compose build $(ARGS)
|
|
||||||
|
|
||||||
##@ [Application]
|
|
||||||
install: ## Install the composer dependencies
|
|
||||||
docker compose run --rm composer install
|
|
||||||
|
|
||||||
test: ## Run the tests
|
|
||||||
docker compose run --rm composer test
|
|
||||||
42
README.md
42
README.md
@ -1,43 +1,53 @@
|
|||||||
<p align="center">
|
<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">
|
<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=2.x&label=Tests%202.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="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="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>
|
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
|
||||||
|
<a href="https://whyphp.dev"><img src="https://img.shields.io/badge/Why_PHP-in_2026-7A86E8?style=flat-square&labelColor=18181b" alt="Why PHP in 2026"></a>
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
> 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.
|
**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://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)**
|
||||||
|
- 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
|
||||||
|
|
||||||
- **[LaraJobs](https://larajobs.com)**
|
- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
|
||||||
- **[Brokerchooser](https://brokerchooser.com)**
|
- **[Mailtrap](https://l.rw.rw/pestphp)**
|
||||||
- **[Forge](https://forge.laravel.com)**
|
- **[SerpApi](https://serpapi.com/?ref=nunomaduro)**
|
||||||
- **[Spatie](https://spatie.be)**
|
- **[Tighten](https://tighten.com/?ref=nunomaduro)**
|
||||||
- **[Worksome](https://www.worksome.com/)**
|
- **[Redberry](https://redberry.international/laravel-development/?utm_source=pest&utm_medium=banner&utm_campaign=pest_sponsorship)**
|
||||||
|
|
||||||
|
### Gold Sponsors
|
||||||
|
|
||||||
|
- **[CMS Max](https://cmsmax.com/?ref=pestphp)**
|
||||||
|
|
||||||
### Premium Sponsors
|
### Premium Sponsors
|
||||||
|
|
||||||
|
- [Zapiet](https://zapiet.com/?ref=pestphp)
|
||||||
|
- [Load Forge](https://loadforge.com/?ref=pestphp)
|
||||||
|
- [Route4Me](https://route4me.com/pt?ref=pestphp)
|
||||||
|
- [Nerdify](https://getnerdify.com/?ref=pestphp)
|
||||||
- [Akaunting](https://akaunting.com/?ref=pestphp)
|
- [Akaunting](https://akaunting.com/?ref=pestphp)
|
||||||
- [Codecourse](https://codecourse.com/?ref=pestphp)
|
- [TestMu AI](https://www.testmuai.com/?utm_medium=sponsor&utm_source=pest)
|
||||||
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
|
|
||||||
- [Laracasts](https://laracasts.com/?ref=pestphp)
|
|
||||||
- [Localazy](https://localazy.com/?ref=pestphp)
|
|
||||||
- [Route4Me](https://www.route4me.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)**.
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
When releasing a new version of Pest there are some checks and updates that need to be done:
|
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`
|
- 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}...3.x](https://github.com/pestphp/pest/compare/{latest_version}...3.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)
|
- Update the version number in [src/Pest.php](src/Pest.php)
|
||||||
- Run the tests locally using: `composer test`
|
- Run the tests locally using: `composer test`
|
||||||
- Commit the Pest file with the message: `git commit -m "release: vX.X.X"`
|
- Commit the Pest file with the message: `git commit -m "release: vX.X.X"`
|
||||||
|
|||||||
23
bin/pest
23
bin/pest
@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php declare(strict_types=1);
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Pest\Kernel;
|
use Pest\Kernel;
|
||||||
use Pest\Panic;
|
use Pest\Panic;
|
||||||
@ -37,7 +39,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
|
|
||||||
if (str_contains($value, '--test-directory=')) {
|
if (str_contains($value, '--test-directory=')) {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
} else if ($value === '--test-directory') {
|
} elseif ($value === '--test-directory') {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
|
|
||||||
if (isset($arguments[$key + 1])) {
|
if (isset($arguments[$key + 1])) {
|
||||||
@ -62,7 +64,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
|
|
||||||
if (str_contains($value, '--assignee=')) {
|
if (str_contains($value, '--assignee=')) {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
} else if ($value === '--assignee') {
|
} elseif ($value === '--assignee') {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
|
|
||||||
if (isset($arguments[$key + 1])) {
|
if (isset($arguments[$key + 1])) {
|
||||||
@ -72,7 +74,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
|
|
||||||
if (str_contains($value, '--issue=')) {
|
if (str_contains($value, '--issue=')) {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
} else if ($value === '--issue') {
|
} elseif ($value === '--issue') {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
|
|
||||||
if (isset($arguments[$key + 1])) {
|
if (isset($arguments[$key + 1])) {
|
||||||
@ -82,7 +84,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
|
|
||||||
if (str_contains($value, '--ticket=')) {
|
if (str_contains($value, '--ticket=')) {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
} else if ($value === '--ticket') {
|
} elseif ($value === '--ticket') {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
|
|
||||||
if (isset($arguments[$key + 1])) {
|
if (isset($arguments[$key + 1])) {
|
||||||
@ -92,7 +94,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
|
|
||||||
if (str_contains($value, '--pr=')) {
|
if (str_contains($value, '--pr=')) {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
} else if ($value === '--pr') {
|
} elseif ($value === '--pr') {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
|
|
||||||
if (isset($arguments[$key + 1])) {
|
if (isset($arguments[$key + 1])) {
|
||||||
@ -102,7 +104,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
|
|
||||||
if (str_contains($value, '--pull-request=')) {
|
if (str_contains($value, '--pull-request=')) {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
} else if ($value === '--pull-request') {
|
} elseif ($value === '--pull-request') {
|
||||||
unset($arguments[$key]);
|
unset($arguments[$key]);
|
||||||
|
|
||||||
if (isset($arguments[$key + 1])) {
|
if (isset($arguments[$key + 1])) {
|
||||||
@ -117,7 +119,6 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Used when Pest is required using composer.
|
// Used when Pest is required using composer.
|
||||||
$vendorPath = dirname(__DIR__, 4).'/vendor/autoload.php';
|
$vendorPath = dirname(__DIR__, 4).'/vendor/autoload.php';
|
||||||
|
|
||||||
@ -134,7 +135,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
|
|
||||||
// Get $rootPath based on $autoloadPath
|
// Get $rootPath based on $autoloadPath
|
||||||
$rootPath = dirname($autoloadPath, 2);
|
$rootPath = dirname($autoloadPath, 2);
|
||||||
$input = new ArgvInput();
|
$input = new ArgvInput;
|
||||||
|
|
||||||
$testSuite = TestSuite::getInstance(
|
$testSuite = TestSuite::getInstance(
|
||||||
$rootPath,
|
$rootPath,
|
||||||
@ -146,11 +147,11 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($todo) {
|
if ($todo) {
|
||||||
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter());
|
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($notes) {
|
if ($notes) {
|
||||||
$testSuite->tests->addTestCaseMethodFilter(new NotesTestCaseFilter());
|
$testSuite->tests->addTestCaseMethodFilter(new NotesTestCaseFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($assignee = $input->getParameterOption('--assignee')) {
|
if ($assignee = $input->getParameterOption('--assignee')) {
|
||||||
|
|||||||
@ -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,11 +81,12 @@ $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']),
|
||||||
$getopt['testdox-columns'] ?? null,
|
(int) ($getopt['testdox-columns'] ?? null),
|
||||||
);
|
);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@ -17,18 +17,21 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2.0",
|
"php": "^8.3.0",
|
||||||
"brianium/paratest": "^7.5.4",
|
"brianium/paratest": "^7.19.0",
|
||||||
"nunomaduro/collision": "^8.4.0",
|
"nunomaduro/collision": "^8.9.0",
|
||||||
"nunomaduro/termwind": "^2.1.0",
|
"nunomaduro/termwind": "^2.4.0",
|
||||||
"pestphp/pest-plugin": "^3.0.0",
|
"pestphp/pest-plugin": "^4.0.0",
|
||||||
"pestphp/pest-plugin-arch": "^3.0.0",
|
"pestphp/pest-plugin-arch": "^4.0.0",
|
||||||
"pestphp/pest-plugin-mutate": "^3.0.3",
|
"pestphp/pest-plugin-mutate": "^4.0.1",
|
||||||
"phpunit/phpunit": "^11.3.6"
|
"pestphp/pest-plugin-profanity": "^4.2.1",
|
||||||
|
"phpunit/phpunit": "^12.5.12",
|
||||||
|
"symfony/process": "^7.4.5|^8.0.5"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"phpunit/phpunit": ">11.3.6",
|
"filp/whoops": "<2.18.3",
|
||||||
"sebastian/exporter": "<6.0.0",
|
"phpunit/phpunit": ">12.5.12",
|
||||||
|
"sebastian/exporter": "<7.0.0",
|
||||||
"webmozart/assert": "<1.11.0"
|
"webmozart/assert": "<1.11.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@ -52,9 +55,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"pestphp/pest-dev-tools": "^3.0.0",
|
"pestphp/pest-dev-tools": "^4.1.0",
|
||||||
"pestphp/pest-plugin-type-coverage": "^3.0.0",
|
"pestphp/pest-plugin-browser": "^4.3.0",
|
||||||
"symfony/process": "^7.1.3"
|
"pestphp/pest-plugin-type-coverage": "^4.0.3",
|
||||||
|
"psy/psysh": "^0.12.20"
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
@ -70,16 +74,17 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"refacto": "rector",
|
"refacto": "rector",
|
||||||
"lint": "pint",
|
"lint": "pint --parallel",
|
||||||
"test:refacto": "rector --dry-run",
|
"test:refacto": "rector --dry-run",
|
||||||
"test:lint": "pint --test",
|
"test:lint": "pint --parallel --test",
|
||||||
|
"test:profanity": "php bin/pest --profanity --compact",
|
||||||
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
|
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
|
||||||
"test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100",
|
"test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100",
|
||||||
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
|
"test:unit": "php bin/pest --exclude-group=integration --compact",
|
||||||
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
|
"test:inline": "php bin/pest --configuration=phpunit.inline.xml",
|
||||||
"test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3",
|
"test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3",
|
||||||
"test:integration": "php bin/pest --colors=always --group=integration -v",
|
"test:integration": "php bin/pest --group=integration -v",
|
||||||
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
|
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots",
|
||||||
"test": [
|
"test": [
|
||||||
"@test:refacto",
|
"@test:refacto",
|
||||||
"@test:lint",
|
"@test:lint",
|
||||||
@ -110,6 +115,7 @@
|
|||||||
"Pest\\Plugins\\Snapshot",
|
"Pest\\Plugins\\Snapshot",
|
||||||
"Pest\\Plugins\\Verbose",
|
"Pest\\Plugins\\Verbose",
|
||||||
"Pest\\Plugins\\Version",
|
"Pest\\Plugins\\Version",
|
||||||
|
"Pest\\Plugins\\Shard",
|
||||||
"Pest\\Plugins\\Parallel"
|
"Pest\\Plugins\\Parallel"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
php:
|
|
||||||
build:
|
|
||||||
context: ./docker
|
|
||||||
volumes:
|
|
||||||
- .:/var/www/html
|
|
||||||
composer:
|
|
||||||
build:
|
|
||||||
context: ./docker
|
|
||||||
volumes:
|
|
||||||
- .:/var/www/html
|
|
||||||
entrypoint: ["composer"]
|
|
||||||
@ -52,6 +52,8 @@ use PHPUnit\Util\Filter;
|
|||||||
use PHPUnit\Util\ThrowableToStringMapper;
|
use PHPUnit\Util\ThrowableToStringMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*
|
||||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
*/
|
*/
|
||||||
final readonly class ThrowableBuilder
|
final readonly class ThrowableBuilder
|
||||||
@ -68,7 +70,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();
|
||||||
@ -82,7 +84,7 @@ final readonly class ThrowableBuilder
|
|||||||
$t->getMessage(),
|
$t->getMessage(),
|
||||||
ThrowableToStringMapper::map($t),
|
ThrowableToStringMapper::map($t),
|
||||||
$trace,
|
$trace,
|
||||||
$previous
|
$previous,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -99,7 +99,9 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($test instanceof HasPrintableTestCaseName) {
|
if ($test instanceof HasPrintableTestCaseName) {
|
||||||
$name = $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName();
|
$name = trim(
|
||||||
|
$test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName().$test->dataSetAsString()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
$name = $test::class.'::'.$test->nameWithDataSet();
|
$name = $test::class.'::'.$test->nameWithDataSet();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,9 +46,10 @@ declare(strict_types=1);
|
|||||||
namespace PHPUnit\Runner\ResultCache;
|
namespace PHPUnit\Runner\ResultCache;
|
||||||
|
|
||||||
use const DIRECTORY_SEPARATOR;
|
use const DIRECTORY_SEPARATOR;
|
||||||
|
use const LOCK_EX;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestStatus\TestStatus;
|
use PHPUnit\Framework\TestStatus\TestStatus;
|
||||||
use PHPUnit\Runner\DirectoryCannotBeCreatedException;
|
use PHPUnit\Runner\DirectoryDoesNotExistException;
|
||||||
use PHPUnit\Runner\Exception;
|
use PHPUnit\Runner\Exception;
|
||||||
use PHPUnit\Util\Filesystem;
|
use PHPUnit\Util\Filesystem;
|
||||||
|
|
||||||
@ -65,24 +66,23 @@ use function json_encode;
|
|||||||
use function Pest\version;
|
use function Pest\version;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*
|
||||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
*/
|
*/
|
||||||
final class DefaultResultCache implements ResultCache
|
final class DefaultResultCache implements ResultCache
|
||||||
{
|
{
|
||||||
/**
|
private const string DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private const DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
|
|
||||||
|
|
||||||
private readonly string $cacheFilename;
|
private readonly string $cacheFilename;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @psalm-var array<string, TestStatus>
|
* @var array<string, TestStatus>
|
||||||
*/
|
*/
|
||||||
private array $defects = [];
|
private array $defects = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @psalm-var array<string, float>
|
* @var array<string, float>
|
||||||
*/
|
*/
|
||||||
private array $times = [];
|
private array $times = [];
|
||||||
|
|
||||||
@ -95,28 +95,39 @@ final class DefaultResultCache implements ResultCache
|
|||||||
$this->cacheFilename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_RESULT_CACHE_FILENAME;
|
$this->cacheFilename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_RESULT_CACHE_FILENAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStatus(string $id, TestStatus $status): void
|
public function setStatus(ResultCacheId $id, TestStatus $status): void
|
||||||
{
|
{
|
||||||
if ($status->isSuccess()) {
|
if ($status->isSuccess()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->defects[$id] = $status;
|
$this->defects[$id->asString()] = $status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function status(string $id): TestStatus
|
public function status(ResultCacheId $id): TestStatus
|
||||||
{
|
{
|
||||||
return $this->defects[$id] ?? TestStatus::unknown();
|
return $this->defects[$id->asString()] ?? TestStatus::unknown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setTime(string $id, float $time): void
|
public function setTime(ResultCacheId $id, float $time): void
|
||||||
{
|
{
|
||||||
$this->times[$id] = $time;
|
$this->times[$id->asString()] = $time;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function time(string $id): float
|
public function time(ResultCacheId $id): float
|
||||||
{
|
{
|
||||||
return $this->times[$id] ?? 0.0;
|
return $this->times[$id->asString()] ?? 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mergeWith(self $other): void
|
||||||
|
{
|
||||||
|
foreach ($other->defects as $id => $defect) {
|
||||||
|
$this->defects[$id] = $defect;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($other->times as $id => $time) {
|
||||||
|
$this->times[$id] = $time;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function load(): void
|
public function load(): void
|
||||||
@ -165,7 +176,7 @@ final class DefaultResultCache implements ResultCache
|
|||||||
public function persist(): void
|
public function persist(): void
|
||||||
{
|
{
|
||||||
if (! Filesystem::createDirectory(dirname($this->cacheFilename))) {
|
if (! Filesystem::createDirectory(dirname($this->cacheFilename))) {
|
||||||
throw new DirectoryCannotBeCreatedException($this->cacheFilename);
|
throw new DirectoryDoesNotExistException(dirname($this->cacheFilename));
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
|
|||||||
@ -45,6 +45,7 @@ declare(strict_types=1);
|
|||||||
namespace PHPUnit\TextUI;
|
namespace PHPUnit\TextUI;
|
||||||
|
|
||||||
use Pest\Plugins\Only;
|
use Pest\Plugins\Only;
|
||||||
|
use Pest\Runner\Filter\EnsureTestCaseIsInitiatedFilter;
|
||||||
use PHPUnit\Event;
|
use PHPUnit\Event;
|
||||||
use PHPUnit\Framework\TestSuite;
|
use PHPUnit\Framework\TestSuite;
|
||||||
use PHPUnit\Runner\Filter\Factory;
|
use PHPUnit\Runner\Filter\Factory;
|
||||||
@ -66,6 +67,12 @@ final readonly class TestSuiteFilterProcessor
|
|||||||
{
|
{
|
||||||
$factory = new Factory;
|
$factory = new Factory;
|
||||||
|
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
(fn () => $this->filters[] = [
|
||||||
|
'className' => EnsureTestCaseIsInitiatedFilter::class,
|
||||||
|
'argument' => '',
|
||||||
|
])->call($factory);
|
||||||
|
|
||||||
if (! $configuration->hasFilter() &&
|
if (! $configuration->hasFilter() &&
|
||||||
! $configuration->hasGroups() &&
|
! $configuration->hasGroups() &&
|
||||||
! $configuration->hasExcludeGroups() &&
|
! $configuration->hasExcludeGroups() &&
|
||||||
@ -73,6 +80,8 @@ final readonly class TestSuiteFilterProcessor
|
|||||||
! $configuration->hasTestsCovering() &&
|
! $configuration->hasTestsCovering() &&
|
||||||
! $configuration->hasTestsUsing() &&
|
! $configuration->hasTestsUsing() &&
|
||||||
! Only::isEnabled()) {
|
! Only::isEnabled()) {
|
||||||
|
$suite->injectFilter($factory);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
199
phpstan-baseline.neon
Normal file
199
phpstan-baseline.neon
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
parameters:
|
||||||
|
ignoreErrors:
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#1 of callable callable\(Pest\\Expectation\<string\|null\>\)\: Pest\\Arch\\Contracts\\ArchExpectation expects Pest\\Expectation\<string\|null\>, Pest\\Expectation\<string\|null\> given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/ArchPresets/AbstractPreset.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Trait Pest\\Concerns\\Expectable is used zero times and is not analysed\.$#'
|
||||||
|
identifier: trait.unused
|
||||||
|
count: 1
|
||||||
|
path: src/Concerns/Expectable.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Trait Pest\\Concerns\\Logging\\WritesToConsole is used zero times and is not analysed\.$#'
|
||||||
|
identifier: trait.unused
|
||||||
|
count: 1
|
||||||
|
path: src/Concerns/Logging/WritesToConsole.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Trait Pest\\Concerns\\Testable is used zero times and is not analysed\.$#'
|
||||||
|
identifier: trait.unused
|
||||||
|
count: 1
|
||||||
|
path: src/Concerns/Testable.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Loose comparison using \!\= between \(Closure\|null\) and false will always evaluate to false\.$#'
|
||||||
|
identifier: notEqual.alwaysFalse
|
||||||
|
count: 1
|
||||||
|
path: src/Expectation.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Method Pest\\Expectation\:\:and\(\) should return Pest\\Expectation\<TAndValue\> but returns \(Pest\\Expectation&TAndValue\)\|Pest\\Expectation\<TAndValue of mixed\>\.$#'
|
||||||
|
identifier: return.type
|
||||||
|
count: 1
|
||||||
|
path: src/Expectation.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$each contains generic class Pest\\Expectations\\EachExpectation but does not specify its types\: TValue$#'
|
||||||
|
identifier: missingType.generics
|
||||||
|
count: 1
|
||||||
|
path: src/Expectation.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$not contains generic class Pest\\Expectations\\OppositeExpectation but does not specify its types\: TValue$#'
|
||||||
|
identifier: missingType.generics
|
||||||
|
count: 1
|
||||||
|
path: src/Expectation.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#2 \$newScope of method Closure\:\:bindTo\(\) expects ''static''\|class\-string\|object\|null, string given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Expectation.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Function expect\(\) should return Pest\\Expectation\<TValue\|null\> but returns Pest\\Expectation\<TValue\|null\>\.$#'
|
||||||
|
identifier: return.type
|
||||||
|
count: 1
|
||||||
|
path: src/Functions.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#1 \$argv of method PHPUnit\\TextUI\\Application\:\:run\(\) expects list\<string\>, array\<int, string\> given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Kernel.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Call to an undefined method object&TValue of mixed\:\:__toString\(\)\.$#'
|
||||||
|
identifier: method.notFound
|
||||||
|
count: 1
|
||||||
|
path: src/Mixins/Expectation.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Call to an undefined method object&TValue of mixed\:\:toArray\(\)\.$#'
|
||||||
|
identifier: method.notFound
|
||||||
|
count: 4
|
||||||
|
path: src/Mixins/Expectation.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Call to an undefined method object&TValue of mixed\:\:toSnapshot\(\)\.$#'
|
||||||
|
identifier: method.notFound
|
||||||
|
count: 1
|
||||||
|
path: src/Mixins/Expectation.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Call to an undefined method object&TValue of mixed\:\:toString\(\)\.$#'
|
||||||
|
identifier: method.notFound
|
||||||
|
count: 1
|
||||||
|
path: src/Mixins/Expectation.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#'
|
||||||
|
identifier: staticMethod.alreadyNarrowedType
|
||||||
|
count: 2
|
||||||
|
path: src/Mixins/Expectation.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^PHPDoc tag @var with type callable\(\)\: bool is not subtype of native type Closure\|null\.$#'
|
||||||
|
identifier: varTag.nativeType
|
||||||
|
count: 1
|
||||||
|
path: src/PendingCalls/TestCall.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#1 \$argv of class Symfony\\Component\\Console\\Input\\ArgvInput constructor expects list\<string\>\|null, array\<int, string\> given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#13 \$testRunnerTriggeredDeprecationEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestRunner\\DeprecationTriggered\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#14 \$testRunnerTriggeredWarningEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestRunner\\WarningTriggered\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#15 \$errors of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#16 \$deprecations of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#17 \$notices of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#18 \$warnings of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#19 \$phpDeprecations of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#20 \$phpNotices of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#21 \$phpWarnings of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#4 \$testErroredEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\AfterLastTestMethodErrored\|PHPUnit\\Event\\Test\\BeforeFirstTestMethodErrored\|PHPUnit\\Event\\Test\\Errored\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#5 \$testFailedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\Failed\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#7 \$testSuiteSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestSuite\\Skipped\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#8 \$testSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\Skipped\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Parameter \#9 \$testMarkedIncompleteEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\MarkedIncomplete\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Property Pest\\Plugins\\Parallel\\Paratest\\WrapperRunner\:\:\$pending \(list\<non\-empty\-string\>\) does not accept array\<int, non\-empty\-string\>\.$#'
|
||||||
|
identifier: assign.propertyType
|
||||||
|
count: 1
|
||||||
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
17
phpstan.neon
17
phpstan.neon
@ -1,23 +1,12 @@
|
|||||||
includes:
|
includes:
|
||||||
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
- phpstan-baseline.neon
|
||||||
- vendor/ergebnis/phpstan-rules/rules.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:
|
||||||
- "#has a nullable return type declaration.#"
|
|
||||||
- "#Language construct isset\\(\\) should not be used.#"
|
|
||||||
- "#is not allowed to extend#"
|
|
||||||
- "#is concrete, but does not have a Test suffix#"
|
|
||||||
- "#with a nullable type declaration#"
|
|
||||||
- "#type mixed is not subtype of native#"
|
- "#type mixed is not subtype of native#"
|
||||||
- "# with null as default value#"
|
|
||||||
- "#has parameter \\$closure with default value.#"
|
|
||||||
- "#has parameter \\$description with default value.#"
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="default">
|
<testsuite name="default">
|
||||||
<directory suffix=".php">./tests</directory>
|
<directory suffix=".php">./tests</directory>
|
||||||
|
<directory suffix=".php">./tests-external</directory>
|
||||||
<exclude>./tests/.snapshots</exclude>
|
<exclude>./tests/.snapshots</exclude>
|
||||||
<exclude>./tests/.tests</exclude>
|
<exclude>./tests/.tests</exclude>
|
||||||
<exclude>./tests/Fixtures/Inheritance</exclude>
|
<exclude>./tests/Fixtures/Inheritance</exclude>
|
||||||
|
|||||||
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector;
|
||||||
use Rector\Config\RectorConfig;
|
use Rector\Config\RectorConfig;
|
||||||
|
use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector;
|
||||||
|
use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector;
|
||||||
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
|
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
|
||||||
|
|
||||||
return RectorConfig::configure()
|
return RectorConfig::configure()
|
||||||
@ -12,6 +15,9 @@ return RectorConfig::configure()
|
|||||||
->withSkip([
|
->withSkip([
|
||||||
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
|
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
|
||||||
ReturnNeverTypeRector::class,
|
ReturnNeverTypeRector::class,
|
||||||
|
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
|
||||||
|
NarrowObjectReturnTypeRector::class,
|
||||||
|
RemoveParentDelegatingConstructorRector::class,
|
||||||
])
|
])
|
||||||
->withPreparedSets(
|
->withPreparedSets(
|
||||||
deadCode: true,
|
deadCode: true,
|
||||||
|
|||||||
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>
|
||||||
@ -27,17 +27,21 @@ final class Laravel extends AbstractPreset
|
|||||||
->ignoring('App\Enums');
|
->ignoring('App\Enums');
|
||||||
|
|
||||||
$this->expectations[] = expect('App\Enums')
|
$this->expectations[] = expect('App\Enums')
|
||||||
->toBeEnums();
|
->toBeEnums()
|
||||||
|
->ignoring('App\Enums\Concerns');
|
||||||
|
|
||||||
$this->expectations[] = expect('App\Features')
|
$this->expectations[] = expect('App\Features')
|
||||||
->toBeClasses();
|
->toBeClasses()
|
||||||
|
->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()
|
||||||
->toImplement('Throwable');
|
->toImplement('Throwable')
|
||||||
|
->ignoring('App\Exceptions\Handler');
|
||||||
|
|
||||||
$this->expectations[] = expect('App')
|
$this->expectations[] = expect('App')
|
||||||
->not->toImplement(Throwable::class)
|
->not->toImplement(Throwable::class)
|
||||||
@ -146,10 +150,10 @@ final class Laravel extends AbstractPreset
|
|||||||
->toHaveSuffix('Controller');
|
->toHaveSuffix('Controller');
|
||||||
|
|
||||||
$this->expectations[] = expect('App\Http')
|
$this->expectations[] = expect('App\Http')
|
||||||
->toOnlyBeUsedIn('App\Http');
|
->toOnlyBeUsedIn(['App\Http', 'App\Providers']);
|
||||||
|
|
||||||
$this->expectations[] = expect('App\Http\Controllers')
|
$this->expectations[] = expect('App\Http\Controllers')
|
||||||
->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy']);
|
->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy', 'middleware']);
|
||||||
|
|
||||||
$this->expectations[] = expect([
|
$this->expectations[] = expect([
|
||||||
'dd',
|
'dd',
|
||||||
@ -159,5 +163,15 @@ final class Laravel extends AbstractPreset
|
|||||||
'exit',
|
'exit',
|
||||||
'ray',
|
'ray',
|
||||||
])->not->toBeUsed();
|
])->not->toBeUsed();
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Policies')
|
||||||
|
->classes()
|
||||||
|
->toHaveSuffix('Policy');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Attributes')
|
||||||
|
->classes()
|
||||||
|
->toImplement('Illuminate\Contracts\Container\ContextualAttribute')
|
||||||
|
->toHaveAttribute('Attribute')
|
||||||
|
->toHaveMethod('resolve');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,6 @@ final class Security extends AbstractPreset
|
|||||||
'create_function',
|
'create_function',
|
||||||
'unserialize',
|
'unserialize',
|
||||||
'extract',
|
'extract',
|
||||||
'parse_str',
|
|
||||||
'mb_parse_str',
|
'mb_parse_str',
|
||||||
'dl',
|
'dl',
|
||||||
'assert',
|
'assert',
|
||||||
|
|||||||
@ -21,6 +21,7 @@ final class Strict extends AbstractPreset
|
|||||||
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHaveProtectedMethods(),
|
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHaveProtectedMethods(),
|
||||||
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeAbstract(),
|
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeAbstract(),
|
||||||
fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictTypes(),
|
fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictTypes(),
|
||||||
|
fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictEquality(),
|
||||||
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->toBeFinal(),
|
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->toBeFinal(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ final class BootExcludeList implements Bootstrapper
|
|||||||
*
|
*
|
||||||
* @var array<int, non-empty-string>
|
* @var array<int, non-empty-string>
|
||||||
*/
|
*/
|
||||||
private const EXCLUDE_LIST = [
|
private const array EXCLUDE_LIST = [
|
||||||
'bin',
|
'bin',
|
||||||
'overrides',
|
'overrides',
|
||||||
'resources',
|
'resources',
|
||||||
|
|||||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Pest\Bootstrappers;
|
namespace Pest\Bootstrappers;
|
||||||
|
|
||||||
use Pest\Contracts\Bootstrapper;
|
use Pest\Contracts\Bootstrapper;
|
||||||
|
use Pest\Exceptions\FatalException;
|
||||||
use Pest\Support\DatasetInfo;
|
use Pest\Support\DatasetInfo;
|
||||||
use Pest\Support\Str;
|
use Pest\Support\Str;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
@ -24,7 +25,7 @@ final class BootFiles implements Bootstrapper
|
|||||||
*
|
*
|
||||||
* @var array<int, string>
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
private const STRUCTURE = [
|
private const array STRUCTURE = [
|
||||||
'Expectations',
|
'Expectations',
|
||||||
'Expectations.php',
|
'Expectations.php',
|
||||||
'Helpers',
|
'Helpers',
|
||||||
@ -40,6 +41,10 @@ final class BootFiles implements Bootstrapper
|
|||||||
$rootPath = TestSuite::getInstance()->rootPath;
|
$rootPath = TestSuite::getInstance()->rootPath;
|
||||||
$testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory();
|
$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) {
|
foreach (self::STRUCTURE as $filename) {
|
||||||
$filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename);
|
$filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename);
|
||||||
|
|
||||||
@ -78,7 +83,7 @@ final class BootFiles implements Bootstrapper
|
|||||||
|
|
||||||
private function bootDatasets(string $testsPath): void
|
private function bootDatasets(string $testsPath): void
|
||||||
{
|
{
|
||||||
assert(strlen($testsPath) > 0);
|
assert($testsPath !== '');
|
||||||
|
|
||||||
$files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');
|
$files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');
|
||||||
|
|
||||||
|
|||||||
@ -15,17 +15,17 @@ final class BootOverrides implements Bootstrapper
|
|||||||
/**
|
/**
|
||||||
* The list of files to be overridden.
|
* The list of files to be overridden.
|
||||||
*
|
*
|
||||||
* @var array<string, string>
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
public const FILES = [
|
public const array FILES = [
|
||||||
'c96b1cb57d7fc8e649f4c13a8abe418c2541bcfab194fb6702b99f777f52ee84' => 'Runner/Filter/NameFilterIterator.php',
|
'Runner/Filter/NameFilterIterator.php',
|
||||||
'a4a43de01f641c6944ee83d963795a46d32b5206b5ab3bbc6cce76e67190acbf' => 'Runner/ResultCache/DefaultResultCache.php',
|
'Runner/ResultCache/DefaultResultCache.php',
|
||||||
'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php',
|
'Runner/TestSuiteLoader.php',
|
||||||
'3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
|
'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
|
||||||
'8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
|
'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
|
||||||
'43883b7e5811886cf3731c8ed6304d5a77078d9731e1e505abc2da36bde19f3e' => 'TextUI/TestSuiteFilterProcessor.php',
|
'TextUI/TestSuiteFilterProcessor.php',
|
||||||
'357d5cd7007f8559b26e1b8cdf43bb6fb15b51b79db981779da6f31b7ec39dad' => 'Event/Value/ThrowableBuilder.php',
|
'Event/Value/ThrowableBuilder.php',
|
||||||
'676273f1fe483877cf2d95c5aedbf9ae5d6a8e2f4c12d6ce716df6591e6db023' => 'Logging/JUnit/JunitXmlLogger.php',
|
'Logging/JUnit/JunitXmlLogger.php',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -20,7 +20,7 @@ final readonly class BootSubscribers implements Bootstrapper
|
|||||||
*
|
*
|
||||||
* @var array<int, class-string<Subscriber>>
|
* @var array<int, class-string<Subscriber>>
|
||||||
*/
|
*/
|
||||||
private const SUBSCRIBERS = [
|
private const array SUBSCRIBERS = [
|
||||||
Subscribers\EnsureConfigurationIsAvailable::class,
|
Subscribers\EnsureConfigurationIsAvailable::class,
|
||||||
Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
|
Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
|
||||||
Subscribers\EnsureKernelDumpIsFlushed::class,
|
Subscribers\EnsureKernelDumpIsFlushed::class,
|
||||||
|
|||||||
@ -8,6 +8,8 @@ use Closure;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
*
|
||||||
|
* @template T of object
|
||||||
*/
|
*/
|
||||||
trait Extendable
|
trait Extendable
|
||||||
{
|
{
|
||||||
@ -20,6 +22,8 @@ trait Extendable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new extend.
|
* Register a new extend.
|
||||||
|
*
|
||||||
|
* @param-closure-this T $extend
|
||||||
*/
|
*/
|
||||||
public function extend(string $name, Closure $extend): void
|
public function extend(string $name, Closure $extend): void
|
||||||
{
|
{
|
||||||
|
|||||||
@ -6,10 +6,12 @@ namespace Pest\Concerns;
|
|||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Pest\Exceptions\DatasetArgumentsMismatch;
|
use Pest\Exceptions\DatasetArgumentsMismatch;
|
||||||
|
use Pest\Panic;
|
||||||
use Pest\Preset;
|
use Pest\Preset;
|
||||||
use Pest\Support\ChainableClosure;
|
use Pest\Support\ChainableClosure;
|
||||||
use Pest\Support\ExceptionTrace;
|
use Pest\Support\ExceptionTrace;
|
||||||
use Pest\Support\Reflection;
|
use Pest\Support\Reflection;
|
||||||
|
use Pest\Support\Shell;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use PHPUnit\Framework\Attributes\PostCondition;
|
use PHPUnit\Framework\Attributes\PostCondition;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@ -61,8 +63,10 @@ trait Testable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's describing, if any.
|
* The test's describing, if any.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
public ?string $__describing = null;
|
public array $__describing = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the test has ran or not.
|
* Whether the test has ran or not.
|
||||||
@ -99,27 +103,6 @@ trait Testable
|
|||||||
*/
|
*/
|
||||||
private array $__snapshotChanges = [];
|
private array $__snapshotChanges = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new Test Case instance.
|
|
||||||
*/
|
|
||||||
public function __construct(string $name)
|
|
||||||
{
|
|
||||||
parent::__construct($name);
|
|
||||||
|
|
||||||
$test = TestSuite::getInstance()->tests->get(self::$__filename);
|
|
||||||
|
|
||||||
if ($test->hasMethod($name)) {
|
|
||||||
$method = $test->getMethod($name);
|
|
||||||
$this->__description = self::$__latestDescription = $method->description;
|
|
||||||
self::$__latestAssignees = $method->assignees;
|
|
||||||
self::$__latestNotes = $method->notes;
|
|
||||||
self::$__latestIssues = $method->issues;
|
|
||||||
self::$__latestPrs = $method->prs;
|
|
||||||
$this->__describing = $method->describing;
|
|
||||||
$this->__test = $method->getClosure();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the test case static properties.
|
* Resets the test case static properties.
|
||||||
*/
|
*/
|
||||||
@ -212,7 +195,11 @@ trait Testable
|
|||||||
$beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll);
|
$beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
call_user_func(Closure::bind($beforeAll, null, self::class));
|
try {
|
||||||
|
call_user_func(Closure::bind($beforeAll, null, self::class));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Panic::with($e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -240,8 +227,6 @@ trait Testable
|
|||||||
|
|
||||||
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
||||||
|
|
||||||
$method->setUp($this);
|
|
||||||
|
|
||||||
$description = $method->description;
|
$description = $method->description;
|
||||||
if ($this->dataName()) {
|
if ($this->dataName()) {
|
||||||
$description = str_contains((string) $description, ':dataset')
|
$description = str_contains((string) $description, ':dataset')
|
||||||
@ -283,6 +268,33 @@ trait Testable
|
|||||||
$this->__callClosure($beforeEach, $arguments);
|
$this->__callClosure($beforeEach, $arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize test case properties from TestSuite.
|
||||||
|
*/
|
||||||
|
public function __initializeTestCase(): void
|
||||||
|
{
|
||||||
|
// Return if the test case has already been initialized
|
||||||
|
if (isset($this->__test)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $this->name();
|
||||||
|
$test = TestSuite::getInstance()->tests->get(self::$__filename);
|
||||||
|
|
||||||
|
if ($test->hasMethod($name)) {
|
||||||
|
$method = $test->getMethod($name);
|
||||||
|
$this->__description = self::$__latestDescription = $method->description;
|
||||||
|
self::$__latestAssignees = $method->assignees;
|
||||||
|
self::$__latestNotes = $method->notes;
|
||||||
|
self::$__latestIssues = $method->issues;
|
||||||
|
self::$__latestPrs = $method->prs;
|
||||||
|
$this->__describing = $method->describing;
|
||||||
|
$this->__test = $method->getClosure();
|
||||||
|
|
||||||
|
$method->setUp($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets executed after the Test Case.
|
* Gets executed after the Test Case.
|
||||||
*/
|
*/
|
||||||
@ -432,15 +444,7 @@ trait Testable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($this->__snapshotChanges) === 1) {
|
$this->markTestIncomplete(implode('. ', $this->__snapshotChanges));
|
||||||
$this->markTestIncomplete($this->__snapshotChanges[0]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$messages = implode(PHP_EOL, array_map(static fn (string $message): string => '- $message', $this->__snapshotChanges));
|
|
||||||
|
|
||||||
$this->markTestIncomplete($messages);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -464,7 +468,7 @@ trait Testable
|
|||||||
*/
|
*/
|
||||||
public static function getLatestPrintableTestCaseMethodName(): string
|
public static function getLatestPrintableTestCaseMethodName(): string
|
||||||
{
|
{
|
||||||
return self::$__latestDescription;
|
return self::$__latestDescription ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -479,4 +483,12 @@ trait Testable
|
|||||||
'notes' => self::$__latestNotes,
|
'notes' => self::$__latestNotes,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a shell for the test case.
|
||||||
|
*/
|
||||||
|
public function shell(): void
|
||||||
|
{
|
||||||
|
Shell::open();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest;
|
namespace Pest;
|
||||||
|
|
||||||
|
use Pest\PendingCalls\BeforeEachCall;
|
||||||
use Pest\PendingCalls\UsesCall;
|
use Pest\PendingCalls\UsesCall;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,6 +63,14 @@ final readonly class Configuration
|
|||||||
return (new UsesCall($this->filename, []))->group(...$groups);
|
return (new UsesCall($this->filename, []))->group(...$groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks all tests in the current file to be run exclusively.
|
||||||
|
*/
|
||||||
|
public function only(): void
|
||||||
|
{
|
||||||
|
(new BeforeEachCall(TestSuite::getInstance(), $this->filename))->only();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Depending on where is called, it will extend the given classes and traits globally or locally.
|
* Depending on where is called, it will extend the given classes and traits globally or locally.
|
||||||
*/
|
*/
|
||||||
@ -102,6 +111,14 @@ final readonly class Configuration
|
|||||||
return Configuration\Project::getInstance();
|
return Configuration\Project::getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the browser configuration.
|
||||||
|
*/
|
||||||
|
public function browser(): Browser\Configuration
|
||||||
|
{
|
||||||
|
return new Browser\Configuration;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Proxies calls to the uses method.
|
* Proxies calls to the uses method.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -16,7 +16,7 @@ final readonly class Help
|
|||||||
*
|
*
|
||||||
* @var array<int, string>
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
private const HELP_MESSAGES = [
|
private const array HELP_MESSAGES = [
|
||||||
'<comment>Pest Options:</comment>',
|
'<comment>Pest Options:</comment>',
|
||||||
' <info>--init</info> Initialise a standard Pest configuration',
|
' <info>--init</info> Initialise a standard Pest configuration',
|
||||||
' <info>--coverage</info> Enable coverage and output to standard output',
|
' <info>--coverage</info> Enable coverage and output to standard output',
|
||||||
|
|||||||
@ -22,10 +22,14 @@ final readonly class Thanks
|
|||||||
*
|
*
|
||||||
* @var array<string, string>
|
* @var array<string, string>
|
||||||
*/
|
*/
|
||||||
private const FUNDING_MESSAGES = [
|
private const array FUNDING_MESSAGES = [
|
||||||
'Star' => 'https://github.com/pestphp/pest',
|
'Star' => 'https://github.com/pestphp/pest',
|
||||||
'News' => 'https://twitter.com/pestphp',
|
'YouTube' => 'https://youtube.com/@nunomaduro',
|
||||||
'Videos' => 'https://youtube.com/@nunomaduro',
|
'TikTok' => 'https://tiktok.com/@enunomaduro',
|
||||||
|
'Twitch' => 'https://twitch.tv/nunomaduro',
|
||||||
|
'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',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ final class DatasetMissing extends BadFunctionCallException implements Exception
|
|||||||
public function __construct(string $file, string $name, array $arguments)
|
public function __construct(string $file, string $name, array $arguments)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf(
|
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,
|
$name,
|
||||||
count($arguments),
|
count($arguments),
|
||||||
implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)),
|
implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)),
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -19,7 +19,11 @@ final class TestCaseAlreadyInUse extends InvalidArgumentException implements Exc
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $inUse, string $newOne, string $folder)
|
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`',
|
parent::__construct(sprintf(
|
||||||
$newOne, $folder, $inUse));
|
'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(
|
parent::__construct(
|
||||||
sprintf(
|
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->description,
|
||||||
$method->filename
|
$method->filename
|
||||||
)
|
)
|
||||||
|
|||||||
@ -52,7 +52,9 @@ use ReflectionProperty;
|
|||||||
*/
|
*/
|
||||||
final class Expectation
|
final class Expectation
|
||||||
{
|
{
|
||||||
|
/** @use Extendable<self<TValue>> */
|
||||||
use Extendable;
|
use Extendable;
|
||||||
|
|
||||||
use Pipeable;
|
use Pipeable;
|
||||||
use Retrievable;
|
use Retrievable;
|
||||||
|
|
||||||
@ -223,7 +225,7 @@ final class Expectation
|
|||||||
throw new BadMethodCallException('Expectation value is not iterable.');
|
throw new BadMethodCallException('Expectation value is not iterable.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($callbacks) == 0) {
|
if ($callbacks === []) {
|
||||||
throw new InvalidArgumentException('No sequence expectations defined.');
|
throw new InvalidArgumentException('No sequence expectations defined.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,7 +266,7 @@ final class Expectation
|
|||||||
$matched = false;
|
$matched = false;
|
||||||
|
|
||||||
foreach ($expressions as $key => $callback) {
|
foreach ($expressions as $key => $callback) {
|
||||||
if ($subject != $key) {
|
if ($subject != $key) { // @pest-arch-ignore-line
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,7 +332,7 @@ final class Expectation
|
|||||||
* @param array<int, mixed> $parameters
|
* @param array<int, mixed> $parameters
|
||||||
* @return Expectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue>
|
* @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 (! self::hasMethod($method)) {
|
||||||
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $method)) {
|
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $method)) {
|
||||||
@ -355,6 +357,10 @@ final class Expectation
|
|||||||
$reflectionClosure = new \ReflectionFunction($closure);
|
$reflectionClosure = new \ReflectionFunction($closure);
|
||||||
$expectation = $reflectionClosure->getClosureThis();
|
$expectation = $reflectionClosure->getClosureThis();
|
||||||
|
|
||||||
|
if ($reflectionClosure->getReturnType()?->__toString() === ArchExpectation::class) {
|
||||||
|
return $closure(...$parameters);
|
||||||
|
}
|
||||||
|
|
||||||
assert(is_object($expectation));
|
assert(is_object($expectation));
|
||||||
|
|
||||||
ExpectationPipeline::for($closure)
|
ExpectationPipeline::for($closure)
|
||||||
@ -380,7 +386,7 @@ final class Expectation
|
|||||||
if (self::hasExtend($name)) {
|
if (self::hasExtend($name)) {
|
||||||
$extend = self::$extends[$name]->bindTo($this, Expectation::class);
|
$extend = self::$extends[$name]->bindTo($this, Expectation::class);
|
||||||
|
|
||||||
if ($extend != false) {
|
if ($extend != false) { // @pest-arch-ignore-line
|
||||||
return $extend;
|
return $extend;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -393,7 +399,7 @@ final class Expectation
|
|||||||
*
|
*
|
||||||
* @return Expectation<TValue>|OppositeExpectation<TValue>|EachExpectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue|null>|TValue
|
* @return Expectation<TValue>|OppositeExpectation<TValue>|EachExpectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue|null>|TValue
|
||||||
*/
|
*/
|
||||||
public function __get(string $name)
|
public function __get(string $name): mixed
|
||||||
{
|
{
|
||||||
if (! self::hasMethod($name)) {
|
if (! self::hasMethod($name)) {
|
||||||
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $name)) {
|
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $name)) {
|
||||||
@ -509,12 +515,25 @@ final class Expectation
|
|||||||
{
|
{
|
||||||
return Targeted::make(
|
return Targeted::make(
|
||||||
$this,
|
$this,
|
||||||
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*(\/\*[\s\S]*?\*\/|\/\/[^\r\n]*(?:\r?\n|$)|\s)*declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;/m', (string) file_get_contents($object->path)),
|
||||||
'to use strict types',
|
'to use strict types',
|
||||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target uses strict equality.
|
||||||
|
*/
|
||||||
|
public function toUseStrictEquality(): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this,
|
||||||
|
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' == ') && ! str_contains((string) file_get_contents($object->path), ' != '),
|
||||||
|
'to use strict equality',
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' == ') || str_contains($line, ' != ')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target is final.
|
* Asserts that the given expectation target is final.
|
||||||
*/
|
*/
|
||||||
@ -522,7 +541,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')),
|
||||||
);
|
);
|
||||||
@ -535,7 +554,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')),
|
||||||
);
|
);
|
||||||
@ -548,7 +567,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')),
|
||||||
);
|
);
|
||||||
@ -569,7 +588,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')),
|
||||||
);
|
);
|
||||||
@ -586,7 +605,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')),
|
||||||
);
|
);
|
||||||
@ -657,7 +676,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')),
|
||||||
);
|
);
|
||||||
@ -699,7 +718,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')),
|
||||||
);
|
);
|
||||||
@ -720,7 +739,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')),
|
||||||
);
|
);
|
||||||
@ -760,6 +779,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;
|
||||||
}
|
}
|
||||||
@ -779,7 +802,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')),
|
||||||
);
|
);
|
||||||
@ -796,7 +819,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')),
|
||||||
@ -810,7 +834,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')),
|
||||||
);
|
);
|
||||||
@ -823,7 +847,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')),
|
||||||
);
|
);
|
||||||
@ -842,7 +866,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -872,6 +896,14 @@ final class Expectation
|
|||||||
return ToUseNothing::make($this);
|
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.
|
* Not supported.
|
||||||
*/
|
*/
|
||||||
@ -915,7 +947,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'))
|
||||||
);
|
);
|
||||||
@ -1024,7 +1056,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')),
|
||||||
);
|
);
|
||||||
@ -1053,7 +1085,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',
|
||||||
|
|||||||
@ -15,6 +15,7 @@ use Pest\Arch\PendingArchExpectation;
|
|||||||
use Pest\Arch\SingleArchExpectation;
|
use Pest\Arch\SingleArchExpectation;
|
||||||
use Pest\Arch\Support\FileLineFinder;
|
use Pest\Arch\Support\FileLineFinder;
|
||||||
use Pest\Exceptions\InvalidExpectation;
|
use Pest\Exceptions\InvalidExpectation;
|
||||||
|
use Pest\Exceptions\MissingDependency;
|
||||||
use Pest\Expectation;
|
use Pest\Expectation;
|
||||||
use Pest\Support\Arr;
|
use Pest\Support\Arr;
|
||||||
use Pest\Support\Exporter;
|
use Pest\Support\Exporter;
|
||||||
@ -24,6 +25,7 @@ use PHPUnit\Framework\AssertionFailedError;
|
|||||||
use PHPUnit\Framework\ExpectationFailedException;
|
use PHPUnit\Framework\ExpectationFailedException;
|
||||||
use ReflectionMethod;
|
use ReflectionMethod;
|
||||||
use ReflectionProperty;
|
use ReflectionProperty;
|
||||||
|
use Spoofchecker;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,7 +76,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 +89,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 +113,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 +135,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,22 +158,44 @@ 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')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target does not use the strict equality operator.
|
||||||
|
*/
|
||||||
|
public function toUseStrictEquality(): ArchExpectation
|
||||||
|
{
|
||||||
|
/** @var Expectation<array<int, string>|string> $original */
|
||||||
|
$original = $this->original;
|
||||||
|
|
||||||
|
return Targeted::make(
|
||||||
|
$original,
|
||||||
|
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' === ') && ! str_contains((string) file_get_contents($object->path), ' !== '),
|
||||||
|
'to use strict equality',
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' === ') || str_contains($line, ' !== ')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target is not final.
|
* Asserts that the given expectation target is not final.
|
||||||
*/
|
*/
|
||||||
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')),
|
||||||
);
|
);
|
||||||
@ -170,9 +206,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')),
|
||||||
);
|
);
|
||||||
@ -183,9 +222,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')),
|
||||||
);
|
);
|
||||||
@ -204,9 +246,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')),
|
||||||
);
|
);
|
||||||
@ -221,17 +266,42 @@ 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')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target does not have suspicious characters.
|
||||||
|
*/
|
||||||
|
public function toHaveSuspiciousCharacters(): ArchExpectation
|
||||||
|
{
|
||||||
|
if (! class_exists(Spoofchecker::class)) {
|
||||||
|
throw new MissingDependency(__FUNCTION__, 'ext-intl >= 2.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
$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.
|
* Asserts that the given expectation target does not have the given methods.
|
||||||
*
|
*
|
||||||
@ -253,8 +323,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)
|
||||||
@ -273,7 +346,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)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,8 +369,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)
|
||||||
@ -316,7 +392,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)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,8 +415,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)
|
||||||
@ -359,7 +438,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)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,9 +455,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')),
|
||||||
);
|
);
|
||||||
@ -397,8 +479,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),
|
||||||
@ -418,9 +503,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')),
|
||||||
);
|
);
|
||||||
@ -439,9 +527,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')),
|
||||||
);
|
);
|
||||||
@ -452,9 +543,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')),
|
||||||
);
|
);
|
||||||
@ -477,11 +571,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -502,11 +599,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -523,9 +623,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')),
|
||||||
);
|
);
|
||||||
@ -544,9 +647,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')),
|
||||||
);
|
);
|
||||||
@ -557,9 +663,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')),
|
||||||
);
|
);
|
||||||
@ -586,7 +695,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -596,7 +708,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));
|
||||||
}
|
}
|
||||||
@ -619,9 +734,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'))
|
||||||
);
|
);
|
||||||
@ -632,9 +750,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'))
|
||||||
);
|
);
|
||||||
@ -724,9 +845,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',
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Factories\Covers;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class CoversNothing {}
|
|
||||||
@ -166,7 +166,7 @@ final class TestCaseFactory
|
|||||||
}
|
}
|
||||||
PHP;
|
PHP;
|
||||||
|
|
||||||
eval($classCode); // @phpstan-ignore-line
|
eval($classCode);
|
||||||
} catch (ParseError $caught) {
|
} catch (ParseError $caught) {
|
||||||
throw new RuntimeException(sprintf(
|
throw new RuntimeException(sprintf(
|
||||||
"Unable to create test case for test file at %s. \n %s",
|
"Unable to create test case for test file at %s. \n %s",
|
||||||
|
|||||||
@ -31,8 +31,10 @@ final class TestCaseMethodFactory
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's describing, if any.
|
* The test's describing, if any.
|
||||||
|
*
|
||||||
|
* @var array<int, \Pest\Support\Description>
|
||||||
*/
|
*/
|
||||||
public ?string $describing = null;
|
public array $describing = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's description, if any.
|
* The test's description, if any.
|
||||||
@ -153,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);
|
||||||
@ -201,7 +203,7 @@ final class TestCaseMethodFactory
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($this->depends as $depend) {
|
foreach ($this->depends as $depend) {
|
||||||
$depend = Str::evaluable($this->describing !== null ? Str::describe($this->describing, $depend) : $depend);
|
$depend = Str::evaluable($this->describing === [] ? $depend : Str::describe($this->describing, $depend));
|
||||||
|
|
||||||
$this->attributes[] = new Attribute(
|
$this->attributes[] = new Attribute(
|
||||||
\PHPUnit\Framework\Attributes\Depends::class,
|
\PHPUnit\Framework\Attributes\Depends::class,
|
||||||
|
|||||||
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Pest\Browser\Api\ArrayablePendingAwaitablePage;
|
||||||
|
use Pest\Browser\Api\PendingAwaitablePage;
|
||||||
use Pest\Concerns\Expectable;
|
use Pest\Concerns\Expectable;
|
||||||
use Pest\Configuration;
|
use Pest\Configuration;
|
||||||
use Pest\Exceptions\AfterAllWithinDescribe;
|
use Pest\Exceptions\AfterAllWithinDescribe;
|
||||||
use Pest\Exceptions\BeforeAllWithinDescribe;
|
use Pest\Exceptions\BeforeAllWithinDescribe;
|
||||||
use Pest\Expectation;
|
use Pest\Expectation;
|
||||||
|
use Pest\Installers\PluginBrowser;
|
||||||
use Pest\Mutate\Contracts\MutationTestRunner;
|
use Pest\Mutate\Contracts\MutationTestRunner;
|
||||||
use Pest\Mutate\Repositories\ConfigurationRepository;
|
use Pest\Mutate\Repositories\ConfigurationRepository;
|
||||||
use Pest\PendingCalls\AfterEachCall;
|
use Pest\PendingCalls\AfterEachCall;
|
||||||
@ -18,6 +21,7 @@ use Pest\Repositories\DatasetsRepository;
|
|||||||
use Pest\Support\Backtrace;
|
use Pest\Support\Backtrace;
|
||||||
use Pest\Support\Container;
|
use Pest\Support\Container;
|
||||||
use Pest\Support\DatasetInfo;
|
use Pest\Support\DatasetInfo;
|
||||||
|
use Pest\Support\Description;
|
||||||
use Pest\Support\HigherOrderTapProxy;
|
use Pest\Support\HigherOrderTapProxy;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@ -43,7 +47,7 @@ if (! function_exists('beforeAll')) {
|
|||||||
*/
|
*/
|
||||||
function beforeAll(Closure $closure): void
|
function beforeAll(Closure $closure): void
|
||||||
{
|
{
|
||||||
if (! is_null(DescribeCall::describing())) {
|
if (DescribeCall::describing() !== []) {
|
||||||
$filename = Backtrace::file();
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
throw new BeforeAllWithinDescribe($filename);
|
throw new BeforeAllWithinDescribe($filename);
|
||||||
@ -95,7 +99,7 @@ if (! function_exists('describe')) {
|
|||||||
{
|
{
|
||||||
$filename = Backtrace::testFile();
|
$filename = Backtrace::testFile();
|
||||||
|
|
||||||
return new DescribeCall(TestSuite::getInstance(), $filename, $description, $tests);
|
return new DescribeCall(TestSuite::getInstance(), $filename, new Description($description), $tests);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +209,7 @@ if (! function_exists('afterAll')) {
|
|||||||
*/
|
*/
|
||||||
function afterAll(Closure $closure): void
|
function afterAll(Closure $closure): void
|
||||||
{
|
{
|
||||||
if (! is_null(DescribeCall::describing())) {
|
if (DescribeCall::describing() !== []) {
|
||||||
$filename = Backtrace::file();
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
throw new AfterAllWithinDescribe($filename);
|
throw new AfterAllWithinDescribe($filename);
|
||||||
@ -217,7 +221,7 @@ if (! function_exists('afterAll')) {
|
|||||||
|
|
||||||
if (! function_exists('covers')) {
|
if (! function_exists('covers')) {
|
||||||
/**
|
/**
|
||||||
* Specifies which classes, or functions, a test method covers.
|
* Specifies which classes, or functions, a test case covers.
|
||||||
*
|
*
|
||||||
* @param array<int, string>|string $classesOrFunctions
|
* @param array<int, string>|string $classesOrFunctions
|
||||||
*/
|
*/
|
||||||
@ -243,3 +247,86 @@ if (! function_exists('covers')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! function_exists('mutates')) {
|
||||||
|
/**
|
||||||
|
* Specifies which classes, enums, or traits a test case mutates.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $targets
|
||||||
|
*/
|
||||||
|
function mutates(array|string ...$targets): void
|
||||||
|
{
|
||||||
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
|
$beforeEachCall = (new BeforeEachCall(TestSuite::getInstance(), $filename));
|
||||||
|
$beforeEachCall->group('__pest_mutate_only');
|
||||||
|
|
||||||
|
/** @var MutationTestRunner $runner */
|
||||||
|
$runner = Container::getInstance()->get(MutationTestRunner::class);
|
||||||
|
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */
|
||||||
|
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
|
||||||
|
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
|
||||||
|
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
|
||||||
|
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
|
||||||
|
|
||||||
|
if ($runner->isEnabled() && ! $everything && ! is_array($classes) && ! is_array($paths)) {
|
||||||
|
$beforeEachCall->only('__pest_mutate_only');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var ConfigurationRepository $configurationRepository */
|
||||||
|
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
|
||||||
|
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
|
||||||
|
|
||||||
|
if (! is_array($paths)) {
|
||||||
|
$configurationRepository->globalConfiguration('default')->class(...$targets); // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
* @var array<int, class-string>
|
||||||
*/
|
*/
|
||||||
private const BOOTSTRAPPERS = [
|
private const array BOOTSTRAPPERS = [
|
||||||
Bootstrappers\BootOverrides::class,
|
Bootstrappers\BootOverrides::class,
|
||||||
Bootstrappers\BootSubscribers::class,
|
Bootstrappers\BootSubscribers::class,
|
||||||
Bootstrappers\BootFiles::class,
|
Bootstrappers\BootFiles::class,
|
||||||
@ -71,7 +71,7 @@ final readonly class Kernel
|
|||||||
$output,
|
$output,
|
||||||
);
|
);
|
||||||
|
|
||||||
register_shutdown_function(fn () => $kernel->shutdown());
|
register_shutdown_function($kernel->shutdown(...));
|
||||||
|
|
||||||
foreach (self::BOOTSTRAPPERS as $bootstrapper) {
|
foreach (self::BOOTSTRAPPERS as $bootstrapper) {
|
||||||
$bootstrapper = Container::getInstance()->get($bootstrapper);
|
$bootstrapper = Container::getInstance()->get($bootstrapper);
|
||||||
|
|||||||
@ -40,7 +40,7 @@ final class KernelDump
|
|||||||
*/
|
*/
|
||||||
public function disable(): void
|
public function disable(): void
|
||||||
{
|
{
|
||||||
@ob_clean(); // @phpstan-ignore-line
|
@ob_clean();
|
||||||
|
|
||||||
if ($this->buffer !== '') {
|
if ($this->buffer !== '') {
|
||||||
$this->flush();
|
$this->flush();
|
||||||
|
|||||||
@ -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;
|
||||||
@ -30,7 +31,7 @@ final readonly class Converter
|
|||||||
/**
|
/**
|
||||||
* The prefix for the test suite name.
|
* The prefix for the test suite name.
|
||||||
*/
|
*/
|
||||||
private const PREFIX = 'P\\';
|
private const string PREFIX = 'P\\';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The state generator.
|
* The state generator.
|
||||||
@ -130,7 +131,7 @@ final readonly class Converter
|
|||||||
|
|
||||||
// clean the paths of each frame.
|
// clean the paths of each frame.
|
||||||
$frames = array_map(
|
$frames = array_map(
|
||||||
fn (string $frame): string => $this->toRelativePath($frame),
|
$this->toRelativePath(...),
|
||||||
$frames
|
$frames
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -150,7 +151,7 @@ final readonly class Converter
|
|||||||
{
|
{
|
||||||
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
|
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
|
||||||
$firstTest = $this->getFirstTest($testSuite);
|
$firstTest = $this->getFirstTest($testSuite);
|
||||||
if ($firstTest != null) {
|
if ($firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
|
||||||
return $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
|
return $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -178,7 +179,7 @@ final readonly class Converter
|
|||||||
public function getTestSuiteLocation(TestSuite $testSuite): ?string
|
public function getTestSuiteLocation(TestSuite $testSuite): ?string
|
||||||
{
|
{
|
||||||
$firstTest = $this->getFirstTest($testSuite);
|
$firstTest = $this->getFirstTest($testSuite);
|
||||||
if ($firstTest == null) {
|
if (! $firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$path = $firstTest->testDox()->prettifiedClassName();
|
$path = $firstTest->testDox()->prettifiedClassName();
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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)) {
|
||||||
@ -782,15 +781,13 @@ final class Expectation
|
|||||||
foreach ($array as $key => $value) {
|
foreach ($array as $key => $value) {
|
||||||
Assert::assertArrayHasKey($key, $valueAsArray, $message);
|
Assert::assertArrayHasKey($key, $valueAsArray, $message);
|
||||||
|
|
||||||
if ($message === '') {
|
$assertMessage = $message !== '' ? $message : sprintf(
|
||||||
$message = sprintf(
|
'Failed asserting that an array has a key %s with the value %s.',
|
||||||
'Failed asserting that an array has a key %s with the value %s.',
|
$this->export($key),
|
||||||
$this->export($key),
|
$this->export($valueAsArray[$key]),
|
||||||
$this->export($valueAsArray[$key]),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert::assertEquals($value, $valueAsArray[$key], $message);
|
Assert::assertEquals($value, $valueAsArray[$key], $assertMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -803,7 +800,7 @@ final class Expectation
|
|||||||
* @param iterable<string, mixed> $object
|
* @param iterable<string, mixed> $object
|
||||||
* @return self<TValue>
|
* @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) {
|
foreach ((array) $object as $property => $value) {
|
||||||
if (! is_object($this->value) && ! is_string($this->value)) {
|
if (! is_object($this->value) && ! is_string($this->value)) {
|
||||||
@ -815,15 +812,13 @@ final class Expectation
|
|||||||
/* @phpstan-ignore-next-line */
|
/* @phpstan-ignore-next-line */
|
||||||
$propertyValue = $this->value->{$property};
|
$propertyValue = $this->value->{$property};
|
||||||
|
|
||||||
if ($message === '') {
|
$assertMessage = $message !== '' ? $message : sprintf(
|
||||||
$message = sprintf(
|
'Failed asserting that an object has a property %s with the value %s.',
|
||||||
'Failed asserting that an object has a property %s with the value %s.',
|
$this->export($property),
|
||||||
$this->export($property),
|
$this->export($propertyValue),
|
||||||
$this->export($propertyValue),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert::assertEquals($value, $propertyValue, $message);
|
Assert::assertEquals($value, $propertyValue, $assertMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -1159,4 +1154,21 @@ final class Expectation
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the value can be converted to a slug
|
||||||
|
*
|
||||||
|
* @return self<TValue>
|
||||||
|
*/
|
||||||
|
public function toBeSlug(string $message = ''): self
|
||||||
|
{
|
||||||
|
if ($message === '') {
|
||||||
|
$message = "Failed asserting that {$this->value} can be converted to a slug.";
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = Str::slugify((string) $this->value);
|
||||||
|
Assert::assertNotEmpty($slug, $message);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ namespace Pest\PendingCalls;
|
|||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Pest\PendingCalls\Concerns\Describable;
|
use Pest\PendingCalls\Concerns\Describable;
|
||||||
|
use Pest\Support\Arr;
|
||||||
use Pest\Support\Backtrace;
|
use Pest\Support\Backtrace;
|
||||||
use Pest\Support\ChainableClosure;
|
use Pest\Support\ChainableClosure;
|
||||||
use Pest\Support\HigherOrderMessageCollection;
|
use Pest\Support\HigherOrderMessageCollection;
|
||||||
@ -54,8 +55,8 @@ final class AfterEachCall
|
|||||||
$proxies = $this->proxies;
|
$proxies = $this->proxies;
|
||||||
|
|
||||||
$afterEachTestCase = ChainableClosure::boundWhen(
|
$afterEachTestCase = ChainableClosure::boundWhen(
|
||||||
fn (): bool => is_null($describing) || $this->__describing === $describing,
|
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);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ namespace Pest\PendingCalls;
|
|||||||
use Closure;
|
use Closure;
|
||||||
use Pest\Exceptions\AfterBeforeTestFunction;
|
use Pest\Exceptions\AfterBeforeTestFunction;
|
||||||
use Pest\PendingCalls\Concerns\Describable;
|
use Pest\PendingCalls\Concerns\Describable;
|
||||||
|
use Pest\Support\Arr;
|
||||||
use Pest\Support\Backtrace;
|
use Pest\Support\Backtrace;
|
||||||
use Pest\Support\ChainableClosure;
|
use Pest\Support\ChainableClosure;
|
||||||
use Pest\Support\HigherOrderMessageCollection;
|
use Pest\Support\HigherOrderMessageCollection;
|
||||||
@ -63,12 +64,12 @@ final class BeforeEachCall
|
|||||||
|
|
||||||
$beforeEachTestCall = function (TestCall $testCall) use ($describing): void {
|
$beforeEachTestCall = function (TestCall $testCall) use ($describing): void {
|
||||||
|
|
||||||
if ($this->describing !== null) {
|
if ($this->describing !== []) {
|
||||||
if ($describing !== $this->describing) {
|
if (Arr::last($describing) !== Arr::last($this->describing)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($describing !== $testCall->describing) {
|
if (! in_array(Arr::last($describing), $testCall->describing, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,8 +78,8 @@ final class BeforeEachCall
|
|||||||
};
|
};
|
||||||
|
|
||||||
$beforeEachTestCase = ChainableClosure::boundWhen(
|
$beforeEachTestCase = ChainableClosure::boundWhen(
|
||||||
fn (): bool => is_null($describing) || $this->__describing === $describing,
|
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);
|
||||||
@ -96,7 +97,7 @@ final class BeforeEachCall
|
|||||||
*/
|
*/
|
||||||
public function after(Closure $closure): self
|
public function after(Closure $closure): self
|
||||||
{
|
{
|
||||||
if ($this->describing === null) {
|
if ($this->describing === []) {
|
||||||
throw new AfterBeforeTestFunction($this->filename);
|
throw new AfterBeforeTestFunction($this->filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,11 +11,15 @@ trait Describable
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Note: this is property is not used; however, it gets added automatically by rector php.
|
* Note: this is property is not used; however, it gets added automatically by rector php.
|
||||||
|
*
|
||||||
|
* @var array<int, \Pest\Support\Description>
|
||||||
*/
|
*/
|
||||||
public string $__describing;
|
public array $__describing;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The describing of the test case.
|
* The describing of the test case.
|
||||||
|
*
|
||||||
|
* @var array<int, \Pest\Support\Description>
|
||||||
*/
|
*/
|
||||||
public ?string $describing = null;
|
public array $describing = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ namespace Pest\PendingCalls;
|
|||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Pest\Support\Backtrace;
|
use Pest\Support\Backtrace;
|
||||||
|
use Pest\Support\Description;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,8 +16,10 @@ final class DescribeCall
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The current describe call.
|
* The current describe call.
|
||||||
|
*
|
||||||
|
* @var array<int, Description>
|
||||||
*/
|
*/
|
||||||
private static ?string $describing = null;
|
private static array $describing = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The describe "before each" call.
|
* The describe "before each" call.
|
||||||
@ -29,7 +32,7 @@ final class DescribeCall
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly TestSuite $testSuite,
|
public readonly TestSuite $testSuite,
|
||||||
public readonly string $filename,
|
public readonly string $filename,
|
||||||
public readonly string $description,
|
public readonly Description $description,
|
||||||
public readonly Closure $tests
|
public readonly Closure $tests
|
||||||
) {
|
) {
|
||||||
//
|
//
|
||||||
@ -37,8 +40,10 @@ final class DescribeCall
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* What is the current describing.
|
* What is the current describing.
|
||||||
|
*
|
||||||
|
* @return array<int, Description>
|
||||||
*/
|
*/
|
||||||
public static function describing(): ?string
|
public static function describing(): array
|
||||||
{
|
{
|
||||||
return self::$describing;
|
return self::$describing;
|
||||||
}
|
}
|
||||||
@ -50,12 +55,12 @@ final class DescribeCall
|
|||||||
{
|
{
|
||||||
unset($this->currentBeforeEachCall);
|
unset($this->currentBeforeEachCall);
|
||||||
|
|
||||||
self::$describing = $this->description;
|
self::$describing[] = $this->description;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
($this->tests)();
|
($this->tests)();
|
||||||
} finally {
|
} finally {
|
||||||
self::$describing = null;
|
array_pop(self::$describing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,10 +76,10 @@ final class DescribeCall
|
|||||||
if (! $this->currentBeforeEachCall instanceof \Pest\PendingCalls\BeforeEachCall) {
|
if (! $this->currentBeforeEachCall instanceof \Pest\PendingCalls\BeforeEachCall) {
|
||||||
$this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename);
|
$this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename);
|
||||||
|
|
||||||
$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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,12 +5,14 @@ declare(strict_types=1);
|
|||||||
namespace Pest\PendingCalls;
|
namespace Pest\PendingCalls;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Pest\Concerns\Testable;
|
||||||
use Pest\Exceptions\InvalidArgumentException;
|
use Pest\Exceptions\InvalidArgumentException;
|
||||||
use Pest\Exceptions\TestDescriptionMissing;
|
use Pest\Exceptions\TestDescriptionMissing;
|
||||||
use Pest\Factories\Attribute;
|
use Pest\Factories\Attribute;
|
||||||
use Pest\Factories\TestCaseMethodFactory;
|
use Pest\Factories\TestCaseMethodFactory;
|
||||||
use Pest\Mutate\Repositories\ConfigurationRepository;
|
use Pest\Mutate\Repositories\ConfigurationRepository;
|
||||||
use Pest\PendingCalls\Concerns\Describable;
|
use Pest\PendingCalls\Concerns\Describable;
|
||||||
|
use Pest\Plugins\Environment;
|
||||||
use Pest\Plugins\Only;
|
use Pest\Plugins\Only;
|
||||||
use Pest\Support\Backtrace;
|
use Pest\Support\Backtrace;
|
||||||
use Pest\Support\Container;
|
use Pest\Support\Container;
|
||||||
@ -25,9 +27,9 @@ use PHPUnit\Framework\TestCase;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*
|
*
|
||||||
* @mixin HigherOrderCallables|TestCase
|
* @mixin HigherOrderCallables|TestCase|Testable
|
||||||
*/
|
*/
|
||||||
final class TestCall
|
final class TestCall // @phpstan-ignore-line
|
||||||
{
|
{
|
||||||
use Describable;
|
use Describable;
|
||||||
|
|
||||||
@ -75,7 +77,7 @@ final class TestCall
|
|||||||
throw new TestDescriptionMissing($this->filename);
|
throw new TestDescriptionMissing($this->filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
$description = is_null($this->describing)
|
$description = $this->describing === []
|
||||||
? $this->description
|
? $this->description
|
||||||
: Str::describe($this->describing, $this->description);
|
: Str::describe($this->describing, $this->description);
|
||||||
|
|
||||||
@ -177,10 +179,9 @@ final class TestCall
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the current test multiple times with
|
* Runs the current test multiple times with each item of the given `iterable`.
|
||||||
* each item of the given `iterable`.
|
|
||||||
*
|
*
|
||||||
* @param array<\Closure|iterable<int|string, mixed>|string> $data
|
* @param Closure|iterable<array-key, mixed>|string $data
|
||||||
*/
|
*/
|
||||||
public function with(Closure|iterable|string ...$data): self
|
public function with(Closure|iterable|string ...$data): self
|
||||||
{
|
{
|
||||||
@ -223,7 +224,7 @@ final class TestCall
|
|||||||
*/
|
*/
|
||||||
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;
|
||||||
}
|
}
|
||||||
@ -314,6 +315,61 @@ final class TestCall
|
|||||||
: $this;
|
: $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weather the current test is running on a CI environment.
|
||||||
|
*/
|
||||||
|
private function runningOnCI(): bool
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
'CI',
|
||||||
|
'GITHUB_ACTIONS',
|
||||||
|
'GITLAB_CI',
|
||||||
|
'CIRCLECI',
|
||||||
|
'TRAVIS',
|
||||||
|
'APPVEYOR',
|
||||||
|
'BITBUCKET_BUILD_NUMBER',
|
||||||
|
'BUILDKITE',
|
||||||
|
'TEAMCITY_VERSION',
|
||||||
|
'JENKINS_URL',
|
||||||
|
'SYSTEM_COLLECTIONURI',
|
||||||
|
'CI_NAME',
|
||||||
|
'TASKCLUSTER_ROOT_URL',
|
||||||
|
'DRONE',
|
||||||
|
'WERCKER',
|
||||||
|
'NEVERCODE',
|
||||||
|
'SEMAPHORE',
|
||||||
|
'NETLIFY',
|
||||||
|
'NOW_BUILDER',
|
||||||
|
] as $env) {
|
||||||
|
if (getenv($env) !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Environment::name() === Environment::CI;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips the current test when running on a CI environments.
|
||||||
|
*/
|
||||||
|
public function skipOnCI(): self
|
||||||
|
{
|
||||||
|
if ($this->runningOnCI()) {
|
||||||
|
return $this->skip('This test is skipped on [CI].');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function skipLocally(): self
|
||||||
|
{
|
||||||
|
if ($this->runningOnCI() === false) {
|
||||||
|
return $this->skip('This test is skipped [locally].');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skips the current test unless the given test is running on Windows.
|
* Skips the current test unless the given test is running on Windows.
|
||||||
*/
|
*/
|
||||||
@ -603,18 +659,29 @@ final class TestCall
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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(
|
assert($classes !== []);
|
||||||
\PHPUnit\Framework\Attributes\CoversNothing::class,
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds one or more references to the tested method or class. This helps
|
||||||
|
* to link test cases to the source code for easier navigation.
|
||||||
|
*
|
||||||
|
* @param array<class-string|string>|class-string ...$classes
|
||||||
|
*/
|
||||||
|
public function see(string|array ...$classes): self
|
||||||
|
{
|
||||||
|
return $this->references(...$classes);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Informs the test runner that no expectations happen in this test,
|
* Informs the test runner that no expectations happen in this test,
|
||||||
* and its purpose is simply to check whether the given code can
|
* and its purpose is simply to check whether the given code can
|
||||||
@ -682,7 +749,7 @@ final class TestCall
|
|||||||
throw new TestDescriptionMissing($this->filename);
|
throw new TestDescriptionMissing($this->filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! is_null($this->describing)) {
|
if ($this->describing !== []) {
|
||||||
$this->testCaseMethod->describing = $this->describing;
|
$this->testCaseMethod->describing = $this->describing;
|
||||||
$this->testCaseMethod->description = Str::describe($this->describing, $this->description);
|
$this->testCaseMethod->description = Str::describe($this->describing, $this->description);
|
||||||
} else {
|
} else {
|
||||||
@ -692,7 +759,12 @@ final class TestCall
|
|||||||
$this->testSuite->tests->set($this->testCaseMethod);
|
$this->testSuite->tests->set($this->testCaseMethod);
|
||||||
|
|
||||||
if (! is_null($testCase = $this->testSuite->tests->get($this->filename))) {
|
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
|
function version(): string
|
||||||
{
|
{
|
||||||
return '3.0.8';
|
return '4.4.1';
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDirectory(string $file = ''): string
|
function testDirectory(string $file = ''): string
|
||||||
|
|||||||
@ -21,7 +21,7 @@ final class Cache implements HandlesArguments
|
|||||||
/**
|
/**
|
||||||
* The temporary folder.
|
* The temporary folder.
|
||||||
*/
|
*/
|
||||||
private const TEMPORARY_FOLDER = __DIR__
|
private const string TEMPORARY_FOLDER = __DIR__
|
||||||
.DIRECTORY_SEPARATOR
|
.DIRECTORY_SEPARATOR
|
||||||
.'..'
|
.'..'
|
||||||
.DIRECTORY_SEPARATOR
|
.DIRECTORY_SEPARATOR
|
||||||
|
|||||||
@ -21,7 +21,7 @@ final class Configuration implements HandlesArguments, Terminable
|
|||||||
/**
|
/**
|
||||||
* The base PHPUnit file.
|
* The base PHPUnit file.
|
||||||
*/
|
*/
|
||||||
public const BASE_PHPUNIT_FILE = __DIR__
|
public const string BASE_PHPUNIT_FILE = __DIR__
|
||||||
.DIRECTORY_SEPARATOR
|
.DIRECTORY_SEPARATOR
|
||||||
.'..'
|
.'..'
|
||||||
.DIRECTORY_SEPARATOR
|
.DIRECTORY_SEPARATOR
|
||||||
@ -34,7 +34,7 @@ final class Configuration implements HandlesArguments, Terminable
|
|||||||
*/
|
*/
|
||||||
public function handleArguments(array $arguments): array
|
public function handleArguments(array $arguments): array
|
||||||
{
|
{
|
||||||
if ($this->hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) {
|
if ($this->hasArgument('--configuration', $arguments) || $this->hasArgument('-c', $arguments) || $this->hasCustomConfigurationFile()) {
|
||||||
return $arguments;
|
return $arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,26 +17,32 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
*/
|
*/
|
||||||
final class Coverage implements AddsOutput, HandlesArguments
|
final class Coverage implements AddsOutput, HandlesArguments
|
||||||
{
|
{
|
||||||
/**
|
private const string COVERAGE_OPTION = 'coverage';
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private const COVERAGE_OPTION = 'coverage';
|
|
||||||
|
|
||||||
/**
|
private const string MIN_OPTION = 'min';
|
||||||
* @var string
|
|
||||||
*/
|
private const string EXACTLY_OPTION = 'exactly';
|
||||||
private const MIN_OPTION = 'min';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 +57,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 +79,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 +113,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 +144,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 +173,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,12 +14,12 @@ final class Environment implements HandlesArguments
|
|||||||
/**
|
/**
|
||||||
* The continuous integration environment.
|
* The continuous integration environment.
|
||||||
*/
|
*/
|
||||||
public const CI = 'ci';
|
public const string CI = 'ci';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The local environment.
|
* The local environment.
|
||||||
*/
|
*/
|
||||||
public const LOCAL = 'local';
|
public const string LOCAL = 'local';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current environment.
|
* The current environment.
|
||||||
|
|||||||
@ -99,6 +99,7 @@ final readonly class Help implements HandlesArguments
|
|||||||
{
|
{
|
||||||
$helpReflection = new PHPUnitHelp;
|
$helpReflection = new PHPUnitHelp;
|
||||||
|
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
$content = (fn (): array => $this->elements())->call($helpReflection);
|
$content = (fn (): array => $this->elements())->call($helpReflection);
|
||||||
|
|
||||||
$content['Configuration'] = [...[[
|
$content['Configuration'] = [...[[
|
||||||
@ -141,6 +142,9 @@ final readonly class Help implements HandlesArguments
|
|||||||
], [
|
], [
|
||||||
'arg' => '--retry',
|
'arg' => '--retry',
|
||||||
'desc' => 'Run non-passing tests first and stop execution upon first error or failure',
|
'desc' => 'Run non-passing tests first and stop execution upon first error or failure',
|
||||||
|
], [
|
||||||
|
'arg' => '--dirty',
|
||||||
|
'desc' => 'Only run tests that have uncommitted changes according to Git',
|
||||||
], ...$content['Selection']];
|
], ...$content['Selection']];
|
||||||
|
|
||||||
$content['Reporting'] = [...$content['Reporting'], ...[
|
$content['Reporting'] = [...$content['Reporting'], ...[
|
||||||
|
|||||||
@ -20,12 +20,12 @@ final readonly class Init implements HandlesArguments
|
|||||||
/**
|
/**
|
||||||
* The option the triggers the init job.
|
* The option the triggers the init job.
|
||||||
*/
|
*/
|
||||||
private const INIT_OPTION = '--init';
|
private const string INIT_OPTION = '--init';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The files that will be created.
|
* The files that will be created.
|
||||||
*/
|
*/
|
||||||
private const STUBS = [
|
private const array STUBS = [
|
||||||
'phpunit.xml.stub' => 'phpunit.xml',
|
'phpunit.xml.stub' => 'phpunit.xml',
|
||||||
'Pest.php.stub' => 'tests/Pest.php',
|
'Pest.php.stub' => 'tests/Pest.php',
|
||||||
'TestCase.php.stub' => 'tests/TestCase.php',
|
'TestCase.php.stub' => 'tests/TestCase.php',
|
||||||
@ -119,6 +119,6 @@ final readonly class Init implements HandlesArguments
|
|||||||
*/
|
*/
|
||||||
private function isLaravelInstalled(): bool
|
private function isLaravelInstalled(): bool
|
||||||
{
|
{
|
||||||
return InstalledVersions::isInstalled('laravel/laravel');
|
return InstalledVersions::isInstalled('laravel/framework');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,10 @@ declare(strict_types=1);
|
|||||||
namespace Pest\Plugins;
|
namespace Pest\Plugins;
|
||||||
|
|
||||||
use Pest\Contracts\Plugins\Terminable;
|
use Pest\Contracts\Plugins\Terminable;
|
||||||
|
use Pest\Factories\Attribute;
|
||||||
|
use Pest\Factories\TestCaseMethodFactory;
|
||||||
use Pest\PendingCalls\TestCall;
|
use Pest\PendingCalls\TestCall;
|
||||||
|
use PHPUnit\Framework\Attributes\Group;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@ -15,7 +18,7 @@ final class Only implements Terminable
|
|||||||
/**
|
/**
|
||||||
* The temporary folder.
|
* The temporary folder.
|
||||||
*/
|
*/
|
||||||
private const TEMPORARY_FOLDER = __DIR__
|
private const string TEMPORARY_FOLDER = __DIR__
|
||||||
.DIRECTORY_SEPARATOR
|
.DIRECTORY_SEPARATOR
|
||||||
.'..'
|
.'..'
|
||||||
.DIRECTORY_SEPARATOR
|
.DIRECTORY_SEPARATOR
|
||||||
@ -23,28 +26,19 @@ final class Only implements Terminable
|
|||||||
.DIRECTORY_SEPARATOR
|
.DIRECTORY_SEPARATOR
|
||||||
.'.temp';
|
.'.temp';
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public function terminate(): void
|
|
||||||
{
|
|
||||||
if (Parallel::isWorker()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
|
|
||||||
|
|
||||||
if (file_exists($lockFile)) {
|
|
||||||
unlink($lockFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the lock file.
|
* Creates the lock file.
|
||||||
*/
|
*/
|
||||||
public static function enable(TestCall $testCall, string $group = '__pest_only'): void
|
public static function enable(TestCall|TestCaseMethodFactory $testCall, string $group = '__pest_only'): void
|
||||||
{
|
{
|
||||||
$testCall->group($group);
|
if ($testCall instanceof TestCall) {
|
||||||
|
$testCall->group($group);
|
||||||
|
} else {
|
||||||
|
$testCall->attributes[] = new Attribute(
|
||||||
|
Group::class,
|
||||||
|
[$group],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (Environment::name() === Environment::CI || Parallel::isWorker()) {
|
if (Environment::name() === Environment::CI || Parallel::isWorker()) {
|
||||||
return;
|
return;
|
||||||
@ -88,4 +82,20 @@ final class Only implements Terminable
|
|||||||
|
|
||||||
return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line
|
return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function terminate(): void
|
||||||
|
{
|
||||||
|
if (Parallel::isWorker()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
|
||||||
|
|
||||||
|
if (file_exists($lockFile)) {
|
||||||
|
unlink($lockFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,9 +23,9 @@ final class Parallel implements HandlesArguments
|
|||||||
{
|
{
|
||||||
use HandleArguments;
|
use HandleArguments;
|
||||||
|
|
||||||
private const GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_';
|
private const string GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_';
|
||||||
|
|
||||||
private const HANDLERS = [
|
private const array HANDLERS = [
|
||||||
Parallel\Handlers\Parallel::class,
|
Parallel\Handlers\Parallel::class,
|
||||||
Parallel\Handlers\Pest::class,
|
Parallel\Handlers\Pest::class,
|
||||||
Parallel\Handlers\Laravel::class,
|
Parallel\Handlers\Laravel::class,
|
||||||
@ -34,7 +34,7 @@ final class Parallel implements HandlesArguments
|
|||||||
/**
|
/**
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request'];
|
private const array UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the given command line arguments indicate that the test suite should be run in parallel.
|
* Whether the given command line arguments indicate that the test suite should be run in parallel.
|
||||||
|
|||||||
@ -18,7 +18,7 @@ final class Parallel implements HandlesArguments
|
|||||||
/**
|
/**
|
||||||
* The list of arguments to remove.
|
* The list of arguments to remove.
|
||||||
*/
|
*/
|
||||||
private const ARGS_TO_REMOVE = [
|
private const array ARGS_TO_REMOVE = [
|
||||||
'--parallel',
|
'--parallel',
|
||||||
'-p',
|
'-p',
|
||||||
'--no-output',
|
'--no-output',
|
||||||
|
|||||||
@ -11,6 +11,7 @@ final class CleanConsoleOutput extends ConsoleOutput
|
|||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
#[\Override]
|
||||||
protected function doWrite(string $message, bool $newline): void // @pest-arch-ignore-line
|
protected function doWrite(string $message, bool $newline): void // @pest-arch-ignore-line
|
||||||
{
|
{
|
||||||
if ($this->isOpeningHeadline($message)) {
|
if ($this->isOpeningHeadline($message)) {
|
||||||
|
|||||||
@ -59,10 +59,10 @@ final class ResultPrinter
|
|||||||
private readonly OutputInterface $output,
|
private readonly OutputInterface $output,
|
||||||
private readonly Options $options
|
private readonly Options $options
|
||||||
) {
|
) {
|
||||||
$this->printer = new class($this->output) implements Printer
|
$this->printer = new readonly class($this->output) implements Printer
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly OutputInterface $output,
|
private OutputInterface $output,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function print(string $buffer): void
|
public function print(string $buffer): void
|
||||||
|
|||||||
@ -17,8 +17,10 @@ use ParaTest\WrapperRunner\WrapperWorker;
|
|||||||
use Pest\Result;
|
use Pest\Result;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use PHPUnit\Event\Facade as EventFacade;
|
use PHPUnit\Event\Facade as EventFacade;
|
||||||
|
use PHPUnit\Event\Test\AfterLastTestMethodFailed;
|
||||||
use PHPUnit\Event\TestRunner\WarningTriggered;
|
use PHPUnit\Event\TestRunner\WarningTriggered;
|
||||||
use PHPUnit\Runner\CodeCoverage;
|
use PHPUnit\Runner\CodeCoverage;
|
||||||
|
use PHPUnit\Runner\ResultCache\DefaultResultCache;
|
||||||
use PHPUnit\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;
|
||||||
@ -49,7 +51,7 @@ final class WrapperRunner implements RunnerInterface
|
|||||||
/**
|
/**
|
||||||
* The time to sleep between cycles.
|
* The time to sleep between cycles.
|
||||||
*/
|
*/
|
||||||
private const CYCLE_SLEEP = 10000;
|
private const int CYCLE_SLEEP = 10000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The result printer.
|
* The result printer.
|
||||||
@ -79,7 +81,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 = [];
|
||||||
@ -126,6 +131,7 @@ final class WrapperRunner implements RunnerInterface
|
|||||||
$parameters = $this->handleLaravelHerd($parameters);
|
$parameters = $this->handleLaravelHerd($parameters);
|
||||||
|
|
||||||
$parameters[] = $wrapper;
|
$parameters[] = $wrapper;
|
||||||
|
$parameters[] = '--test-directory='.TestSuite::getInstance()->testPath;
|
||||||
|
|
||||||
$this->parameters = $parameters;
|
$this->parameters = $parameters;
|
||||||
$this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry;
|
$this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry;
|
||||||
@ -264,7 +270,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,37 +305,52 @@ 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);
|
||||||
|
|
||||||
|
/** @var list<AfterLastTestMethodFailed> $failedEvents */
|
||||||
|
$failedEvents = array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents());
|
||||||
|
|
||||||
$testResultSum = new TestResult(
|
$testResultSum = new TestResult(
|
||||||
(int) $testResultSum->hasTests() + (int) $testResult->hasTests(),
|
(int) $testResultSum->hasTests() + (int) $testResult->hasTests(),
|
||||||
$testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
|
$testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
|
||||||
$testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(),
|
$testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(),
|
||||||
array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()),
|
array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()),
|
||||||
array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()),
|
$failedEvents,
|
||||||
array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()),
|
array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()),
|
||||||
array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()),
|
array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()),
|
||||||
array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()),
|
array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()),
|
||||||
array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()),
|
array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()),
|
||||||
array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()),
|
array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()),
|
||||||
array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()),
|
array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()),
|
||||||
array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()),
|
array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()),
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()),
|
array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()),
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
array_merge_recursive($testResultSum->testRunnerTriggeredNoticeEvents(), $testResult->testRunnerTriggeredNoticeEvents()),
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
|
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
array_merge_recursive($testResultSum->errors(), $testResult->errors()),
|
array_merge_recursive($testResultSum->errors(), $testResult->errors()),
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
|
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
array_merge_recursive($testResultSum->notices(), $testResult->notices()),
|
array_merge_recursive($testResultSum->notices(), $testResult->notices()),
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
array_merge_recursive($testResultSum->warnings(), $testResult->warnings()),
|
array_merge_recursive($testResultSum->warnings(), $testResult->warnings()),
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()),
|
array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()),
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()),
|
array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()),
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()),
|
array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()),
|
||||||
$testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
|
$testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
|
||||||
);
|
);
|
||||||
@ -346,8 +368,10 @@ final class WrapperRunner implements RunnerInterface
|
|||||||
$testResultSum->testMarkedIncompleteEvents(),
|
$testResultSum->testMarkedIncompleteEvents(),
|
||||||
$testResultSum->testTriggeredPhpunitDeprecationEvents(),
|
$testResultSum->testTriggeredPhpunitDeprecationEvents(),
|
||||||
$testResultSum->testTriggeredPhpunitErrorEvents(),
|
$testResultSum->testTriggeredPhpunitErrorEvents(),
|
||||||
|
$testResultSum->testTriggeredPhpunitNoticeEvents(),
|
||||||
$testResultSum->testTriggeredPhpunitWarningEvents(),
|
$testResultSum->testTriggeredPhpunitWarningEvents(),
|
||||||
$testResultSum->testRunnerTriggeredDeprecationEvents(),
|
$testResultSum->testRunnerTriggeredDeprecationEvents(),
|
||||||
|
$testResultSum->testRunnerTriggeredNoticeEvents(),
|
||||||
array_values(array_filter(
|
array_values(array_filter(
|
||||||
$testResultSum->testRunnerTriggeredWarningEvents(),
|
$testResultSum->testRunnerTriggeredWarningEvents(),
|
||||||
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')
|
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')
|
||||||
@ -360,9 +384,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 +410,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);
|
||||||
|
|||||||
@ -34,7 +34,7 @@ final class CompactPrinter
|
|||||||
/**
|
/**
|
||||||
* @var array<string, array<int, string>>
|
* @var array<string, array<int, string>>
|
||||||
*/
|
*/
|
||||||
private const LOOKUP_TABLE = [
|
private const array LOOKUP_TABLE = [
|
||||||
'.' => ['gray', '.'],
|
'.' => ['gray', '.'],
|
||||||
'S' => ['yellow', 's'],
|
'S' => ['yellow', 's'],
|
||||||
'T' => ['cyan', 't'],
|
'T' => ['cyan', 't'],
|
||||||
@ -131,14 +131,14 @@ final class CompactPrinter
|
|||||||
$status['collected'],
|
$status['collected'],
|
||||||
$status['threshold'],
|
$status['threshold'],
|
||||||
$status['roots'],
|
$status['roots'],
|
||||||
null,
|
0.00,
|
||||||
null,
|
0.00,
|
||||||
null,
|
0.00,
|
||||||
null,
|
0.00,
|
||||||
null,
|
false,
|
||||||
null,
|
false,
|
||||||
null,
|
false,
|
||||||
null,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
$telemetry = new Info(
|
$telemetry = new Info(
|
||||||
|
|||||||
177
src/Plugins/Shard.php
Normal file
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.
|
* The list of verbosity levels.
|
||||||
*/
|
*/
|
||||||
private const VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
|
private const array VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ use function sprintf;
|
|||||||
*/
|
*/
|
||||||
final class DatasetsRepository
|
final class DatasetsRepository
|
||||||
{
|
{
|
||||||
private const SEPARATOR = '>>';
|
private const string SEPARATOR = '>>';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the datasets.
|
* Holds the datasets.
|
||||||
@ -67,11 +67,11 @@ final class DatasetsRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Closure|array<int|string, mixed>
|
* @return array<int|string, mixed>
|
||||||
*
|
*
|
||||||
* @throws ShouldNotHappen
|
* @throws ShouldNotHappen
|
||||||
*/
|
*/
|
||||||
public static function get(string $filename, string $description): Closure|array
|
public static function get(string $filename, string $description): 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']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,6 +191,7 @@ final class DatasetsRepository
|
|||||||
return str_starts_with($currentTestFile, $datasetScope);
|
return str_starts_with($currentTestFile, $datasetScope);
|
||||||
}, ARRAY_FILTER_USE_KEY);
|
}, ARRAY_FILTER_USE_KEY);
|
||||||
|
|
||||||
|
/** @var string|null $closestScopeDatasetKey */
|
||||||
$closestScopeDatasetKey = array_reduce(
|
$closestScopeDatasetKey = array_reduce(
|
||||||
array_keys($matchingDatasets),
|
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
|
fn (string|int|null $keyA, string|int|null $keyB): string|int|null => $keyA !== null && strlen((string) $keyA) > strlen((string) $keyB) ? $keyA : $keyB
|
||||||
@ -221,7 +221,6 @@ final class DatasetsRepository
|
|||||||
$result = $tmp;
|
$result = $tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @phpstan-ignore-next-line
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,8 +19,9 @@ 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 $rootPath,
|
||||||
readonly private string $snapshotsPath,
|
private readonly string $testsPath,
|
||||||
|
private readonly string $snapshotsPath,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,7 +104,19 @@ final class SnapshotRepository
|
|||||||
*/
|
*/
|
||||||
private function getSnapshotFilename(): string
|
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
|
// remove extension from filename
|
||||||
$relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.'));
|
$relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.'));
|
||||||
|
|||||||
@ -4,20 +4,16 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest;
|
namespace Pest;
|
||||||
|
|
||||||
use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection;
|
|
||||||
use PHPUnit\TestRunner\TestResult\TestResult;
|
use PHPUnit\TestRunner\TestResult\TestResult;
|
||||||
use PHPUnit\TextUI\Configuration\Configuration;
|
use PHPUnit\TextUI\Configuration\Configuration;
|
||||||
|
use PHPUnit\TextUI\ShellExitCodeCalculator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Result
|
final class Result
|
||||||
{
|
{
|
||||||
private const SUCCESS_EXIT = 0;
|
private const int SUCCESS_EXIT = 0;
|
||||||
|
|
||||||
private const FAILURE_EXIT = 1;
|
|
||||||
|
|
||||||
private const EXCEPTION_EXIT = 2;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the exit code is different from 0.
|
* If the exit code is different from 0.
|
||||||
@ -40,44 +36,8 @@ final class Result
|
|||||||
*/
|
*/
|
||||||
public static function exitCode(Configuration $configuration, TestResult $result): int
|
public static function exitCode(Configuration $configuration, TestResult $result): int
|
||||||
{
|
{
|
||||||
if ($result->wasSuccessfulIgnoringPhpunitWarnings()) {
|
$shell = new ShellExitCodeCalculator;
|
||||||
if ($configuration->failOnWarning()) {
|
|
||||||
$warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents()
|
|
||||||
+ count($result->warnings())
|
|
||||||
+ count($result->phpWarnings());
|
|
||||||
|
|
||||||
if ($warnings > 0) {
|
return $shell->calculate($configuration, $result);
|
||||||
return self::FAILURE_EXIT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $result->hasTestTriggeredPhpunitWarningEvents()) {
|
|
||||||
return self::SUCCESS_EXIT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) {
|
|
||||||
return self::FAILURE_EXIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($result->wasSuccessfulIgnoringPhpunitWarnings()) {
|
|
||||||
if ($configuration->failOnRisky() && $result->hasTestConsideredRiskyEvents()) {
|
|
||||||
$returnCode = self::FAILURE_EXIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($configuration->failOnIncomplete() && $result->hasTestMarkedIncompleteEvents()) {
|
|
||||||
$returnCode = self::FAILURE_EXIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($configuration->failOnSkipped() && $result->hasTestSkippedEvents()) {
|
|
||||||
$returnCode = self::FAILURE_EXIT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($result->hasTestErroredEvents()) {
|
|
||||||
return self::EXCEPTION_EXIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::FAILURE_EXIT;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,19 +23,17 @@ 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);
|
||||||
|
|
||||||
$testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => $event->message() !== 'No tests found in class "Pest\TestCases\IgnorableTestCase".'));
|
$testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => str_contains($event->message(), 'No tests found in class') === false));
|
||||||
|
|
||||||
$property->setValue($collector, $testRunnerTriggeredWarningEvents);
|
$property->setValue($collector, $testRunnerTriggeredWarningEvents);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,4 +81,14 @@ final class Arr
|
|||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the last element or false for empty array
|
||||||
|
*
|
||||||
|
* @param array<array-key, mixed> $array
|
||||||
|
*/
|
||||||
|
public static function last(array $array): mixed
|
||||||
|
{
|
||||||
|
return end($array);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,12 +11,9 @@ use Pest\Exceptions\ShouldNotHappen;
|
|||||||
*/
|
*/
|
||||||
final class Backtrace
|
final class Backtrace
|
||||||
{
|
{
|
||||||
/**
|
private const string FILE = 'file';
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private const FILE = 'file';
|
|
||||||
|
|
||||||
private const BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS;
|
private const int BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current test file.
|
* Returns the current test file.
|
||||||
|
|||||||
@ -15,18 +15,18 @@ 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
|
||||||
{
|
{
|
||||||
if ($closure == null) {
|
if (! $closure instanceof \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 == false) {
|
if (! $closure instanceof \Closure) {
|
||||||
throw ShouldNotHappen::fromMessage('Could not bind closure.');
|
throw ShouldNotHappen::fromMessage('Could not bind closure.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -11,9 +11,9 @@ use function Pest\testDirectory;
|
|||||||
*/
|
*/
|
||||||
final class DatasetInfo
|
final class DatasetInfo
|
||||||
{
|
{
|
||||||
public const DATASETS_DIR_NAME = 'Datasets';
|
public const string DATASETS_DIR_NAME = 'Datasets';
|
||||||
|
|
||||||
public const DATASETS_FILE_NAME = 'Datasets.php';
|
public const string DATASETS_FILE_NAME = 'Datasets.php';
|
||||||
|
|
||||||
public static function isInsideADatasetsDirectory(string $file): bool
|
public static function isInsideADatasetsDirectory(string $file): bool
|
||||||
{
|
{
|
||||||
|
|||||||
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
|
final class ExceptionTrace
|
||||||
{
|
{
|
||||||
private const UNDEFINED_METHOD = 'Call to undefined method P\\';
|
private const string UNDEFINED_METHOD = 'Call to undefined method P\\';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures the given closure reports the good execution context.
|
* Ensures the given closure reports the good execution context.
|
||||||
|
|||||||
@ -15,7 +15,7 @@ final readonly class Exporter
|
|||||||
/**
|
/**
|
||||||
* The maximum number of items in an array to export.
|
* The maximum number of items in an array to export.
|
||||||
*/
|
*/
|
||||||
private const MAX_ARRAY_ITEMS = 3;
|
private const int MAX_ARRAY_ITEMS = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Exporter instance.
|
* Creates a new Exporter instance.
|
||||||
@ -66,6 +66,7 @@ final readonly class Exporter
|
|||||||
|
|
||||||
$result[] = $context->contains($data[$key]) !== false
|
$result[] = $context->contains($data[$key]) !== false
|
||||||
? '*RECURSION*'
|
? '*RECURSION*'
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
: sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context));
|
: sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,7 @@ final readonly class HigherOrderCallables
|
|||||||
*/
|
*/
|
||||||
public function and(mixed $value): Expectation
|
public function and(mixed $value): Expectation
|
||||||
{
|
{
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
return $this->expect($value);
|
return $this->expect($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use Throwable;
|
|||||||
*/
|
*/
|
||||||
final class HigherOrderMessage
|
final class HigherOrderMessage
|
||||||
{
|
{
|
||||||
public const UNDEFINED_METHOD = 'Method %s does not exist';
|
public const string UNDEFINED_METHOD = 'Method %s does not exist';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An optional condition that will determine if the message will be executed.
|
* An optional condition that will determine if the message will be executed.
|
||||||
@ -50,14 +50,13 @@ final class HigherOrderMessage
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->hasHigherOrderCallable()) {
|
if ($this->hasHigherOrderCallable()) {
|
||||||
/* @phpstan-ignore-next-line */
|
|
||||||
return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments);
|
return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return is_array($this->arguments)
|
return is_array($this->arguments)
|
||||||
? Reflection::call($target, $this->name, $this->arguments)
|
? Reflection::call($target, $this->name, $this->arguments)
|
||||||
: $target->{$this->name}; /* @phpstan-ignore-line */
|
: $target->{$this->name};
|
||||||
} catch (Throwable $throwable) {
|
} catch (Throwable $throwable) {
|
||||||
Reflection::setPropertyValue($throwable, 'file', $this->filename);
|
Reflection::setPropertyValue($throwable, 'file', $this->filename);
|
||||||
Reflection::setPropertyValue($throwable, 'line', $this->line);
|
Reflection::setPropertyValue($throwable, 'line', $this->line);
|
||||||
@ -65,7 +64,6 @@ final class HigherOrderMessage
|
|||||||
if ($throwable->getMessage() === $this->getUndefinedMethodMessage($target, $this->name)) {
|
if ($throwable->getMessage() === $this->getUndefinedMethodMessage($target, $this->name)) {
|
||||||
/** @var ReflectionClass<TValue> $reflection */
|
/** @var ReflectionClass<TValue> $reflection */
|
||||||
$reflection = new ReflectionClass($target);
|
$reflection = new ReflectionClass($target);
|
||||||
/* @phpstan-ignore-next-line */
|
|
||||||
$reflection = $reflection->getParentClass() ?: $reflection;
|
$reflection = $reflection->getParentClass() ?: $reflection;
|
||||||
Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->name));
|
Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->name));
|
||||||
}
|
}
|
||||||
@ -96,10 +94,6 @@ final class HigherOrderMessage
|
|||||||
|
|
||||||
private function getUndefinedMethodMessage(object $target, string $methodName): string
|
private function getUndefinedMethodMessage(object $target, string $methodName): string
|
||||||
{
|
{
|
||||||
if (\PHP_MAJOR_VERSION >= 8) {
|
return sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName));
|
||||||
return sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName));
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf(self::UNDEFINED_METHOD, $methodName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,18 +26,16 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically pass properties gets to the target.
|
* Dynamically pass properties gets to the target.
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function __get(string $property)
|
public function __get(string $property): mixed
|
||||||
{
|
{
|
||||||
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();
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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', [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,12 +13,9 @@ final class Str
|
|||||||
* Pool of alpha-numeric characters for generating (unsafe) random strings
|
* Pool of alpha-numeric characters for generating (unsafe) random strings
|
||||||
* from.
|
* from.
|
||||||
*/
|
*/
|
||||||
private const POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
private const string POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
|
||||||
/**
|
private const string PREFIX = '__pest_evaluable_';
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private const PREFIX = '__pest_evaluable_';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a (unsecure & non-cryptographically safe) random alpha-numeric
|
* Create a (unsecure & non-cryptographically safe) random alpha-numeric
|
||||||
@ -82,7 +79,7 @@ final class Str
|
|||||||
return $subject;
|
return $subject;
|
||||||
}
|
}
|
||||||
|
|
||||||
return substr($subject, 0, $pos);
|
return mb_substr($subject, 0, $pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,10 +100,14 @@ final class Str
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a describe block as `$describeDescription` → `$testDescription` format.
|
* Creates a describe block as `$describeDescription` → `$testDescription` format.
|
||||||
|
*
|
||||||
|
* @param array<int, Description> $describeDescriptions
|
||||||
*/
|
*/
|
||||||
public static function describe(string $describeDescription, string $testDescription): string
|
public static function describe(array $describeDescriptions, string $testDescription): string
|
||||||
{
|
{
|
||||||
return sprintf('`%s` → %s', $describeDescription, $testDescription);
|
$descriptionComponents = [...$describeDescriptions, $testDescription];
|
||||||
|
|
||||||
|
return sprintf(str_repeat('`%s` → ', count($describeDescriptions)).'%s', ...$descriptionComponents);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,4 +117,14 @@ final class Str
|
|||||||
{
|
{
|
||||||
return (bool) filter_var($value, FILTER_VALIDATE_URL);
|
return (bool) filter_var($value, FILTER_VALIDATE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given `$target` to a URL-friendly "slug".
|
||||||
|
*/
|
||||||
|
public static function slugify(string $target): string
|
||||||
|
{
|
||||||
|
$target = preg_replace('/[^a-zA-Z0-9]+/', '-', $target);
|
||||||
|
|
||||||
|
return strtolower(trim((string) $target, '-'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,6 +78,7 @@ final class TestSuite
|
|||||||
$this->afterAll = new AfterAllRepository;
|
$this->afterAll = new AfterAllRepository;
|
||||||
$this->rootPath = (string) realpath($rootPath);
|
$this->rootPath = (string) realpath($rootPath);
|
||||||
$this->snapshots = new SnapshotRepository(
|
$this->snapshots = new SnapshotRepository(
|
||||||
|
$this->rootPath,
|
||||||
implode(DIRECTORY_SEPARATOR, [$this->rootPath, $this->testPath]),
|
implode(DIRECTORY_SEPARATOR, [$this->rootPath, $this->testPath]),
|
||||||
implode(DIRECTORY_SEPARATOR, ['.pest', 'snapshots']),
|
implode(DIRECTORY_SEPARATOR, ['.pest', 'snapshots']),
|
||||||
);
|
);
|
||||||
@ -101,7 +102,7 @@ final class TestSuite
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (! self::$instance instanceof self) {
|
if (! self::$instance instanceof self) {
|
||||||
Panic::with(new InvalidPestCommand);
|
throw new InvalidPestCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::$instance;
|
return self::$instance;
|
||||||
@ -119,7 +120,7 @@ final class TestSuite
|
|||||||
assert($this->test instanceof TestCase);
|
assert($this->test instanceof TestCase);
|
||||||
|
|
||||||
$description = str_replace('__pest_evaluable_', '', $this->test->name());
|
$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);
|
return str_replace(' ', '_', $description.$datasetAsString);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// pest()->extend(Tests\TestCase::class)->in('Feature');
|
pest()->extend(Tests\TestCase::class)->in('Feature');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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>
|
||||||
|
|||||||
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>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user