mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 15:57:21 +01:00
Compare commits
372 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b014e4b18 | |||
| 034715e8b1 | |||
| 09eff785c4 | |||
| 22cc7805d7 | |||
| 669dc0da71 | |||
| 689da4ed4e | |||
| 2f15861b0d | |||
| 0d50d35b5e | |||
| ce61ced8e1 | |||
| 7227d24611 | |||
| 45f16484d5 | |||
| b16e8650da | |||
| c2f30e0148 | |||
| 47ce45de56 | |||
| 32881774d2 | |||
| ea72461f1b | |||
| 49f15521e0 | |||
| 95c5394b66 | |||
| 8de30cc8b7 | |||
| b17feef3f4 | |||
| d8e4a405ad | |||
| 04af21183a | |||
| 09edaa9c2d | |||
| fa41a67be9 | |||
| 1a8f7025fa | |||
| 6afd2ec9df | |||
| d772069db2 | |||
| bb1a0b5e79 | |||
| 3c333ebbb8 | |||
| 868ac1840f | |||
| f857b4889c | |||
| c6b81e6e12 | |||
| c78d33b69e | |||
| bfd351783e | |||
| 526af2a75e | |||
| bf9d011045 | |||
| aaee0e420b | |||
| 772448db80 | |||
| e22fb2e6c0 | |||
| 49aa44c470 | |||
| 1cae035887 | |||
| 15183c4145 | |||
| ae288d1123 | |||
| 2d80ff19ec | |||
| c82f77ea75 | |||
| 5050ae304f | |||
| 98e947e0cc | |||
| 68785986a0 | |||
| 8c078087ff | |||
| 65f74f620c | |||
| dd20323ca7 | |||
| a7ca7afe4e | |||
| baf764f286 | |||
| 3a907c886b | |||
| e6823679dd | |||
| a021b5b8c3 | |||
| e2d360b1b5 | |||
| 8920b850e1 | |||
| 509074b3fa | |||
| 6f9ea14c68 | |||
| 60dcfb36a8 | |||
| ca25d5b13f | |||
| 5cba63e2ba | |||
| dd45a5c655 | |||
| dde943b993 | |||
| bb8677549a | |||
| 5ae5ac9a54 | |||
| cc6f1b43f6 | |||
| 88197fe1d5 | |||
| f53f855e9c | |||
| df69b0b791 | |||
| ecdbe7a472 | |||
| 2c6c3119d2 | |||
| 9ceb0834ae | |||
| 86d2191cae | |||
| 748beb17d5 | |||
| 7ba235f61a | |||
| 700bd517f4 | |||
| cbcfa2c5e2 | |||
| 243e45a551 | |||
| 9b11857ae6 | |||
| bb29e97200 | |||
| 8fe2698c28 | |||
| 823c3d4b17 | |||
| 39c9b15bc0 | |||
| b13acb630d | |||
| 172d94c0ca | |||
| f72d6f2278 | |||
| 71811d6e3a | |||
| dfdbd357e9 | |||
| 4d9ed8768c | |||
| 6638d279e1 | |||
| b5cd0ffb65 | |||
| 7ef40760c2 | |||
| ce4495b093 | |||
| 868547114f | |||
| 9c07dd9990 | |||
| 09beb812d4 | |||
| 4e98dfe3c3 | |||
| ad6dca94fa | |||
| 86f46c2efd | |||
| ccfd4fd77a | |||
| e4d2dac354 | |||
| 7e4c51e13d | |||
| aacd874ebe | |||
| 1c236aab26 | |||
| b6bf01148f | |||
| 347bcfd8a8 | |||
| 0ced3171b0 | |||
| 38638e865f | |||
| adbc6b4a89 | |||
| 9353015691 | |||
| 17058d1709 | |||
| 8ffa66dc7c | |||
| 0697555dc2 | |||
| af680ca8aa | |||
| 651aab560c | |||
| 41e50cac05 | |||
| 6fb1133d52 | |||
| 63ba117b33 | |||
| 33d36d77cb | |||
| 4e7db91ee8 | |||
| d0ff2c8ec2 | |||
| 5e41e546a0 | |||
| 45cce6ce93 | |||
| 6a8a4f3243 | |||
| 101e26749a | |||
| b3c8c24aea | |||
| ef29b4f091 | |||
| a7553b7593 | |||
| f2691623cf | |||
| 99107544ff | |||
| 2e411893d2 | |||
| 135c8a0d46 | |||
| 1cdd7d6744 | |||
| fca0c3a10c | |||
| 0331a87be1 | |||
| ef120125e0 | |||
| 8a9a416133 | |||
| 4783334f15 | |||
| d3be6b72dd | |||
| 7d3118db65 | |||
| eac7abebcb | |||
| 6896dd486a | |||
| 1e5b399603 | |||
| ccdf43726d | |||
| 67dbce2d42 | |||
| ee32f25485 | |||
| 09ca7a1fd5 | |||
| dade84e6b6 | |||
| 1c4bc8b1dc | |||
| 0d2f3eb60e | |||
| 29787d1ff1 | |||
| 474b9b7e17 | |||
| 5c3bf469d5 | |||
| d9252e85d6 | |||
| 0289466ce8 | |||
| 57ef989df8 | |||
| 9d02b649e2 | |||
| 00643312b7 | |||
| eac6585a2e | |||
| 04c39bae2e | |||
| c65755725d | |||
| ec58040f6e | |||
| 3fa73e40cc | |||
| c07513c6a0 | |||
| 85d91d5652 | |||
| 02bae3b649 | |||
| 3ba2b68afc | |||
| ed3ec79aab | |||
| 894dca83f7 | |||
| b873b89b62 | |||
| 1bee283d15 | |||
| 7b4dd410f6 | |||
| 4396ee2e03 | |||
| e4550c8d51 | |||
| a25cfb435c | |||
| fe4fe12df1 | |||
| 3bcc99a372 | |||
| e8f122bf47 | |||
| 9fc607a2b8 | |||
| 3ad788dddb | |||
| 2108d18be5 | |||
| aa4a5fcd15 | |||
| 1688888f15 | |||
| 40539ca720 | |||
| 7144d6dfbd | |||
| 7240250a15 | |||
| 508e42a2ff | |||
| d8156fee53 | |||
| abc245bf85 | |||
| 65dacd5647 | |||
| 917f7a64a0 | |||
| e8b09d6f8c | |||
| 0c4e6de823 | |||
| 52282cc590 | |||
| a46142d8c7 | |||
| 241dcf8f34 | |||
| 927cee609e | |||
| 98e4ebb8fd | |||
| c173e3e86b | |||
| c73655f4f9 | |||
| 4ac1c6efc6 | |||
| 2e5a308b0d | |||
| 7b8e4aec08 | |||
| 13fb66f15c | |||
| dd1bd92910 | |||
| d665b53b22 | |||
| c54b7e400e | |||
| c1e1fff0d0 | |||
| 2e4a8329a6 | |||
| 878988a02d | |||
| ceb7244b43 | |||
| 84256aa8b9 | |||
| d6b59e4e96 | |||
| 087d09120a | |||
| cc41a7f81d | |||
| bd16769b93 | |||
| 60b1e63c23 | |||
| c7bcb6eb7b | |||
| d25ec50384 | |||
| 9e27813897 | |||
| b33af71036 | |||
| 3c6c89a6ad | |||
| 55f6b5696e | |||
| ba914fa2fb | |||
| c919bb5bc4 | |||
| 8169382362 | |||
| 04b099e87c | |||
| fecdb7f572 | |||
| b611d0d444 | |||
| ac7199c96d | |||
| 7756457dc4 | |||
| 10da81eee4 | |||
| 8bbee3c1e5 | |||
| 16125df77b | |||
| 80530cb1e0 | |||
| 2070538fd3 | |||
| a2cb78710d | |||
| 335bfdb79d | |||
| cfa00da885 | |||
| f49d1e0e18 | |||
| 303f4c0113 | |||
| 35a1fcd0cf | |||
| adb2fb51df | |||
| a7a3e4240e | |||
| e4af33867b | |||
| 0c51b159a7 | |||
| c6984323c3 | |||
| 831d9bf49a | |||
| 12f6aa604c | |||
| 265f0c7da9 | |||
| 680111fb1e | |||
| aa6ff95ea4 | |||
| 863a0cc837 | |||
| 126a84a63e | |||
| 0ccbe5c8f0 | |||
| a4f8ae1a12 | |||
| 6094682158 | |||
| d519e40b95 | |||
| 6a1161ead8 | |||
| a1b3547dd6 | |||
| b9e3146a47 | |||
| ce1607cba9 | |||
| ac07bc1770 | |||
| 521a41dd10 | |||
| 1b68b340e8 | |||
| 853f6efce6 | |||
| 62a9a78ee2 | |||
| 78d9fd31d0 | |||
| 2c3234fb3d | |||
| 1b64fef7ba | |||
| a136231503 | |||
| 602b696348 | |||
| 5b0f88c227 | |||
| f31a2c3220 | |||
| e8fa98c810 | |||
| 07e314fbf5 | |||
| 4baf27911e | |||
| 12e48a14d1 | |||
| 1bc0f79508 | |||
| cb0f256791 | |||
| 7b9bae0415 | |||
| 3dffdf7cb8 | |||
| 923970a117 | |||
| b3db7dfd4c | |||
| b303f9f818 | |||
| d29997d5b0 | |||
| 13f340a742 | |||
| eeade88ad2 | |||
| 06280ef75d | |||
| aa46f73888 | |||
| 3660865e5e | |||
| 13695d597b | |||
| fab2de833f | |||
| 5b630bcdff | |||
| e70edbfa38 | |||
| b1558ddde5 | |||
| 582529377b | |||
| 88714598b6 | |||
| 5136267bbe | |||
| 19e748f0d4 | |||
| a53a9d03cf | |||
| edaa045283 | |||
| c5ce355f3c | |||
| 62d8459627 | |||
| a5bf6a3fcb | |||
| 7a46514df8 | |||
| cb1735f4d8 | |||
| 607a4906ac | |||
| 317ea0356e | |||
| 1153531104 | |||
| cfb724cd77 | |||
| 0060b6f955 | |||
| 95cd550524 | |||
| 815ae3c644 | |||
| 887bed3d45 | |||
| 79da02c500 | |||
| 0aecd5d5d7 | |||
| e95c4ee636 | |||
| 2e7fec6be5 | |||
| 4be7082de5 | |||
| fb90f778b9 | |||
| 9d58e1a77e | |||
| 9c077ed352 | |||
| 2562d36518 | |||
| 1d2fe2de2d | |||
| 2d82ee2837 | |||
| 1eee9df679 | |||
| 8c57cc1731 | |||
| 4febd8a11b | |||
| 880b003bee | |||
| e0f9d0bccf | |||
| d4853feecd | |||
| 86e812284d | |||
| 4e31973040 | |||
| f75063c420 | |||
| 6a48e9d44b | |||
| 1f8e6e4e9f | |||
| bb593846e5 | |||
| 108d181a05 | |||
| be407ac904 | |||
| 9ce52ee7ce | |||
| 3ff41bcb68 | |||
| 2b094b4188 | |||
| dd4d5bbd4e | |||
| ab64912c70 | |||
| 1506d8bb27 | |||
| 5aa13b8e97 | |||
| b143ed7aac | |||
| 26dd5f298f | |||
| d939ee938e | |||
| 515de3972f | |||
| bf573b3cac | |||
| 53dc9ffa06 | |||
| 04d2fa5ce8 | |||
| 7764a7a162 | |||
| 727a427837 | |||
| b1c59ec2e6 | |||
| f69a3cf832 | |||
| ed0bf1786f | |||
| 2d1d8a81e1 | |||
| d515cf965e | |||
| 2e01776272 | |||
| 15cd7187e9 | |||
| 86c107ae5e | |||
| 67e452e9ed | |||
| ecff90da1c | |||
| 3ee5c29a00 | |||
| 79f5973e5a | |||
| 37c40cb735 | |||
| 28ee2917f1 |
6
.github/workflows/static.yml
vendored
6
.github/workflows/static.yml
vendored
@ -24,15 +24,15 @@ jobs:
|
|||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: 8.1
|
php-version: 8.2
|
||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: composer update --prefer-stable --no-interaction --no-progress --ansi
|
run: composer update --prefer-stable --no-interaction --no-progress --ansi
|
||||||
|
|
||||||
- name: Type Check
|
# - name: Type Check
|
||||||
run: composer test:type:check
|
# run: composer test:type:check
|
||||||
|
|
||||||
- name: Type Coverage
|
- name: Type Coverage
|
||||||
run: composer test:type:coverage
|
run: composer test:type:coverage
|
||||||
|
|||||||
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@ -3,8 +3,6 @@ name: Tests
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
@ -15,12 +13,9 @@ jobs:
|
|||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
symfony: ['6.4', '7.0']
|
symfony: ['7.0']
|
||||||
php: ['8.1', '8.2', '8.3']
|
php: ['8.2', '8.3']
|
||||||
dependency_version: [prefer-lowest, prefer-stable]
|
dependency_version: [prefer-lowest, prefer-stable]
|
||||||
exclude:
|
|
||||||
- php: '8.1'
|
|
||||||
symfony: '7.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 }}
|
||||||
|
|
||||||
|
|||||||
@ -69,7 +69,7 @@ If you want to check things work against a specific version of PHP, you may incl
|
|||||||
the `PHP` build argument when building the image:
|
the `PHP` build argument when building the image:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make build ARGS="--build-arg PHP=8.2"
|
make build ARGS="--build-arg PHP=8.3"
|
||||||
```
|
```
|
||||||
|
|
||||||
The default PHP version will always be the lowest version of PHP supported by Pest.
|
The default PHP version will always be the lowest version of PHP supported by Pest.
|
||||||
|
|||||||
11
README.md
11
README.md
@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://raw.githubusercontent.com/pestphp/art/master/v2/banner.png" width="600" alt="PEST">
|
<img src="https://raw.githubusercontent.com/pestphp/art/master/v3/banner.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=2.x&label=Tests%202.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>
|
||||||
@ -9,6 +9,9 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
|
> Pest v3 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest3-now-available)**.
|
||||||
|
|
||||||
**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)**
|
||||||
@ -21,6 +24,8 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
|
|||||||
|
|
||||||
### Platinum Sponsors
|
### Platinum Sponsors
|
||||||
|
|
||||||
|
- **[LaraJobs](https://larajobs.com)**
|
||||||
|
- **[Brokerchooser](https://brokerchooser.com)**
|
||||||
- **[Forge](https://forge.laravel.com)**
|
- **[Forge](https://forge.laravel.com)**
|
||||||
- **[Spatie](https://spatie.be)**
|
- **[Spatie](https://spatie.be)**
|
||||||
- **[Worksome](https://www.worksome.com/)**
|
- **[Worksome](https://www.worksome.com/)**
|
||||||
@ -29,10 +34,10 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
|
|||||||
|
|
||||||
- [Akaunting](https://akaunting.com/?ref=pestphp)
|
- [Akaunting](https://akaunting.com/?ref=pestphp)
|
||||||
- [Codecourse](https://codecourse.com/?ref=pestphp)
|
- [Codecourse](https://codecourse.com/?ref=pestphp)
|
||||||
|
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
|
||||||
- [Laracasts](https://laracasts.com/?ref=pestphp)
|
- [Laracasts](https://laracasts.com/?ref=pestphp)
|
||||||
- [Laradir](https://laradir.com/?ref=pestphp)
|
|
||||||
- [Localazy](https://localazy.com/?ref=pestphp)
|
- [Localazy](https://localazy.com/?ref=pestphp)
|
||||||
- [Stormlikes](https://www.stormlikes.net/?ref=pestphp)
|
- [Route4Me](https://www.route4me.com/?ref=pestphp)
|
||||||
- [Zapiet](https://www.zapiet.com/?ref=pestphp)
|
- [Zapiet](https://www.zapiet.com/?ref=pestphp)
|
||||||
|
|
||||||
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
|
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
|
||||||
|
|||||||
@ -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 v1 you should use the `1.x` branch instead.**
|
> **For Pest v2 you should use the `2.x` branch instead.**
|
||||||
|
|
||||||
- Clear your local repository with: `git add . && git reset --hard && git checkout 2.x`
|
- Clear your local repository with: `git add . && git reset --hard && git checkout 3.x`
|
||||||
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...2.x](https://github.com/pestphp/pest/compare/{latest_version}...2.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)
|
||||||
- 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"`
|
||||||
|
|||||||
116
bin/pest
116
bin/pest
@ -4,6 +4,10 @@
|
|||||||
use Pest\Kernel;
|
use Pest\Kernel;
|
||||||
use Pest\Panic;
|
use Pest\Panic;
|
||||||
use Pest\TestCaseFilters\GitDirtyTestCaseFilter;
|
use Pest\TestCaseFilters\GitDirtyTestCaseFilter;
|
||||||
|
use Pest\TestCaseMethodFilters\AssigneeTestCaseFilter;
|
||||||
|
use Pest\TestCaseMethodFilters\IssueTestCaseFilter;
|
||||||
|
use Pest\TestCaseMethodFilters\NotesTestCaseFilter;
|
||||||
|
use Pest\TestCaseMethodFilters\PrTestCaseFilter;
|
||||||
use Pest\TestCaseMethodFilters\TodoTestCaseFilter;
|
use Pest\TestCaseMethodFilters\TodoTestCaseFilter;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use Symfony\Component\Console\Input\ArgvInput;
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
@ -13,43 +17,107 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
// Ensures Collision's Printer is registered.
|
// Ensures Collision's Printer is registered.
|
||||||
$_SERVER['COLLISION_PRINTER'] = 'DefaultPrinter';
|
$_SERVER['COLLISION_PRINTER'] = 'DefaultPrinter';
|
||||||
|
|
||||||
$args = $_SERVER['argv'];
|
$arguments = $originalArguments = $_SERVER['argv'];
|
||||||
|
|
||||||
$dirty = false;
|
$dirty = false;
|
||||||
$todo = false;
|
$todo = false;
|
||||||
|
$notes = false;
|
||||||
|
|
||||||
|
foreach ($arguments as $key => $value) {
|
||||||
|
|
||||||
foreach ($args as $key => $value) {
|
|
||||||
if ($value === '--compact') {
|
if ($value === '--compact') {
|
||||||
$_SERVER['COLLISION_PRINTER_COMPACT'] = 'true';
|
$_SERVER['COLLISION_PRINTER_COMPACT'] = 'true';
|
||||||
unset($args[$key]);
|
unset($arguments[$key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($value === '--profile') {
|
if ($value === '--profile') {
|
||||||
$_SERVER['COLLISION_PRINTER_PROFILE'] = 'true';
|
$_SERVER['COLLISION_PRINTER_PROFILE'] = 'true';
|
||||||
unset($args[$key]);
|
unset($arguments[$key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str_contains($value, '--test-directory')) {
|
if (str_contains($value, '--test-directory=')) {
|
||||||
unset($args[$key]);
|
unset($arguments[$key]);
|
||||||
|
} else if ($value === '--test-directory') {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
|
||||||
|
if (isset($arguments[$key + 1])) {
|
||||||
|
unset($arguments[$key + 1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($value === '--dirty') {
|
if ($value === '--dirty') {
|
||||||
$dirty = true;
|
$dirty = true;
|
||||||
unset($args[$key]);
|
unset($arguments[$key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($value, ['--todo', '--todos'], true)) {
|
if (in_array($value, ['--todo', '--todos'], true)) {
|
||||||
$todo = true;
|
$todo = true;
|
||||||
unset($args[$key]);
|
unset($arguments[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === '--notes') {
|
||||||
|
$notes = true;
|
||||||
|
unset($arguments[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--assignee=')) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
} else if ($value === '--assignee') {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
|
||||||
|
if (isset($arguments[$key + 1])) {
|
||||||
|
unset($arguments[$key + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--issue=')) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
} else if ($value === '--issue') {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
|
||||||
|
if (isset($arguments[$key + 1])) {
|
||||||
|
unset($arguments[$key + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--ticket=')) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
} else if ($value === '--ticket') {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
|
||||||
|
if (isset($arguments[$key + 1])) {
|
||||||
|
unset($arguments[$key + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--pr=')) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
} else if ($value === '--pr') {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
|
||||||
|
if (isset($arguments[$key + 1])) {
|
||||||
|
unset($arguments[$key + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--pull-request=')) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
} else if ($value === '--pull-request') {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
|
||||||
|
if (isset($arguments[$key + 1])) {
|
||||||
|
unset($arguments[$key + 1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str_contains($value, '--teamcity')) {
|
if (str_contains($value, '--teamcity')) {
|
||||||
unset($args[$key]);
|
unset($arguments[$key]);
|
||||||
$args[] = '--no-output';
|
$arguments[] = '--no-output';
|
||||||
unset($_SERVER['COLLISION_PRINTER']);
|
unset($_SERVER['COLLISION_PRINTER']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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';
|
||||||
|
|
||||||
@ -81,6 +149,30 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter());
|
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($notes) {
|
||||||
|
$testSuite->tests->addTestCaseMethodFilter(new NotesTestCaseFilter());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($assignee = $input->getParameterOption('--assignee')) {
|
||||||
|
$testSuite->tests->addTestCaseMethodFilter(new AssigneeTestCaseFilter((string) $assignee));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($issue = $input->getParameterOption('--issue')) {
|
||||||
|
$testSuite->tests->addTestCaseMethodFilter(new IssueTestCaseFilter((int) $issue));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($issue = $input->getParameterOption('--ticket')) {
|
||||||
|
$testSuite->tests->addTestCaseMethodFilter(new IssueTestCaseFilter((int) $issue));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pr = $input->getParameterOption('--pr')) {
|
||||||
|
$testSuite->tests->addTestCaseMethodFilter(new PrTestCaseFilter((int) $pr));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pr = $input->getParameterOption('--pull-request')) {
|
||||||
|
$testSuite->tests->addTestCaseMethodFilter(new PrTestCaseFilter((int) $pr));
|
||||||
|
}
|
||||||
|
|
||||||
$isDecorated = $input->getParameterOption('--colors', 'always') !== 'never';
|
$isDecorated = $input->getParameterOption('--colors', 'always') !== 'never';
|
||||||
|
|
||||||
$output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated);
|
$output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated);
|
||||||
@ -88,9 +180,9 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
try {
|
try {
|
||||||
$kernel = Kernel::boot($testSuite, $input, $output);
|
$kernel = Kernel::boot($testSuite, $input, $output);
|
||||||
|
|
||||||
$result = $kernel->handle($args);
|
$result = $kernel->handle($originalArguments, $arguments);
|
||||||
|
|
||||||
$kernel->shutdown();
|
$kernel->terminate();
|
||||||
} catch (Throwable|Error $e) {
|
} catch (Throwable|Error $e) {
|
||||||
Panic::with($e);
|
Panic::with($e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
$bootPest = (static function (): void {
|
$bootPest = (static function (): void {
|
||||||
$workerArgv = new ArgvInput();
|
$workerArgv = new ArgvInput;
|
||||||
|
|
||||||
$rootPath = dirname(PHPUNIT_COMPOSER_INSTALL, 2);
|
$rootPath = dirname(PHPUNIT_COMPOSER_INSTALL, 2);
|
||||||
$testSuite = TestSuite::getInstance($rootPath, $workerArgv->getParameterOption(
|
$testSuite = TestSuite::getInstance($rootPath, $workerArgv->getParameterOption(
|
||||||
@ -20,7 +20,7 @@ $bootPest = (static function (): void {
|
|||||||
'tests'
|
'tests'
|
||||||
));
|
));
|
||||||
|
|
||||||
$input = new ArgvInput();
|
$input = new ArgvInput;
|
||||||
|
|
||||||
$output = new ConsoleOutput(OutputInterface::VERBOSITY_NORMAL, true);
|
$output = new ConsoleOutput(OutputInterface::VERBOSITY_NORMAL, true);
|
||||||
|
|
||||||
@ -81,6 +81,7 @@ $bootPest = (static function (): void {
|
|||||||
$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,
|
||||||
);
|
);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@ -17,17 +17,18 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1.0",
|
"php": "^8.2.0",
|
||||||
"brianium/paratest": "^7.3.1",
|
"brianium/paratest": "^7.5.4",
|
||||||
"nunomaduro/collision": "^7.10.0|^8.1.0",
|
"nunomaduro/collision": "^8.4.0",
|
||||||
"nunomaduro/termwind": "^1.15.1|^2.0.0",
|
"nunomaduro/termwind": "^2.1.0",
|
||||||
"pestphp/pest-plugin": "^2.1.1",
|
"pestphp/pest-plugin": "^3.0.0",
|
||||||
"pestphp/pest-plugin-arch": "^2.6.1",
|
"pestphp/pest-plugin-arch": "^3.0.0",
|
||||||
"phpunit/phpunit": "^10.5.7"
|
"pestphp/pest-plugin-mutate": "^3.0.3",
|
||||||
|
"phpunit/phpunit": "^11.3.6"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"phpunit/phpunit": ">10.5.7",
|
"phpunit/phpunit": ">11.3.6",
|
||||||
"sebastian/exporter": "<5.1.0",
|
"sebastian/exporter": "<6.0.0",
|
||||||
"webmozart/assert": "<1.11.0"
|
"webmozart/assert": "<1.11.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@ -51,9 +52,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"pestphp/pest-dev-tools": "^2.16.0",
|
"pestphp/pest-dev-tools": "^3.0.0",
|
||||||
"pestphp/pest-plugin-type-coverage": "^2.8.0",
|
"pestphp/pest-plugin-type-coverage": "^3.0.0",
|
||||||
"symfony/process": "^6.4.0|^7.0.2"
|
"symfony/process": "^7.1.3"
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
@ -73,11 +74,11 @@
|
|||||||
"test:refacto": "rector --dry-run",
|
"test:refacto": "rector --dry-run",
|
||||||
"test:lint": "pint --test",
|
"test:lint": "pint --test",
|
||||||
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
|
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
|
||||||
"test:type:coverage": "php 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 --colors=always --exclude-group=integration --compact",
|
||||||
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
|
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
|
||||||
"test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3",
|
"test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3",
|
||||||
"test:integration": "php bin/pest --colors=always --group=integration",
|
"test:integration": "php bin/pest --colors=always --group=integration -v",
|
||||||
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
|
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
|
||||||
"test": [
|
"test": [
|
||||||
"@test:refacto",
|
"@test:refacto",
|
||||||
@ -92,6 +93,8 @@
|
|||||||
"extra": {
|
"extra": {
|
||||||
"pest": {
|
"pest": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
"Pest\\Mutate\\Plugins\\Mutate",
|
||||||
|
"Pest\\Plugins\\Configuration",
|
||||||
"Pest\\Plugins\\Bail",
|
"Pest\\Plugins\\Bail",
|
||||||
"Pest\\Plugins\\Cache",
|
"Pest\\Plugins\\Cache",
|
||||||
"Pest\\Plugins\\Coverage",
|
"Pest\\Plugins\\Coverage",
|
||||||
@ -107,8 +110,7 @@
|
|||||||
"Pest\\Plugins\\Snapshot",
|
"Pest\\Plugins\\Snapshot",
|
||||||
"Pest\\Plugins\\Verbose",
|
"Pest\\Plugins\\Verbose",
|
||||||
"Pest\\Plugins\\Version",
|
"Pest\\Plugins\\Version",
|
||||||
"Pest\\Plugins\\Parallel",
|
"Pest\\Plugins\\Parallel"
|
||||||
"Pest\\Plugins\\JUnit"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"phpstan": {
|
"phpstan": {
|
||||||
|
|||||||
@ -2,13 +2,11 @@ ARG PHP=8.1
|
|||||||
FROM php:${PHP}-cli-alpine
|
FROM php:${PHP}-cli-alpine
|
||||||
|
|
||||||
RUN apk update && apk add \
|
RUN apk update && apk add \
|
||||||
zip libzip-dev icu-dev git \
|
zip libzip-dev icu-dev git
|
||||||
|
|
||||||
RUN docker-php-ext-configure zip intl
|
|
||||||
RUN docker-php-ext-install zip intl
|
RUN docker-php-ext-install zip intl
|
||||||
RUN docker-php-ext-enable zip intl
|
|
||||||
|
|
||||||
RUN apk add --no-cache linux-headers
|
RUN apk add --no-cache linux-headers autoconf build-base
|
||||||
RUN pecl install xdebug
|
RUN pecl install xdebug
|
||||||
RUN docker-php-ext-enable xdebug
|
RUN docker-php-ext-enable xdebug
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|||||||
@ -1,5 +1,37 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* BSD 3-Clause License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2001-2023, Sebastian Bergmann
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. Neither the name of the copyright holder nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -22,7 +54,7 @@ use PHPUnit\Util\ThrowableToStringMapper;
|
|||||||
/**
|
/**
|
||||||
* @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 ThrowableBuilder
|
final readonly class ThrowableBuilder
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
|
|||||||
@ -1,16 +1,458 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
/*
|
||||||
|
* This file is part of PHPUnit.
|
||||||
|
*
|
||||||
|
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
namespace PHPUnit\Logging\JUnit;
|
namespace PHPUnit\Logging\JUnit;
|
||||||
|
|
||||||
|
use DOMDocument;
|
||||||
|
use DOMElement;
|
||||||
|
use PHPUnit\Event\Code\Test;
|
||||||
|
use PHPUnit\Event\Code\TestMethod;
|
||||||
|
use PHPUnit\Event\EventFacadeIsSealedException;
|
||||||
use PHPUnit\Event\Facade;
|
use PHPUnit\Event\Facade;
|
||||||
|
use PHPUnit\Event\InvalidArgumentException;
|
||||||
|
use PHPUnit\Event\Telemetry\HRTime;
|
||||||
|
use PHPUnit\Event\Telemetry\Info;
|
||||||
|
use PHPUnit\Event\Test\Errored;
|
||||||
|
use PHPUnit\Event\Test\Failed;
|
||||||
|
use PHPUnit\Event\Test\Finished;
|
||||||
|
use PHPUnit\Event\Test\MarkedIncomplete;
|
||||||
|
use PHPUnit\Event\Test\PreparationStarted;
|
||||||
|
use PHPUnit\Event\Test\Prepared;
|
||||||
|
use PHPUnit\Event\Test\Skipped;
|
||||||
|
use PHPUnit\Event\TestSuite\Started;
|
||||||
|
use PHPUnit\Event\UnknownSubscriberTypeException;
|
||||||
use PHPUnit\TextUI\Output\Printer;
|
use PHPUnit\TextUI\Output\Printer;
|
||||||
|
use PHPUnit\Util\Xml;
|
||||||
|
|
||||||
|
use function assert;
|
||||||
|
use function basename;
|
||||||
|
use function is_int;
|
||||||
|
use function sprintf;
|
||||||
|
use function str_replace;
|
||||||
|
use function trim;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*/
|
||||||
final class JunitXmlLogger
|
final class JunitXmlLogger
|
||||||
{
|
{
|
||||||
|
private readonly Printer $printer;
|
||||||
|
|
||||||
|
private readonly \Pest\Logging\Converter $converter; // pest-added
|
||||||
|
|
||||||
|
private DOMDocument $document;
|
||||||
|
|
||||||
|
private DOMElement $root;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DOMElement[]
|
||||||
|
*/
|
||||||
|
private array $testSuites = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-var array<int,int>
|
||||||
|
*/
|
||||||
|
private array $testSuiteTests = [0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-var array<int,int>
|
||||||
|
*/
|
||||||
|
private array $testSuiteAssertions = [0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-var array<int,int>
|
||||||
|
*/
|
||||||
|
private array $testSuiteErrors = [0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-var array<int,int>
|
||||||
|
*/
|
||||||
|
private array $testSuiteFailures = [0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-var array<int,int>
|
||||||
|
*/
|
||||||
|
private array $testSuiteSkipped = [0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-var array<int,int>
|
||||||
|
*/
|
||||||
|
private array $testSuiteTimes = [0];
|
||||||
|
|
||||||
|
private int $testSuiteLevel = 0;
|
||||||
|
|
||||||
|
private ?DOMElement $currentTestCase = null;
|
||||||
|
|
||||||
|
private ?HRTime $time = null;
|
||||||
|
|
||||||
|
private bool $prepared = false;
|
||||||
|
|
||||||
|
private bool $preparationFailed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws EventFacadeIsSealedException
|
||||||
|
* @throws UnknownSubscriberTypeException
|
||||||
|
*/
|
||||||
public function __construct(Printer $printer, Facade $facade)
|
public function __construct(Printer $printer, Facade $facade)
|
||||||
{
|
{
|
||||||
/** @see \Pest\Logging\JUnit\JUnitLogger */
|
$this->printer = $printer;
|
||||||
|
$this->converter = new \Pest\Logging\Converter(\Pest\Support\Container::getInstance()->get(\Pest\TestSuite::class)->rootPath); // pest-added
|
||||||
|
|
||||||
|
$this->registerSubscribers($facade);
|
||||||
|
$this->createDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
$this->printer->print($this->document->saveXML());
|
||||||
|
|
||||||
|
$this->printer->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSuiteStarted(Started $event): void
|
||||||
|
{
|
||||||
|
$testSuite = $this->document->createElement('testsuite');
|
||||||
|
$testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite())); // pest-changed
|
||||||
|
|
||||||
|
if ($event->testSuite()->isForTestClass()) {
|
||||||
|
$testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? ''); // pest-changed
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->testSuiteLevel > 0) {
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
|
||||||
|
} else {
|
||||||
|
$this->root->appendChild($testSuite);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->testSuiteLevel++;
|
||||||
|
$this->testSuites[$this->testSuiteLevel] = $testSuite;
|
||||||
|
$this->testSuiteTests[$this->testSuiteLevel] = 0;
|
||||||
|
$this->testSuiteAssertions[$this->testSuiteLevel] = 0;
|
||||||
|
$this->testSuiteErrors[$this->testSuiteLevel] = 0;
|
||||||
|
$this->testSuiteFailures[$this->testSuiteLevel] = 0;
|
||||||
|
$this->testSuiteSkipped[$this->testSuiteLevel] = 0;
|
||||||
|
$this->testSuiteTimes[$this->testSuiteLevel] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSuiteFinished(): void
|
||||||
|
{
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'tests',
|
||||||
|
(string) $this->testSuiteTests[$this->testSuiteLevel],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'assertions',
|
||||||
|
(string) $this->testSuiteAssertions[$this->testSuiteLevel],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'errors',
|
||||||
|
(string) $this->testSuiteErrors[$this->testSuiteLevel],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'failures',
|
||||||
|
(string) $this->testSuiteFailures[$this->testSuiteLevel],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'skipped',
|
||||||
|
(string) $this->testSuiteSkipped[$this->testSuiteLevel],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'time',
|
||||||
|
sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->testSuiteLevel > 1) {
|
||||||
|
$this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel];
|
||||||
|
$this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel];
|
||||||
|
$this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel];
|
||||||
|
$this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel];
|
||||||
|
$this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel];
|
||||||
|
$this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->testSuiteLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testPreparationStarted(PreparationStarted $event): void
|
||||||
|
{
|
||||||
|
$this->createTestCase($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testPreparationFailed(): void
|
||||||
|
{
|
||||||
|
$this->preparationFailed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testPrepared(): void
|
||||||
|
{
|
||||||
|
$this->prepared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testFinished(Finished $event): void
|
||||||
|
{
|
||||||
|
if ($this->preparationFailed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->handleFinish($event->telemetryInfo(), $event->numberOfAssertionsPerformed());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testMarkedIncomplete(MarkedIncomplete $event): void
|
||||||
|
{
|
||||||
|
$this->handleIncompleteOrSkipped($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testSkipped(Skipped $event): void
|
||||||
|
{
|
||||||
|
$this->handleIncompleteOrSkipped($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testErrored(Errored $event): void
|
||||||
|
{
|
||||||
|
$this->handleFault($event, 'error');
|
||||||
|
|
||||||
|
$this->testSuiteErrors[$this->testSuiteLevel]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testFailed(Failed $event): void
|
||||||
|
{
|
||||||
|
$this->handleFault($event, 'failure');
|
||||||
|
|
||||||
|
$this->testSuiteFailures[$this->testSuiteLevel]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private function handleFinish(Info $telemetryInfo, int $numberOfAssertionsPerformed): void
|
||||||
|
{
|
||||||
|
assert($this->currentTestCase !== null);
|
||||||
|
assert($this->time !== null);
|
||||||
|
|
||||||
|
$time = $telemetryInfo->time()->duration($this->time)->asFloat();
|
||||||
|
|
||||||
|
$this->testSuiteAssertions[$this->testSuiteLevel] += $numberOfAssertionsPerformed;
|
||||||
|
|
||||||
|
$this->currentTestCase->setAttribute(
|
||||||
|
'assertions',
|
||||||
|
(string) $numberOfAssertionsPerformed,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->currentTestCase->setAttribute(
|
||||||
|
'time',
|
||||||
|
sprintf('%F', $time),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->appendChild(
|
||||||
|
$this->currentTestCase,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuiteTests[$this->testSuiteLevel]++;
|
||||||
|
$this->testSuiteTimes[$this->testSuiteLevel] += $time;
|
||||||
|
|
||||||
|
$this->currentTestCase = null;
|
||||||
|
$this->time = null;
|
||||||
|
$this->prepared = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws EventFacadeIsSealedException
|
||||||
|
* @throws UnknownSubscriberTypeException
|
||||||
|
*/
|
||||||
|
private function registerSubscribers(Facade $facade): void
|
||||||
|
{
|
||||||
|
$facade->registerSubscribers(
|
||||||
|
new TestSuiteStartedSubscriber($this),
|
||||||
|
new TestSuiteFinishedSubscriber($this),
|
||||||
|
new TestPreparationStartedSubscriber($this),
|
||||||
|
new TestPreparationFailedSubscriber($this),
|
||||||
|
new TestPreparedSubscriber($this),
|
||||||
|
new TestFinishedSubscriber($this),
|
||||||
|
new TestErroredSubscriber($this),
|
||||||
|
new TestFailedSubscriber($this),
|
||||||
|
new TestSkippedSubscriber($this),
|
||||||
|
new TestRunnerExecutionFinishedSubscriber($this),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createDocument(): void
|
||||||
|
{
|
||||||
|
$this->document = new DOMDocument('1.0', 'UTF-8');
|
||||||
|
$this->document->formatOutput = true;
|
||||||
|
|
||||||
|
$this->root = $this->document->createElement('testsuites');
|
||||||
|
$this->document->appendChild($this->root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private function handleFault(Errored|Failed $event, string $type): void
|
||||||
|
{
|
||||||
|
if (! $this->prepared) {
|
||||||
|
$this->createTestCase($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert($this->currentTestCase !== null);
|
||||||
|
|
||||||
|
$buffer = $this->converter->getTestCaseMethodName($event->test()); // pest-changed
|
||||||
|
|
||||||
|
$throwable = $event->throwable();
|
||||||
|
$buffer .= trim(
|
||||||
|
$this->converter->getExceptionMessage($throwable).PHP_EOL. // pest-changed
|
||||||
|
$this->converter->getExceptionDetails($throwable), // pest-changed
|
||||||
|
);
|
||||||
|
|
||||||
|
$fault = $this->document->createElement(
|
||||||
|
$type,
|
||||||
|
Xml::prepareString($buffer),
|
||||||
|
);
|
||||||
|
|
||||||
|
$fault->setAttribute('type', $throwable->className());
|
||||||
|
|
||||||
|
$this->currentTestCase->appendChild($fault);
|
||||||
|
|
||||||
|
if (! $this->prepared) {
|
||||||
|
$this->handleFinish($event->telemetryInfo(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private function handleIncompleteOrSkipped(MarkedIncomplete|Skipped $event): void
|
||||||
|
{
|
||||||
|
if (! $this->prepared) {
|
||||||
|
$this->createTestCase($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert($this->currentTestCase !== null);
|
||||||
|
|
||||||
|
$skipped = $this->document->createElement('skipped');
|
||||||
|
|
||||||
|
$this->currentTestCase->appendChild($skipped);
|
||||||
|
|
||||||
|
$this->testSuiteSkipped[$this->testSuiteLevel]++;
|
||||||
|
|
||||||
|
if (! $this->prepared) {
|
||||||
|
$this->handleFinish($event->telemetryInfo(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private function testAsString(Test $test): string
|
||||||
|
{
|
||||||
|
if ($test->isPhpt()) {
|
||||||
|
return basename($test->file());
|
||||||
|
}
|
||||||
|
|
||||||
|
assert($test instanceof TestMethod);
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'%s::%s%s',
|
||||||
|
$test->className(),
|
||||||
|
$this->name($test),
|
||||||
|
PHP_EOL,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private function name(Test $test): string
|
||||||
|
{
|
||||||
|
if ($test->isPhpt()) {
|
||||||
|
return basename($test->file());
|
||||||
|
}
|
||||||
|
|
||||||
|
assert($test instanceof TestMethod);
|
||||||
|
|
||||||
|
if (! $test->testData()->hasDataFromDataProvider()) {
|
||||||
|
return $test->methodName();
|
||||||
|
}
|
||||||
|
|
||||||
|
$dataSetName = $test->testData()->dataFromDataProvider()->dataSetName();
|
||||||
|
|
||||||
|
if (is_int($dataSetName)) {
|
||||||
|
return sprintf(
|
||||||
|
'%s with data set #%d',
|
||||||
|
$test->methodName(),
|
||||||
|
$dataSetName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'%s with data set "%s"',
|
||||||
|
$test->methodName(),
|
||||||
|
$dataSetName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @psalm-assert !null $this->currentTestCase
|
||||||
|
*/
|
||||||
|
private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void
|
||||||
|
{
|
||||||
|
$testCase = $this->document->createElement('testcase');
|
||||||
|
|
||||||
|
$test = $event->test();
|
||||||
|
$file = $this->converter->getTestCaseLocation($test); // pest-added
|
||||||
|
|
||||||
|
$testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test)); // pest-changed
|
||||||
|
$testCase->setAttribute('file', $file); // pest-changed
|
||||||
|
|
||||||
|
if ($test->isTestMethod()) {
|
||||||
|
assert($test instanceof TestMethod);
|
||||||
|
|
||||||
|
//$testCase->setAttribute('line', (string) $test->line()); // pest-removed
|
||||||
|
$className = $this->converter->getTrimmedTestClassName($test); // pest-added
|
||||||
|
$testCase->setAttribute('class', $className); // pest-changed
|
||||||
|
$testCase->setAttribute('classname', str_replace('\\', '.', $className)); // pest-changed
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->currentTestCase = $testCase;
|
||||||
|
$this->time = $event->telemetryInfo()->time();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,19 +32,27 @@
|
|||||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of PHPUnit.
|
||||||
|
*
|
||||||
|
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
namespace PHPUnit\Runner\Filter;
|
namespace PHPUnit\Runner\Filter;
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use Pest\Contracts\HasPrintableTestCaseName;
|
use Pest\Contracts\HasPrintableTestCaseName;
|
||||||
use PHPUnit\Framework\SelfDescribing;
|
|
||||||
use PHPUnit\Framework\Test;
|
use PHPUnit\Framework\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use PHPUnit\Framework\TestSuite;
|
use PHPUnit\Framework\TestSuite;
|
||||||
|
use PHPUnit\Runner\PhptTestCase;
|
||||||
use RecursiveFilterIterator;
|
use RecursiveFilterIterator;
|
||||||
use RecursiveIterator;
|
use RecursiveIterator;
|
||||||
|
|
||||||
use function end;
|
use function end;
|
||||||
use function implode;
|
|
||||||
use function preg_match;
|
use function preg_match;
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
use function str_replace;
|
use function str_replace;
|
||||||
@ -52,22 +60,30 @@ use function str_replace;
|
|||||||
/**
|
/**
|
||||||
* @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 NameFilterIterator extends RecursiveFilterIterator
|
abstract class NameFilterIterator extends RecursiveFilterIterator
|
||||||
{
|
{
|
||||||
private ?string $filter = null;
|
/**
|
||||||
|
* @psalm-var non-empty-string
|
||||||
|
*/
|
||||||
|
private readonly string $regularExpression;
|
||||||
|
|
||||||
private ?int $filterMin = null;
|
private readonly ?int $dataSetMinimum;
|
||||||
|
|
||||||
private ?int $filterMax = null;
|
private readonly ?int $dataSetMaximum;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Exception
|
* @psalm-param RecursiveIterator<int, Test> $iterator
|
||||||
|
* @psalm-param non-empty-string $filter
|
||||||
*/
|
*/
|
||||||
public function __construct(RecursiveIterator $iterator, string $filter)
|
public function __construct(RecursiveIterator $iterator, string $filter)
|
||||||
{
|
{
|
||||||
parent::__construct($iterator);
|
parent::__construct($iterator);
|
||||||
|
|
||||||
$this->setFilter($filter);
|
$preparedFilter = $this->prepareFilter($filter);
|
||||||
|
|
||||||
|
$this->regularExpression = $preparedFilter['regularExpression'];
|
||||||
|
$this->dataSetMinimum = $preparedFilter['dataSetMinimum'];
|
||||||
|
$this->dataSetMaximum = $preparedFilter['dataSetMaximum'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function accept(): bool
|
public function accept(): bool
|
||||||
@ -78,29 +94,38 @@ final class NameFilterIterator extends RecursiveFilterIterator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmp = $this->describe($test);
|
if ($test instanceof PhptTestCase) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if ($tmp[0] !== '') {
|
if ($test instanceof HasPrintableTestCaseName) {
|
||||||
$name = implode('::', $tmp);
|
$name = $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName();
|
||||||
} else {
|
} else {
|
||||||
$name = $tmp[1];
|
$name = $test::class.'::'.$test->nameWithDataSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
$accepted = @preg_match($this->filter, $name, $matches);
|
$accepted = @preg_match($this->regularExpression, $name, $matches) === 1;
|
||||||
|
|
||||||
if ($accepted && isset($this->filterMax)) {
|
if ($accepted && isset($this->dataSetMaximum)) {
|
||||||
$set = end($matches);
|
$set = end($matches);
|
||||||
$accepted = $set >= $this->filterMin && $set <= $this->filterMax;
|
$accepted = $set >= $this->dataSetMinimum && $set <= $this->dataSetMaximum;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (bool) $accepted;
|
return $this->doAccept($accepted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract protected function doAccept(bool $result): bool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Exception
|
* @psalm-param non-empty-string $filter
|
||||||
|
*
|
||||||
|
* @psalm-return array{regularExpression: non-empty-string, dataSetMinimum: ?int, dataSetMaximum: ?int}
|
||||||
*/
|
*/
|
||||||
private function setFilter(string $filter): void
|
private function prepareFilter(string $filter): array
|
||||||
{
|
{
|
||||||
|
$dataSetMinimum = null;
|
||||||
|
$dataSetMaximum = null;
|
||||||
|
|
||||||
if (@preg_match($filter, '') === false) {
|
if (@preg_match($filter, '') === false) {
|
||||||
// Handles:
|
// Handles:
|
||||||
// * testAssertEqualsSucceeds#4
|
// * testAssertEqualsSucceeds#4
|
||||||
@ -108,17 +133,17 @@ final class NameFilterIterator extends RecursiveFilterIterator
|
|||||||
if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) {
|
if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) {
|
||||||
if (isset($matches[3]) && $matches[2] < $matches[3]) {
|
if (isset($matches[3]) && $matches[2] < $matches[3]) {
|
||||||
$filter = sprintf(
|
$filter = sprintf(
|
||||||
'%s.*with dataset #(\d+)$',
|
'%s.*with data set #(\d+)$',
|
||||||
$matches[1]
|
$matches[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->filterMin = (int) $matches[2];
|
$dataSetMinimum = (int) $matches[2];
|
||||||
$this->filterMax = (int) $matches[3];
|
$dataSetMaximum = (int) $matches[3];
|
||||||
} else {
|
} else {
|
||||||
$filter = sprintf(
|
$filter = sprintf(
|
||||||
'%s.*with dataset #%s$',
|
'%s.*with data set #%s$',
|
||||||
$matches[1],
|
$matches[1],
|
||||||
$matches[2]
|
$matches[2],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} // Handles:
|
} // Handles:
|
||||||
@ -126,9 +151,9 @@ final class NameFilterIterator extends RecursiveFilterIterator
|
|||||||
// * testDetermineJsonError@JSON.*
|
// * testDetermineJsonError@JSON.*
|
||||||
elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) {
|
elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) {
|
||||||
$filter = sprintf(
|
$filter = sprintf(
|
||||||
'%s.*with dataset "%s"$',
|
'%s.*with data set "%s"$',
|
||||||
$matches[1],
|
$matches[1],
|
||||||
$matches[2]
|
$matches[2],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,34 +164,15 @@ final class NameFilterIterator extends RecursiveFilterIterator
|
|||||||
str_replace(
|
str_replace(
|
||||||
'/',
|
'/',
|
||||||
'\\/',
|
'\\/',
|
||||||
$filter
|
$filter,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->filter = $filter;
|
return [
|
||||||
}
|
'regularExpression' => $filter,
|
||||||
|
'dataSetMinimum' => $dataSetMinimum,
|
||||||
/**
|
'dataSetMaximum' => $dataSetMaximum,
|
||||||
* @psalm-return array{0: string, 1: string}
|
];
|
||||||
*/
|
|
||||||
private function describe(Test $test): array
|
|
||||||
{
|
|
||||||
if ($test instanceof HasPrintableTestCaseName) {
|
|
||||||
return [
|
|
||||||
$test::getPrintableTestCaseName(),
|
|
||||||
$test->getPrintableTestCaseMethodName(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($test instanceof TestCase) {
|
|
||||||
return [$test::class, $test->nameWithDataSet()];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($test instanceof SelfDescribing) {
|
|
||||||
return ['', $test->toString()];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['', $test::class];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,11 +81,6 @@ final class DefaultResultCache implements ResultCache
|
|||||||
*/
|
*/
|
||||||
private array $defects = [];
|
private array $defects = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-var array<string, TestStatus>
|
|
||||||
*/
|
|
||||||
private array $currentDefects = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @psalm-var array<string, float>
|
* @psalm-var array<string, float>
|
||||||
*/
|
*/
|
||||||
@ -102,10 +97,11 @@ final class DefaultResultCache implements ResultCache
|
|||||||
|
|
||||||
public function setStatus(string $id, TestStatus $status): void
|
public function setStatus(string $id, TestStatus $status): void
|
||||||
{
|
{
|
||||||
if ($status->isFailure() || $status->isError()) {
|
if ($status->isSuccess()) {
|
||||||
$this->currentDefects[$id] = $status;
|
return;
|
||||||
$this->defects[$id] = $status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->defects[$id] = $status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function status(string $id): TestStatus
|
public function status(string $id): TestStatus
|
||||||
@ -115,10 +111,6 @@ final class DefaultResultCache implements ResultCache
|
|||||||
|
|
||||||
public function setTime(string $id, float $time): void
|
public function setTime(string $id, float $time): void
|
||||||
{
|
{
|
||||||
if (! isset($this->currentDefects[$id])) {
|
|
||||||
unset($this->defects[$id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->times[$id] = $time;
|
$this->times[$id] = $time;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,9 +125,15 @@ final class DefaultResultCache implements ResultCache
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$contents = file_get_contents($this->cacheFilename);
|
||||||
|
|
||||||
|
if ($contents === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$data = json_decode(
|
$data = json_decode(
|
||||||
file_get_contents($this->cacheFilename),
|
$contents,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($data === null) {
|
if ($data === null) {
|
||||||
@ -183,7 +181,7 @@ final class DefaultResultCache implements ResultCache
|
|||||||
file_put_contents(
|
file_put_contents(
|
||||||
$this->cacheFilename,
|
$this->cacheFilename,
|
||||||
json_encode($data),
|
json_encode($data),
|
||||||
LOCK_EX
|
LOCK_EX,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,11 +38,13 @@ namespace PHPUnit\Runner;
|
|||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Pest\Contracts\HasPrintableTestCaseName;
|
use Pest\Contracts\HasPrintableTestCaseName;
|
||||||
|
use Pest\Panic;
|
||||||
use Pest\TestCases\IgnorableTestCase;
|
use Pest\TestCases\IgnorableTestCase;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use ReflectionClass;
|
use ReflectionClass;
|
||||||
use ReflectionException;
|
use ReflectionException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
use function array_diff;
|
use function array_diff;
|
||||||
use function array_values;
|
use function array_values;
|
||||||
@ -86,7 +88,11 @@ final class TestSuiteLoader
|
|||||||
$suiteClassName = $this->classNameFromFileName($suiteClassFile);
|
$suiteClassName = $this->classNameFromFileName($suiteClassFile);
|
||||||
|
|
||||||
(static function () use ($suiteClassFile) {
|
(static function () use ($suiteClassFile) {
|
||||||
include_once $suiteClassFile;
|
try {
|
||||||
|
include_once $suiteClassFile;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Panic::with($e);
|
||||||
|
}
|
||||||
|
|
||||||
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
|
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -45,6 +45,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace PHPUnit\TextUI\Command;
|
namespace PHPUnit\TextUI\Command;
|
||||||
|
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
|
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
|
||||||
use PHPUnit\TextUI\Configuration\Configuration;
|
use PHPUnit\TextUI\Configuration\Configuration;
|
||||||
use PHPUnit\TextUI\Configuration\NoCoverageCacheDirectoryException;
|
use PHPUnit\TextUI\Configuration\NoCoverageCacheDirectoryException;
|
||||||
@ -55,11 +57,11 @@ use SebastianBergmann\Timer\Timer;
|
|||||||
/**
|
/**
|
||||||
* @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 WarmCodeCoverageCacheCommand implements Command
|
final readonly class WarmCodeCoverageCacheCommand implements Command
|
||||||
{
|
{
|
||||||
private readonly Configuration $configuration;
|
private Configuration $configuration;
|
||||||
|
|
||||||
private readonly CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
|
private CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
|
||||||
|
|
||||||
public function __construct(Configuration $configuration, CodeCoverageFilterRegistry $codeCoverageFilterRegistry)
|
public function __construct(Configuration $configuration, CodeCoverageFilterRegistry $codeCoverageFilterRegistry)
|
||||||
{
|
{
|
||||||
@ -76,16 +78,16 @@ final class WarmCodeCoverageCacheCommand implements Command
|
|||||||
if (! $this->configuration->hasCoverageCacheDirectory()) {
|
if (! $this->configuration->hasCoverageCacheDirectory()) {
|
||||||
return Result::from(
|
return Result::from(
|
||||||
'Cache for static analysis has not been configured'.PHP_EOL,
|
'Cache for static analysis has not been configured'.PHP_EOL,
|
||||||
Result::FAILURE
|
Result::FAILURE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->codeCoverageFilterRegistry->init($this->configuration);
|
$this->codeCoverageFilterRegistry->init($this->configuration, true);
|
||||||
|
|
||||||
if (! $this->codeCoverageFilterRegistry->configured()) {
|
if (! $this->codeCoverageFilterRegistry->configured()) {
|
||||||
return Result::from(
|
return Result::from(
|
||||||
'Filter for code coverage has not been configured'.PHP_EOL,
|
'Filter for code coverage has not been configured'.PHP_EOL,
|
||||||
Result::FAILURE
|
Result::FAILURE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +98,7 @@ final class WarmCodeCoverageCacheCommand implements Command
|
|||||||
$this->configuration->coverageCacheDirectory(),
|
$this->configuration->coverageCacheDirectory(),
|
||||||
! $this->configuration->disableCodeCoverageIgnore(),
|
! $this->configuration->disableCodeCoverageIgnore(),
|
||||||
$this->configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage(),
|
$this->configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage(),
|
||||||
$this->codeCoverageFilterRegistry->get()
|
$this->codeCoverageFilterRegistry->get(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Result::from();
|
return Result::from();
|
||||||
@ -43,7 +43,7 @@ declare(strict_types=1);
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace PHPUnit\TextUI\Output\Default\ProgressPrinter;
|
namespace Pest\Logging\TeamCity\Subscriber;
|
||||||
|
|
||||||
use PHPUnit\Event\Test\Skipped;
|
use PHPUnit\Event\Test\Skipped;
|
||||||
use PHPUnit\Event\Test\SkippedSubscriber;
|
use PHPUnit\Event\Test\SkippedSubscriber;
|
||||||
@ -51,21 +51,16 @@ use ReflectionClass;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @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
|
||||||
*
|
|
||||||
* This file is overridden to allow Pest Parallel to show todo items in the progress output.
|
|
||||||
*/
|
*/
|
||||||
final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
|
final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Notifies the printer that a test was skipped.
|
|
||||||
*/
|
|
||||||
public function notify(Skipped $event): void
|
public function notify(Skipped $event): void
|
||||||
{
|
{
|
||||||
if (str_contains($event->message(), '__TODO__')) {
|
if (str_contains($event->message(), '__TODO__')) {
|
||||||
$this->printTodoItem();
|
$this->printTodoItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->printer()->testSkipped();
|
$this->logger()->testSkipped($event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,7 +33,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This file is part of PHPUnit.
|
* This file is part of PHPUnit.
|
||||||
*
|
*
|
||||||
@ -57,73 +56,74 @@ use function array_map;
|
|||||||
/**
|
/**
|
||||||
* @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 TestSuiteFilterProcessor
|
final readonly class TestSuiteFilterProcessor
|
||||||
{
|
{
|
||||||
private Factory $filterFactory;
|
|
||||||
|
|
||||||
public function __construct(Factory $factory = new Factory)
|
|
||||||
{
|
|
||||||
$this->filterFactory = $factory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Event\RuntimeException
|
* @throws Event\RuntimeException
|
||||||
* @throws FilterNotConfiguredException
|
* @throws FilterNotConfiguredException
|
||||||
*/
|
*/
|
||||||
public function process(Configuration $configuration, TestSuite $suite): void
|
public function process(Configuration $configuration, TestSuite $suite): void
|
||||||
{
|
{
|
||||||
|
$factory = new Factory;
|
||||||
|
|
||||||
if (! $configuration->hasFilter() &&
|
if (! $configuration->hasFilter() &&
|
||||||
! $configuration->hasGroups() &&
|
! $configuration->hasGroups() &&
|
||||||
! $configuration->hasExcludeGroups() &&
|
! $configuration->hasExcludeGroups() &&
|
||||||
|
! $configuration->hasExcludeFilter() &&
|
||||||
! $configuration->hasTestsCovering() &&
|
! $configuration->hasTestsCovering() &&
|
||||||
! $configuration->hasTestsUsing() &&
|
! $configuration->hasTestsUsing() &&
|
||||||
! Only::isEnabled()
|
! Only::isEnabled()) {
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($configuration->hasExcludeGroups()) {
|
if ($configuration->hasExcludeGroups()) {
|
||||||
$this->filterFactory->addExcludeGroupFilter(
|
$factory->addExcludeGroupFilter(
|
||||||
$configuration->excludeGroups()
|
$configuration->excludeGroups(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Only::isEnabled()) {
|
if (Only::isEnabled()) {
|
||||||
$this->filterFactory->addIncludeGroupFilter(['__pest_only']);
|
$factory->addIncludeGroupFilter([Only::group()]);
|
||||||
} elseif ($configuration->hasGroups()) {
|
} elseif ($configuration->hasGroups()) {
|
||||||
$this->filterFactory->addIncludeGroupFilter(
|
$factory->addIncludeGroupFilter(
|
||||||
$configuration->groups()
|
$configuration->groups(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($configuration->hasTestsCovering()) {
|
if ($configuration->hasTestsCovering()) {
|
||||||
$this->filterFactory->addIncludeGroupFilter(
|
$factory->addIncludeGroupFilter(
|
||||||
array_map(
|
array_map(
|
||||||
static fn (string $name): string => '__phpunit_covers_'.$name,
|
static fn (string $name): string => '__phpunit_covers_'.$name,
|
||||||
$configuration->testsCovering()
|
$configuration->testsCovering(),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($configuration->hasTestsUsing()) {
|
if ($configuration->hasTestsUsing()) {
|
||||||
$this->filterFactory->addIncludeGroupFilter(
|
$factory->addIncludeGroupFilter(
|
||||||
array_map(
|
array_map(
|
||||||
static fn (string $name): string => '__phpunit_uses_'.$name,
|
static fn (string $name): string => '__phpunit_uses_'.$name,
|
||||||
$configuration->testsUsing()
|
$configuration->testsUsing(),
|
||||||
)
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($configuration->hasExcludeFilter()) {
|
||||||
|
$factory->addExcludeNameFilter(
|
||||||
|
$configuration->excludeFilter(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($configuration->hasFilter()) {
|
if ($configuration->hasFilter()) {
|
||||||
$this->filterFactory->addNameFilter(
|
$factory->addIncludeNameFilter(
|
||||||
$configuration->filter()
|
$configuration->filter(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$suite->injectFilter($this->filterFactory);
|
$suite->injectFilter($factory);
|
||||||
|
|
||||||
Event\Facade::emitter()->testSuiteFiltered(
|
Event\Facade::emitter()->testSuiteFiltered(
|
||||||
Event\TestSuite\TestSuiteBuilder::from($suite)
|
Event\TestSuite\TestSuiteBuilder::from($suite),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
rector.php
38
rector.php
@ -2,30 +2,22 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
|
|
||||||
use Rector\Config\RectorConfig;
|
use Rector\Config\RectorConfig;
|
||||||
use Rector\Set\ValueObject\LevelSetList;
|
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
|
||||||
use Rector\Set\ValueObject\SetList;
|
|
||||||
|
|
||||||
return static function (RectorConfig $rectorConfig): void {
|
return RectorConfig::configure()
|
||||||
$rectorConfig->paths([
|
->withPaths([
|
||||||
__DIR__.'/src',
|
__DIR__.'/src',
|
||||||
]);
|
])
|
||||||
|
->withSkip([
|
||||||
$rectorConfig->rules([
|
|
||||||
InlineConstructorDefaultToPropertyRector::class,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$rectorConfig->skip([
|
|
||||||
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
|
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
|
||||||
]);
|
ReturnNeverTypeRector::class,
|
||||||
|
])
|
||||||
$rectorConfig->sets([
|
->withPreparedSets(
|
||||||
LevelSetList::UP_TO_PHP_81,
|
deadCode: true,
|
||||||
SetList::CODE_QUALITY,
|
codeQuality: true,
|
||||||
SetList::DEAD_CODE,
|
typeDeclarations: true,
|
||||||
SetList::EARLY_RETURN,
|
privatization: true,
|
||||||
SetList::TYPE_DECLARATION,
|
earlyReturn: true,
|
||||||
SetList::PRIVATIZATION,
|
)
|
||||||
]);
|
->withPhpSets();
|
||||||
};
|
|
||||||
|
|||||||
31
resources/base-phpunit.xml
Normal file
31
resources/base-phpunit.xml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Default">
|
||||||
|
<directory>tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>app</directory>
|
||||||
|
<directory>src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
<php>
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
|
<env name="CACHE_STORE" value="array"/>
|
||||||
|
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
|
||||||
|
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
|
||||||
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
@ -10,7 +10,7 @@
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="my-1">
|
<div class="my-1">
|
||||||
<span class="ml-2 px-1 bg-<?php echo $bgBadgeColor ?>-600 font-bold"><?php echo htmlspecialchars($bgBadgeText) ?></span>
|
<span class="ml-2 px-1 bg-<?php echo $bgBadgeColor ?> font-bold"><?php echo htmlspecialchars($bgBadgeText) ?></span>
|
||||||
<span class="ml-1">
|
<span class="ml-1">
|
||||||
<?php echo htmlspecialchars($content) ?>
|
<?php echo htmlspecialchars($content) ?>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
74
src/ArchPresets/AbstractPreset.php
Normal file
74
src/ArchPresets/AbstractPreset.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
use Pest\Arch\Contracts\ArchExpectation;
|
||||||
|
use Pest\Expectation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
abstract class AbstractPreset // @pest-arch-ignore-line
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The expectations.
|
||||||
|
*
|
||||||
|
* @var array<int, Expectation<mixed>|ArchExpectation>
|
||||||
|
*/
|
||||||
|
protected array $expectations = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new preset instance.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $userNamespaces
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $userNamespaces,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the arch preset.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
abstract public function execute(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ignores the given "targets" or "dependencies".
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $targetsOrDependencies
|
||||||
|
*/
|
||||||
|
final public function ignoring(array|string $targetsOrDependencies): void
|
||||||
|
{
|
||||||
|
$this->expectations = array_map(
|
||||||
|
fn (ArchExpectation|Expectation $expectation): Expectation|ArchExpectation => $expectation instanceof ArchExpectation ? $expectation->ignoring($targetsOrDependencies) : $expectation,
|
||||||
|
$this->expectations,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given callback for each namespace.
|
||||||
|
*
|
||||||
|
* @param callable(Expectation<string|null>): ArchExpectation ...$callbacks
|
||||||
|
*/
|
||||||
|
final public function eachUserNamespace(callable ...$callbacks): void
|
||||||
|
{
|
||||||
|
foreach ($this->userNamespaces as $namespace) {
|
||||||
|
foreach ($callbacks as $callback) {
|
||||||
|
$this->expectations[] = $callback(expect($namespace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes the expectations.
|
||||||
|
*/
|
||||||
|
final public function flush(): void
|
||||||
|
{
|
||||||
|
$this->expectations = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/ArchPresets/Custom.php
Normal file
45
src/ArchPresets/Custom.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Arch\Contracts\ArchExpectation;
|
||||||
|
use Pest\Expectation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Custom extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new preset instance.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $userNamespaces
|
||||||
|
* @param Closure(array<int, string>): array<Expectation<mixed>|ArchExpectation> $execute
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $userNamespaces,
|
||||||
|
private readonly string $name,
|
||||||
|
private readonly Closure $execute,
|
||||||
|
) {
|
||||||
|
parent::__construct($userNamespaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the preset.
|
||||||
|
*/
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the arch preset.
|
||||||
|
*/
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
$this->expectations = ($this->execute)($this->userNamespaces);
|
||||||
|
}
|
||||||
|
}
|
||||||
163
src/ArchPresets/Laravel.php
Normal file
163
src/ArchPresets/Laravel.php
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Laravel extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Executes the arch preset.
|
||||||
|
*/
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
$this->expectations[] = expect('App\Traits')
|
||||||
|
->toBeTraits();
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Concerns')
|
||||||
|
->toBeTraits();
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App')
|
||||||
|
->not->toBeEnums()
|
||||||
|
->ignoring('App\Enums');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Enums')
|
||||||
|
->toBeEnums();
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Features')
|
||||||
|
->toBeClasses();
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Features')
|
||||||
|
->toHaveMethod('resolve');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Exceptions')
|
||||||
|
->classes()
|
||||||
|
->toImplement('Throwable');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App')
|
||||||
|
->not->toImplement(Throwable::class)
|
||||||
|
->ignoring('App\Exceptions');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Http\Middleware')
|
||||||
|
->classes()
|
||||||
|
->toHaveMethod('handle');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Models')
|
||||||
|
->classes()
|
||||||
|
->toExtend('Illuminate\Database\Eloquent\Model')
|
||||||
|
->ignoring('App\Models\Scopes');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Models')
|
||||||
|
->classes()
|
||||||
|
->not->toHaveSuffix('Model');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App')
|
||||||
|
->not->toExtend('Illuminate\Database\Eloquent\Model')
|
||||||
|
->ignoring('App\Models');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Http\Requests')
|
||||||
|
->classes()
|
||||||
|
->toHaveSuffix('Request');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Http\Requests')
|
||||||
|
->toExtend('Illuminate\Foundation\Http\FormRequest');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Http\Requests')
|
||||||
|
->toHaveMethod('rules');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App')
|
||||||
|
->not->toExtend('Illuminate\Foundation\Http\FormRequest')
|
||||||
|
->ignoring('App\Http\Requests');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Console\Commands')
|
||||||
|
->classes()
|
||||||
|
->toHaveSuffix('Command');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Console\Commands')
|
||||||
|
->classes()
|
||||||
|
->toExtend('Illuminate\Console\Command');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Console\Commands')
|
||||||
|
->classes()
|
||||||
|
->toHaveMethod('handle');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App')
|
||||||
|
->not->toExtend('Illuminate\Console\Command')
|
||||||
|
->ignoring('App\Console\Commands');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Mail')
|
||||||
|
->classes()
|
||||||
|
->toExtend('Illuminate\Mail\Mailable');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Mail')
|
||||||
|
->classes()
|
||||||
|
->toImplement('Illuminate\Contracts\Queue\ShouldQueue');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App')
|
||||||
|
->not->toExtend('Illuminate\Mail\Mailable')
|
||||||
|
->ignoring('App\Mail');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Jobs')
|
||||||
|
->classes()
|
||||||
|
->toImplement('Illuminate\Contracts\Queue\ShouldQueue');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Jobs')
|
||||||
|
->classes()
|
||||||
|
->toHaveMethod('handle');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Listeners')
|
||||||
|
->toHaveMethod('handle');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Notifications')
|
||||||
|
->toExtend('Illuminate\Notifications\Notification');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App')
|
||||||
|
->not->toExtend('Illuminate\Notifications\Notification')
|
||||||
|
->ignoring('App\Notifications');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Providers')
|
||||||
|
->toHaveSuffix('ServiceProvider');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Providers')
|
||||||
|
->toExtend('Illuminate\Support\ServiceProvider');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Providers')
|
||||||
|
->not->toBeUsed();
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App')
|
||||||
|
->not->toExtend('Illuminate\Support\ServiceProvider')
|
||||||
|
->ignoring('App\Providers');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App')
|
||||||
|
->not->toHaveSuffix('ServiceProvider')
|
||||||
|
->ignoring('App\Providers');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App')
|
||||||
|
->not->toHaveSuffix('Controller')
|
||||||
|
->ignoring('App\Http\Controllers');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Http\Controllers')
|
||||||
|
->classes()
|
||||||
|
->toHaveSuffix('Controller');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Http')
|
||||||
|
->toOnlyBeUsedIn('App\Http');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Http\Controllers')
|
||||||
|
->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy']);
|
||||||
|
|
||||||
|
$this->expectations[] = expect([
|
||||||
|
'dd',
|
||||||
|
'ddd',
|
||||||
|
'dump',
|
||||||
|
'env',
|
||||||
|
'exit',
|
||||||
|
'ray',
|
||||||
|
])->not->toBeUsed();
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/ArchPresets/Php.php
Normal file
93
src/ArchPresets/Php.php
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Php extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Executes the arch preset.
|
||||||
|
*/
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
$this->expectations[] = expect([
|
||||||
|
'debug_zval_dump',
|
||||||
|
'debug_backtrace',
|
||||||
|
'debug_print_backtrace',
|
||||||
|
'dump',
|
||||||
|
'ray',
|
||||||
|
'ds',
|
||||||
|
'die',
|
||||||
|
'goto',
|
||||||
|
'global',
|
||||||
|
'var_dump',
|
||||||
|
'phpinfo',
|
||||||
|
'echo',
|
||||||
|
'ereg',
|
||||||
|
'eregi',
|
||||||
|
'mysql_connect',
|
||||||
|
'mysql_pconnect',
|
||||||
|
'mysql_query',
|
||||||
|
'mysql_select_db',
|
||||||
|
'mysql_fetch_array',
|
||||||
|
'mysql_fetch_assoc',
|
||||||
|
'mysql_fetch_object',
|
||||||
|
'mysql_fetch_row',
|
||||||
|
'mysql_num_rows',
|
||||||
|
'mysql_affected_rows',
|
||||||
|
'mysql_free_result',
|
||||||
|
'mysql_insert_id',
|
||||||
|
'mysql_error',
|
||||||
|
'mysql_real_escape_string',
|
||||||
|
'print',
|
||||||
|
'print_r',
|
||||||
|
'var_export',
|
||||||
|
'xdebug_break',
|
||||||
|
'xdebug_call_class',
|
||||||
|
'xdebug_call_file',
|
||||||
|
'xdebug_call_int',
|
||||||
|
'xdebug_call_line',
|
||||||
|
'xdebug_code_coverage_started',
|
||||||
|
'xdebug_connect_to_client',
|
||||||
|
'xdebug_debug_zval',
|
||||||
|
'xdebug_debug_zval_stdout',
|
||||||
|
'xdebug_dump_superglobals',
|
||||||
|
'xdebug_get_code_coverage',
|
||||||
|
'xdebug_get_collected_errors',
|
||||||
|
'xdebug_get_function_count',
|
||||||
|
'xdebug_get_function_stack',
|
||||||
|
'xdebug_get_gc_run_count',
|
||||||
|
'xdebug_get_gc_total_collected_roots',
|
||||||
|
'xdebug_get_gcstats_filename',
|
||||||
|
'xdebug_get_headers',
|
||||||
|
'xdebug_get_monitored_functions',
|
||||||
|
'xdebug_get_profiler_filename',
|
||||||
|
'xdebug_get_stack_depth',
|
||||||
|
'xdebug_get_tracefile_name',
|
||||||
|
'xdebug_info',
|
||||||
|
'xdebug_is_debugger_active',
|
||||||
|
'xdebug_memory_usage',
|
||||||
|
'xdebug_notify',
|
||||||
|
'xdebug_peak_memory_usage',
|
||||||
|
'xdebug_print_function_stack',
|
||||||
|
'xdebug_set_filter',
|
||||||
|
'xdebug_start_code_coverage',
|
||||||
|
'xdebug_start_error_collection',
|
||||||
|
'xdebug_start_function_monitor',
|
||||||
|
'xdebug_start_gcstats',
|
||||||
|
'xdebug_start_trace',
|
||||||
|
'xdebug_stop_code_coverage',
|
||||||
|
'xdebug_stop_error_collection',
|
||||||
|
'xdebug_stop_function_monitor',
|
||||||
|
'xdebug_stop_gcstats',
|
||||||
|
'xdebug_stop_trace',
|
||||||
|
'xdebug_time_index',
|
||||||
|
'xdebug_var_dump',
|
||||||
|
'trap',
|
||||||
|
])->not->toBeUsed();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/ArchPresets/Relaxed.php
Normal file
26
src/ArchPresets/Relaxed.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
use Pest\Arch\Contracts\ArchExpectation;
|
||||||
|
use Pest\Expectation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Relaxed extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Executes the arch preset.
|
||||||
|
*/
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
$this->eachUserNamespace(
|
||||||
|
fn (Expectation $namespace): ArchExpectation => $namespace->not->toUseStrictTypes(),
|
||||||
|
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeFinal(),
|
||||||
|
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHavePrivateMethods(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/ArchPresets/Security.php
Normal file
41
src/ArchPresets/Security.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Security extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Executes the arch preset.
|
||||||
|
*/
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
$this->expectations[] = expect([
|
||||||
|
'md5',
|
||||||
|
'sha1',
|
||||||
|
'uniqid',
|
||||||
|
'rand',
|
||||||
|
'mt_rand',
|
||||||
|
'tempnam',
|
||||||
|
'str_shuffle',
|
||||||
|
'shuffle',
|
||||||
|
'array_rand',
|
||||||
|
'eval',
|
||||||
|
'exec',
|
||||||
|
'shell_exec',
|
||||||
|
'system',
|
||||||
|
'passthru',
|
||||||
|
'create_function',
|
||||||
|
'unserialize',
|
||||||
|
'extract',
|
||||||
|
'parse_str',
|
||||||
|
'mb_parse_str',
|
||||||
|
'dl',
|
||||||
|
'assert',
|
||||||
|
])->not->toBeUsed();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/ArchPresets/Strict.php
Normal file
32
src/ArchPresets/Strict.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
use Pest\Arch\Contracts\ArchExpectation;
|
||||||
|
use Pest\Expectation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Strict extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Executes the arch preset.
|
||||||
|
*/
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
$this->eachUserNamespace(
|
||||||
|
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHaveProtectedMethods(),
|
||||||
|
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeAbstract(),
|
||||||
|
fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictTypes(),
|
||||||
|
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->toBeFinal(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->expectations[] = expect([
|
||||||
|
'sleep',
|
||||||
|
'usleep',
|
||||||
|
])->not->toBeUsed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,13 +12,13 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class BootKernelDump implements Bootstrapper
|
final readonly class BootKernelDump implements Bootstrapper
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Creates a new Boot Kernel Dump instance.
|
* Creates a new Boot Kernel Dump instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly OutputInterface $output,
|
private OutputInterface $output,
|
||||||
) {
|
) {
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<int, string>
|
* @var array<string, string>
|
||||||
*/
|
*/
|
||||||
private const FILES = [
|
public const FILES = [
|
||||||
'Runner/Filter/NameFilterIterator.php',
|
'c96b1cb57d7fc8e649f4c13a8abe418c2541bcfab194fb6702b99f777f52ee84' => 'Runner/Filter/NameFilterIterator.php',
|
||||||
'Runner/ResultCache/DefaultResultCache.php',
|
'a4a43de01f641c6944ee83d963795a46d32b5206b5ab3bbc6cce76e67190acbf' => 'Runner/ResultCache/DefaultResultCache.php',
|
||||||
'Runner/TestSuiteLoader.php',
|
'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php',
|
||||||
'TextUI/Command/WarmCodeCoverageCacheCommand.php',
|
'3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
|
||||||
'TextUI/Output/Default/ProgressPrinter/TestSkippedSubscriber.php',
|
'8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
|
||||||
'TextUI/TestSuiteFilterProcessor.php',
|
'43883b7e5811886cf3731c8ed6304d5a77078d9731e1e505abc2da36bde19f3e' => 'TextUI/TestSuiteFilterProcessor.php',
|
||||||
'Event/Value/ThrowableBuilder.php',
|
'357d5cd7007f8559b26e1b8cdf43bb6fb15b51b79db981779da6f31b7ec39dad' => 'Event/Value/ThrowableBuilder.php',
|
||||||
'Logging/JUnit/JunitXmlLogger.php',
|
'676273f1fe483877cf2d95c5aedbf9ae5d6a8e2f4c12d6ce716df6591e6db023' => 'Logging/JUnit/JunitXmlLogger.php',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use PHPUnit\Event\Subscriber;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class BootSubscribers implements Bootstrapper
|
final readonly class BootSubscribers implements Bootstrapper
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The list of Subscribers.
|
* The list of Subscribers.
|
||||||
@ -25,16 +25,14 @@ final class BootSubscribers implements Bootstrapper
|
|||||||
Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
|
Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
|
||||||
Subscribers\EnsureKernelDumpIsFlushed::class,
|
Subscribers\EnsureKernelDumpIsFlushed::class,
|
||||||
Subscribers\EnsureTeamCityEnabled::class,
|
Subscribers\EnsureTeamCityEnabled::class,
|
||||||
Subscribers\EnsureJunitEnabled::class,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the Boot Subscribers.
|
* Creates a new instance of the Boot Subscribers.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Container $container,
|
private Container $container,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boots the list of Subscribers.
|
* Boots the list of Subscribers.
|
||||||
|
|||||||
@ -11,13 +11,13 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class BootView implements Bootstrapper
|
final readonly class BootView implements Bootstrapper
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the Boot View.
|
* Creates a new instance of the Boot View.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly OutputInterface $output
|
private OutputInterface $output
|
||||||
) {
|
) {
|
||||||
// ..
|
// ..
|
||||||
}
|
}
|
||||||
|
|||||||
100
src/Collision/Events.php
Normal file
100
src/Collision/Events.php
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Collision;
|
||||||
|
|
||||||
|
use NunoMaduro\Collision\Adapters\Phpunit\TestResult;
|
||||||
|
use Pest\Configuration\Project;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
use function Termwind\render;
|
||||||
|
use function Termwind\renderUsing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Events
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Sets the output.
|
||||||
|
*/
|
||||||
|
private static ?OutputInterface $output = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the output.
|
||||||
|
*/
|
||||||
|
public static function setOutput(OutputInterface $output): void
|
||||||
|
{
|
||||||
|
self::$output = $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires before the test method description is printed.
|
||||||
|
*/
|
||||||
|
public static function beforeTestMethodDescription(TestResult $result, string $description): string
|
||||||
|
{
|
||||||
|
if (($context = $result->context) === []) {
|
||||||
|
return $description;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderUsing(self::$output);
|
||||||
|
|
||||||
|
[
|
||||||
|
'assignees' => $assignees,
|
||||||
|
'issues' => $issues,
|
||||||
|
'prs' => $prs,
|
||||||
|
] = $context;
|
||||||
|
|
||||||
|
if (($link = Project::getInstance()->issues) !== '') {
|
||||||
|
$issuesDescription = array_map(fn (int $issue): string => sprintf('<a href="%s">#%s</a>', sprintf($link, $issue), $issue), $issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($link = Project::getInstance()->prs) !== '') {
|
||||||
|
$prsDescription = array_map(fn (int $pr): string => sprintf('<a href="%s">#%s</a>', sprintf($link, $pr), $pr), $prs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($link = Project::getInstance()->assignees) !== '' && count($assignees) > 0) {
|
||||||
|
$assigneesDescription = array_map(fn (string $assignee): string => sprintf(
|
||||||
|
'<a href="%s">@%s</a>',
|
||||||
|
sprintf($link, $assignee),
|
||||||
|
$assignee,
|
||||||
|
), $assignees);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($assignees) > 0 || count($issues) > 0 || count($prs) > 0) {
|
||||||
|
$description .= ' '.implode(', ', array_merge(
|
||||||
|
$issuesDescription ?? [],
|
||||||
|
$prsDescription ?? [],
|
||||||
|
isset($assigneesDescription) ? ['['.implode(', ', $assigneesDescription).']'] : [],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires after the test method description is printed.
|
||||||
|
*/
|
||||||
|
public static function afterTestMethodDescription(TestResult $result): void
|
||||||
|
{
|
||||||
|
if (($context = $result->context) === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderUsing(self::$output);
|
||||||
|
|
||||||
|
[
|
||||||
|
'notes' => $notes,
|
||||||
|
] = $context;
|
||||||
|
|
||||||
|
foreach ($notes as $note) {
|
||||||
|
render(sprintf(<<<'HTML'
|
||||||
|
<div class="ml-2">
|
||||||
|
<span class="text-gray"> // %s</span>
|
||||||
|
</div>
|
||||||
|
HTML, $note,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -60,7 +60,7 @@ trait Pipeable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get th list of pipes by the given name.
|
* Get the list of pipes by the given name.
|
||||||
*
|
*
|
||||||
* @return array<int, Closure>
|
* @return array<int, Closure>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -5,14 +5,17 @@ declare(strict_types=1);
|
|||||||
namespace Pest\Concerns;
|
namespace Pest\Concerns;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Pest\Exceptions\DatasetArgsCountMismatch;
|
use Pest\Exceptions\DatasetArgumentsMismatch;
|
||||||
|
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\TestSuite;
|
use Pest\TestSuite;
|
||||||
|
use PHPUnit\Framework\Attributes\PostCondition;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use ReflectionException;
|
use ReflectionException;
|
||||||
use ReflectionFunction;
|
use ReflectionFunction;
|
||||||
|
use ReflectionParameter;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,11 +35,40 @@ trait Testable
|
|||||||
*/
|
*/
|
||||||
private static string $__latestDescription;
|
private static string $__latestDescription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test's assignees.
|
||||||
|
*/
|
||||||
|
private static array $__latestAssignees = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test's notes.
|
||||||
|
*/
|
||||||
|
private static array $__latestNotes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test's issues.
|
||||||
|
*
|
||||||
|
* @var array<int, int>
|
||||||
|
*/
|
||||||
|
private static array $__latestIssues = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test's PRs.
|
||||||
|
*
|
||||||
|
* @var array<int, int>
|
||||||
|
*/
|
||||||
|
private static array $__latestPrs = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's describing, if any.
|
* The test's describing, if any.
|
||||||
*/
|
*/
|
||||||
public ?string $__describing = null;
|
public ?string $__describing = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the test has ran or not.
|
||||||
|
*/
|
||||||
|
public bool $__ran = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's test closure.
|
* The test's test closure.
|
||||||
*/
|
*/
|
||||||
@ -67,15 +99,6 @@ trait Testable
|
|||||||
*/
|
*/
|
||||||
private array $__snapshotChanges = [];
|
private array $__snapshotChanges = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the test case static properties.
|
|
||||||
*/
|
|
||||||
public static function flush(): void
|
|
||||||
{
|
|
||||||
self::$__beforeAll = null;
|
|
||||||
self::$__afterAll = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Test Case instance.
|
* Creates a new Test Case instance.
|
||||||
*/
|
*/
|
||||||
@ -88,11 +111,36 @@ trait Testable
|
|||||||
if ($test->hasMethod($name)) {
|
if ($test->hasMethod($name)) {
|
||||||
$method = $test->getMethod($name);
|
$method = $test->getMethod($name);
|
||||||
$this->__description = self::$__latestDescription = $method->description;
|
$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->__describing = $method->describing;
|
||||||
$this->__test = $method->getClosure($this);
|
$this->__test = $method->getClosure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the test case static properties.
|
||||||
|
*/
|
||||||
|
public static function flush(): void
|
||||||
|
{
|
||||||
|
self::$__beforeAll = null;
|
||||||
|
self::$__afterAll = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new "note" to the Test Case.
|
||||||
|
*/
|
||||||
|
public function note(array|string $note): self
|
||||||
|
{
|
||||||
|
$note = is_array($note) ? $note : [$note];
|
||||||
|
|
||||||
|
self::$__latestNotes = array_merge(self::$__latestNotes, $note);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new "setUpBeforeClass" to the Test Case.
|
* Adds a new "setUpBeforeClass" to the Test Case.
|
||||||
*/
|
*/
|
||||||
@ -186,14 +234,22 @@ trait Testable
|
|||||||
/**
|
/**
|
||||||
* Gets executed before the Test Case.
|
* Gets executed before the Test Case.
|
||||||
*/
|
*/
|
||||||
protected function setUp(): void
|
protected function setUp(...$arguments): void
|
||||||
{
|
{
|
||||||
TestSuite::getInstance()->test = $this;
|
TestSuite::getInstance()->test = $this;
|
||||||
|
|
||||||
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
||||||
|
|
||||||
$description = $this->dataName() ? $method->description.' with '.$this->dataName() : $method->description;
|
$method->setUp($this);
|
||||||
$description = htmlspecialchars(html_entity_decode($description), ENT_NOQUOTES);
|
|
||||||
|
$description = $method->description;
|
||||||
|
if ($this->dataName()) {
|
||||||
|
$description = str_contains((string) $description, ':dataset')
|
||||||
|
? str_replace(':dataset', str_replace('dataset ', '', $this->dataName()), (string) $description)
|
||||||
|
: $description.' with '.$this->dataName();
|
||||||
|
}
|
||||||
|
|
||||||
|
$description = htmlspecialchars(html_entity_decode((string) $description), ENT_NOQUOTES);
|
||||||
|
|
||||||
if ($method->repetitions > 1) {
|
if ($method->repetitions > 1) {
|
||||||
$matches = [];
|
$matches = [];
|
||||||
@ -211,6 +267,10 @@ trait Testable
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->__description = self::$__latestDescription = $description;
|
$this->__description = self::$__latestDescription = $description;
|
||||||
|
self::$__latestAssignees = $method->assignees;
|
||||||
|
self::$__latestNotes = $method->notes;
|
||||||
|
self::$__latestIssues = $method->issues;
|
||||||
|
self::$__latestPrs = $method->prs;
|
||||||
|
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
@ -220,13 +280,13 @@ trait Testable
|
|||||||
$beforeEach = ChainableClosure::bound($this->__beforeEach, $beforeEach);
|
$beforeEach = ChainableClosure::bound($this->__beforeEach, $beforeEach);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->__callClosure($beforeEach, func_get_args());
|
$this->__callClosure($beforeEach, $arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets executed after the Test Case.
|
* Gets executed after the Test Case.
|
||||||
*/
|
*/
|
||||||
protected function tearDown(): void
|
protected function tearDown(...$arguments): void
|
||||||
{
|
{
|
||||||
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
|
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
|
||||||
|
|
||||||
@ -234,11 +294,16 @@ trait Testable
|
|||||||
$afterEach = ChainableClosure::bound($this->__afterEach, $afterEach);
|
$afterEach = ChainableClosure::bound($this->__afterEach, $afterEach);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->__callClosure($afterEach, func_get_args());
|
try {
|
||||||
|
$this->__callClosure($afterEach, func_get_args());
|
||||||
|
} finally {
|
||||||
|
parent::tearDown();
|
||||||
|
|
||||||
parent::tearDown();
|
TestSuite::getInstance()->test = null;
|
||||||
|
|
||||||
TestSuite::getInstance()->test = null;
|
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
||||||
|
$method->tearDown($this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -249,7 +314,7 @@ trait Testable
|
|||||||
private function __runTest(Closure $closure, ...$args): mixed
|
private function __runTest(Closure $closure, ...$args): mixed
|
||||||
{
|
{
|
||||||
$arguments = $this->__resolveTestArguments($args);
|
$arguments = $this->__resolveTestArguments($args);
|
||||||
$this->__ensureDatasetArgumentNumberMatches($arguments);
|
$this->__ensureDatasetArgumentNameAndNumberMatches($arguments);
|
||||||
|
|
||||||
return $this->__callClosure($closure, $arguments);
|
return $this->__callClosure($closure, $arguments);
|
||||||
}
|
}
|
||||||
@ -264,7 +329,12 @@ trait Testable
|
|||||||
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
||||||
|
|
||||||
if ($method->repetitions > 1) {
|
if ($method->repetitions > 1) {
|
||||||
array_shift($arguments);
|
// If the test is repeated, the first argument is the iteration number
|
||||||
|
// we need to move it to the end of the arguments list
|
||||||
|
// so that the datasets are the first n arguments
|
||||||
|
// and the iteration number is the last argument
|
||||||
|
$firstArgument = array_shift($arguments);
|
||||||
|
$arguments[] = $firstArgument;
|
||||||
}
|
}
|
||||||
|
|
||||||
$underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure');
|
$underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure');
|
||||||
@ -286,11 +356,11 @@ trait Testable
|
|||||||
return $arguments;
|
return $arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $arguments[0] instanceof Closure) {
|
if (! isset($arguments[0]) || ! $arguments[0] instanceof Closure) {
|
||||||
return $arguments;
|
return $arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($testParameterTypes[0], [Closure::class, 'callable'])) {
|
if (isset($testParameterTypes[0]) && in_array($testParameterTypes[0], [Closure::class, 'callable'])) {
|
||||||
return $arguments;
|
return $arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,9 +379,9 @@ trait Testable
|
|||||||
* Ensures dataset items count matches underlying test case required parameters
|
* Ensures dataset items count matches underlying test case required parameters
|
||||||
*
|
*
|
||||||
* @throws ReflectionException
|
* @throws ReflectionException
|
||||||
* @throws DatasetArgsCountMismatch
|
* @throws DatasetArgumentsMismatch
|
||||||
*/
|
*/
|
||||||
private function __ensureDatasetArgumentNumberMatches(array $arguments): void
|
private function __ensureDatasetArgumentNameAndNumberMatches(array $arguments): void
|
||||||
{
|
{
|
||||||
if ($arguments === []) {
|
if ($arguments === []) {
|
||||||
return;
|
return;
|
||||||
@ -322,11 +392,21 @@ trait Testable
|
|||||||
$requiredParametersCount = $testReflection->getNumberOfRequiredParameters();
|
$requiredParametersCount = $testReflection->getNumberOfRequiredParameters();
|
||||||
$suppliedParametersCount = count($arguments);
|
$suppliedParametersCount = count($arguments);
|
||||||
|
|
||||||
if ($suppliedParametersCount >= $requiredParametersCount) {
|
$datasetParameterNames = array_keys($arguments);
|
||||||
|
$testParameterNames = array_map(
|
||||||
|
fn (ReflectionParameter $reflectionParameter): string => $reflectionParameter->getName(),
|
||||||
|
array_filter($testReflection->getParameters(), fn (ReflectionParameter $reflectionParameter): bool => ! $reflectionParameter->isOptional()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (array_diff($testParameterNames, $datasetParameterNames) === []) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new DatasetArgsCountMismatch($requiredParametersCount, $suppliedParametersCount);
|
if (isset($testParameterNames[0]) && $suppliedParametersCount >= $requiredParametersCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DatasetArgumentsMismatch($requiredParametersCount, $suppliedParametersCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -337,7 +417,15 @@ trait Testable
|
|||||||
return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
|
return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @postCondition */
|
/**
|
||||||
|
* Uses the given preset on the test.
|
||||||
|
*/
|
||||||
|
public function preset(): Preset
|
||||||
|
{
|
||||||
|
return new Preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[PostCondition]
|
||||||
protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void
|
protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void
|
||||||
{
|
{
|
||||||
if (count($this->__snapshotChanges) === 0) {
|
if (count($this->__snapshotChanges) === 0) {
|
||||||
@ -378,4 +466,17 @@ trait Testable
|
|||||||
{
|
{
|
||||||
return self::$__latestDescription;
|
return self::$__latestDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The printable test case method context.
|
||||||
|
*/
|
||||||
|
public static function getPrintableContext(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'assignees' => self::$__latestAssignees,
|
||||||
|
'issues' => self::$__latestIssues,
|
||||||
|
'prs' => self::$__latestPrs,
|
||||||
|
'notes' => self::$__latestNotes,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/Configuration.php
Normal file
114
src/Configuration.php
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest;
|
||||||
|
|
||||||
|
use Pest\PendingCalls\UsesCall;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @mixin UsesCall
|
||||||
|
*/
|
||||||
|
final readonly class Configuration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The filename of the configuration.
|
||||||
|
*/
|
||||||
|
private string $filename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new configuration instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $filename,
|
||||||
|
) {
|
||||||
|
$this->filename = str_ends_with($filename, DIRECTORY_SEPARATOR.'Pest.php') ? dirname($filename) : $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the given classes and traits in the given targets.
|
||||||
|
*/
|
||||||
|
public function in(string ...$targets): UsesCall
|
||||||
|
{
|
||||||
|
return (new UsesCall($this->filename, []))->in(...$targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depending on where is called, it will extend the given classes and traits globally or locally.
|
||||||
|
*/
|
||||||
|
public function extend(string ...$classAndTraits): UsesCall
|
||||||
|
{
|
||||||
|
return new UsesCall(
|
||||||
|
$this->filename,
|
||||||
|
array_values($classAndTraits)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depending on where is called, it will extend the given classes and traits globally or locally.
|
||||||
|
*/
|
||||||
|
public function extends(string ...$classAndTraits): UsesCall
|
||||||
|
{
|
||||||
|
return $this->extend(...$classAndTraits);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depending on where is called, it will add the given groups globally or locally.
|
||||||
|
*/
|
||||||
|
public function group(string ...$groups): UsesCall
|
||||||
|
{
|
||||||
|
return (new UsesCall($this->filename, []))->group(...$groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depending on where is called, it will extend the given classes and traits globally or locally.
|
||||||
|
*/
|
||||||
|
public function use(string ...$classAndTraits): UsesCall
|
||||||
|
{
|
||||||
|
return $this->extend(...$classAndTraits);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depending on where is called, it will extend the given classes and traits globally or locally.
|
||||||
|
*/
|
||||||
|
public function uses(string ...$classAndTraits): UsesCall
|
||||||
|
{
|
||||||
|
return $this->extends(...$classAndTraits);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the printer configuration.
|
||||||
|
*/
|
||||||
|
public function printer(): Configuration\Printer
|
||||||
|
{
|
||||||
|
return new Configuration\Printer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the presets configuration.
|
||||||
|
*/
|
||||||
|
public function presets(): Configuration\Presets
|
||||||
|
{
|
||||||
|
return new Configuration\Presets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the project configuration.
|
||||||
|
*/
|
||||||
|
public function project(): Configuration\Project
|
||||||
|
{
|
||||||
|
return Configuration\Project::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxies calls to the uses method.
|
||||||
|
*
|
||||||
|
* @param array<array-key, mixed> $arguments
|
||||||
|
*/
|
||||||
|
public function __call(string $name, array $arguments): mixed
|
||||||
|
{
|
||||||
|
return $this->uses()->$name(...$arguments); // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/Configuration/Presets.php
Normal file
19
src/Configuration/Presets.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Configuration;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Preset;
|
||||||
|
|
||||||
|
final class Presets
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a custom preset instance, and adds it to the list of presets.
|
||||||
|
*/
|
||||||
|
public function custom(string $name, Closure $execute): void
|
||||||
|
{
|
||||||
|
Preset::custom($name, $execute);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Configuration/Printer.php
Normal file
23
src/Configuration/Printer.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Configuration;
|
||||||
|
|
||||||
|
use NunoMaduro\Collision\Adapters\Phpunit\Printers\DefaultPrinter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class Printer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Sets the theme to compact.
|
||||||
|
*/
|
||||||
|
public function compact(): self
|
||||||
|
{
|
||||||
|
DefaultPrinter::compact(true);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/Configuration/Project.php
Normal file
109
src/Configuration/Project.php
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Project
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The assignees link.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public string $assignees = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The issues link.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public string $issues = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The PRs link.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public string $prs = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The singleton instance.
|
||||||
|
*/
|
||||||
|
private static ?self $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of the project.
|
||||||
|
*/
|
||||||
|
public static function getInstance(): self
|
||||||
|
{
|
||||||
|
return self::$instance ??= new self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the test project to GitHub.
|
||||||
|
*/
|
||||||
|
public function github(string $project): self
|
||||||
|
{
|
||||||
|
$this->issues = "https://github.com/{$project}/issues/%s";
|
||||||
|
$this->prs = "https://github.com/{$project}/pull/%s";
|
||||||
|
|
||||||
|
$this->assignees = 'https://github.com/%s';
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the test project to GitLab.
|
||||||
|
*/
|
||||||
|
public function gitlab(string $project): self
|
||||||
|
{
|
||||||
|
$this->issues = "https://gitlab.com/{$project}/issues/%s";
|
||||||
|
$this->prs = "https://gitlab.com/{$project}/merge_requests/%s";
|
||||||
|
|
||||||
|
$this->assignees = 'https://gitlab.com/%s';
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the test project to Bitbucket.
|
||||||
|
*/
|
||||||
|
public function bitbucket(string $project): self
|
||||||
|
{
|
||||||
|
$this->issues = "https://bitbucket.org/{$project}/issues/%s";
|
||||||
|
$this->prs = "https://bitbucket.org/{$project}/pull-requests/%s";
|
||||||
|
|
||||||
|
$this->assignees = 'https://bitbucket.org/%s';
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the test project to Jira.
|
||||||
|
*/
|
||||||
|
public function jira(string $namespace, string $project): self
|
||||||
|
{
|
||||||
|
$this->issues = "https://{$namespace}.atlassian.net/browse/{$project}-%s";
|
||||||
|
|
||||||
|
$this->assignees = "https://{$namespace}.atlassian.net/secure/ViewProfile.jspa?name=%s";
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the test project to custom.
|
||||||
|
*/
|
||||||
|
public function custom(string $issues, string $prs, string $assignees): self
|
||||||
|
{
|
||||||
|
$this->issues = $issues;
|
||||||
|
$this->prs = $prs;
|
||||||
|
|
||||||
|
$this->assignees = $assignees;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Help
|
final readonly class Help
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The Command messages.
|
* The Command messages.
|
||||||
@ -27,7 +27,7 @@ final class Help
|
|||||||
/**
|
/**
|
||||||
* Creates a new Console Command instance.
|
* Creates a new Console Command instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(private readonly OutputInterface $output)
|
public function __construct(private OutputInterface $output)
|
||||||
{
|
{
|
||||||
// ..
|
// ..
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Question\ConfirmationQuestion;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Thanks
|
final readonly class Thanks
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The support options.
|
* The support options.
|
||||||
@ -33,8 +33,8 @@ final class Thanks
|
|||||||
* Creates a new Console Command instance.
|
* Creates a new Console Command instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly InputInterface $input,
|
private InputInterface $input,
|
||||||
private readonly OutputInterface $output
|
private OutputInterface $output
|
||||||
) {
|
) {
|
||||||
// ..
|
// ..
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ final class Thanks
|
|||||||
$wantsToSupport = false;
|
$wantsToSupport = false;
|
||||||
|
|
||||||
if (getenv('PEST_NO_SUPPORT') !== 'true' && $this->input->isInteractive()) {
|
if (getenv('PEST_NO_SUPPORT') !== 'true' && $this->input->isInteractive()) {
|
||||||
$wantsToSupport = (new SymfonyQuestionHelper())->ask(
|
$wantsToSupport = (new SymfonyQuestionHelper)->ask(
|
||||||
new ArrayInput([]),
|
new ArrayInput([]),
|
||||||
$this->output,
|
$this->output,
|
||||||
new ConfirmationQuestion(
|
new ConfirmationQuestion(
|
||||||
@ -72,13 +72,13 @@ final class Thanks
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($wantsToSupport === true) {
|
if ($wantsToSupport === true) {
|
||||||
if (PHP_OS_FAMILY == 'Darwin') {
|
if (PHP_OS_FAMILY === 'Darwin') {
|
||||||
exec('open https://github.com/pestphp/pest');
|
exec('open https://github.com/pestphp/pest');
|
||||||
}
|
}
|
||||||
if (PHP_OS_FAMILY == 'Windows') {
|
if (PHP_OS_FAMILY === 'Windows') {
|
||||||
exec('start https://github.com/pestphp/pest');
|
exec('start https://github.com/pestphp/pest');
|
||||||
}
|
}
|
||||||
if (PHP_OS_FAMILY == 'Linux') {
|
if (PHP_OS_FAMILY === 'Linux') {
|
||||||
exec('xdg-open https://github.com/pestphp/pest');
|
exec('xdg-open https://github.com/pestphp/pest');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Contracts;
|
|
||||||
|
|
||||||
use Pest\Factories\TestCaseMethodFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
interface AddsAnnotations
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Adds annotations to the given test case method.
|
|
||||||
*
|
|
||||||
* @param array<int, string> $annotations
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
public function __invoke(TestCaseMethodFactory $method, array $annotations): array;
|
|
||||||
}
|
|
||||||
10
src/Contracts/ArchPreset.php
Normal file
10
src/Contracts/ArchPreset.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface ArchPreset {}
|
||||||
18
src/Contracts/Plugins/HandlesOriginalArguments.php
Normal file
18
src/Contracts/Plugins/HandlesOriginalArguments.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts\Plugins;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface HandlesOriginalArguments
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Adds original arguments before the Test Suite execution.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
*/
|
||||||
|
public function handleOriginalArguments(array $arguments): void;
|
||||||
|
}
|
||||||
@ -7,10 +7,10 @@ namespace Pest\Contracts\Plugins;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
interface Shutdownable
|
interface Terminable
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Shutdowns the plugin.
|
* Terminates the plugin.
|
||||||
*/
|
*/
|
||||||
public function shutdown(): void;
|
public function terminate(): void;
|
||||||
}
|
}
|
||||||
33
src/Evaluators/Attributes.php
Normal file
33
src/Evaluators/Attributes.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Evaluators;
|
||||||
|
|
||||||
|
use Pest\Factories\Attribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Attributes
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Evaluates the given attributes and returns the code.
|
||||||
|
*
|
||||||
|
* @param iterable<int, Attribute> $attributes
|
||||||
|
*/
|
||||||
|
public static function code(iterable $attributes): string
|
||||||
|
{
|
||||||
|
return implode(PHP_EOL, array_map(function (Attribute $attribute): string {
|
||||||
|
$name = $attribute->name;
|
||||||
|
|
||||||
|
if ($attribute->arguments === []) {
|
||||||
|
return " #[\\{$name}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments = array_map(fn (string $argument): string => var_export($argument, true), iterator_to_array($attribute->arguments));
|
||||||
|
|
||||||
|
return sprintf(' #[\\%s(%s)]', $name, implode(', ', $arguments));
|
||||||
|
}, iterator_to_array($attributes)));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/AfterBeforeTestFunction.php
Normal file
24
src/Exceptions/AfterBeforeTestFunction.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Exceptions;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderlessEditor;
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
||||||
|
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class AfterBeforeTestFunction extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new Exception instance.
|
||||||
|
*/
|
||||||
|
public function __construct(string $filename)
|
||||||
|
{
|
||||||
|
parent::__construct('After method cannot be used with before the [test|it] functions in the filename `['.$filename.']`.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
final class DatasetArgsCountMismatch extends Exception
|
|
||||||
{
|
|
||||||
public function __construct(int $requiredCount, int $suppliedCount)
|
|
||||||
{
|
|
||||||
parent::__construct(sprintf('Test expects %d arguments but dataset only provides %d', $requiredCount, $suppliedCount));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
21
src/Exceptions/DatasetArgumentsMismatch.php
Normal file
21
src/Exceptions/DatasetArgumentsMismatch.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
final class DatasetArgumentsMismatch extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(int $requiredCount, int $suppliedCount)
|
||||||
|
{
|
||||||
|
if ($requiredCount <= $suppliedCount) {
|
||||||
|
parent::__construct('Test argument names and dataset keys do not match');
|
||||||
|
} else {
|
||||||
|
parent::__construct(sprintf('Test expects %d arguments but dataset only provides %d', $requiredCount, $suppliedCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
}
|
||||||
16
src/Exceptions/FatalException.php
Normal file
16
src/Exceptions/FatalException.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Exceptions;
|
||||||
|
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class FatalException extends RuntimeException implements RenderlessTrace
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
31
src/Exceptions/TestClosureMustNotBeStatic.php
Normal file
31
src/Exceptions/TestClosureMustNotBeStatic.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Exceptions;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderlessEditor;
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
||||||
|
use Pest\Factories\TestCaseMethodFactory;
|
||||||
|
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class TestClosureMustNotBeStatic extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new Exception instance.
|
||||||
|
*/
|
||||||
|
public function __construct(TestCaseMethodFactory $method)
|
||||||
|
{
|
||||||
|
parent::__construct(
|
||||||
|
sprintf(
|
||||||
|
'Test closure must not be static. Please remove the `static` keyword from the `%s` method in `%s`.',
|
||||||
|
$method->description,
|
||||||
|
$method->filename
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -30,8 +30,12 @@ use Pest\Expectations\HigherOrderExpectation;
|
|||||||
use Pest\Expectations\OppositeExpectation;
|
use Pest\Expectations\OppositeExpectation;
|
||||||
use Pest\Matchers\Any;
|
use Pest\Matchers\Any;
|
||||||
use Pest\Support\ExpectationPipeline;
|
use Pest\Support\ExpectationPipeline;
|
||||||
|
use Pest\Support\Reflection;
|
||||||
use PHPUnit\Architecture\Elements\ObjectDescription;
|
use PHPUnit\Architecture\Elements\ObjectDescription;
|
||||||
use PHPUnit\Framework\ExpectationFailedException;
|
use PHPUnit\Framework\ExpectationFailedException;
|
||||||
|
use ReflectionEnum;
|
||||||
|
use ReflectionMethod;
|
||||||
|
use ReflectionProperty;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template TValue
|
* @template TValue
|
||||||
@ -347,9 +351,15 @@ final class Expectation
|
|||||||
return new HigherOrderExpectation($this, call_user_func_array($this->value->$method(...), $parameters));
|
return new HigherOrderExpectation($this, call_user_func_array($this->value->$method(...), $parameters));
|
||||||
}
|
}
|
||||||
|
|
||||||
ExpectationPipeline::for($this->getExpectationClosure($method))
|
$closure = $this->getExpectationClosure($method);
|
||||||
|
$reflectionClosure = new \ReflectionFunction($closure);
|
||||||
|
$expectation = $reflectionClosure->getClosureThis();
|
||||||
|
|
||||||
|
assert(is_object($expectation));
|
||||||
|
|
||||||
|
ExpectationPipeline::for($closure)
|
||||||
->send(...$parameters)
|
->send(...$parameters)
|
||||||
->through($this->pipes($method, $this, Expectation::class))
|
->through($this->pipes($method, $expectation, Expectation::class))
|
||||||
->run();
|
->run();
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -414,7 +424,7 @@ final class Expectation
|
|||||||
*/
|
*/
|
||||||
public function any(): Any
|
public function any(): Any
|
||||||
{
|
{
|
||||||
return new Any();
|
return new Any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -427,6 +437,71 @@ final class Expectation
|
|||||||
return ToUse::make($this, $targets);
|
return ToUse::make($this, $targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target does have the given permissions
|
||||||
|
*/
|
||||||
|
public function toHaveFileSystemPermissions(string $permissions): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this,
|
||||||
|
fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) === $permissions,
|
||||||
|
sprintf('permissions to be [%s]', $permissions),
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target to have line count less than the given number.
|
||||||
|
*/
|
||||||
|
public function toHaveLineCountLessThan(int $lines): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this,
|
||||||
|
fn (ObjectDescription $object): bool => count(file($object->path)) < $lines, // @phpstan-ignore-line
|
||||||
|
sprintf('to have less than %d lines of code', $lines),
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target have all methods documented.
|
||||||
|
*/
|
||||||
|
public function toHaveMethodsDocumented(): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this,
|
||||||
|
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|
||||||
|
|| array_filter(
|
||||||
|
Reflection::getMethodsFromReflectionClass($object->reflectionClass),
|
||||||
|
fn (ReflectionMethod $method): bool => (enum_exists($object->name) === false || in_array($method->name, ['from', 'tryFrom', 'cases'], true) === false)
|
||||||
|
&& realpath($method->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
|
||||||
|
&& $method->getDocComment() === false,
|
||||||
|
) === [],
|
||||||
|
'to have methods with documentation / annotations',
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target have all properties documented.
|
||||||
|
*/
|
||||||
|
public function toHavePropertiesDocumented(): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this,
|
||||||
|
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|
||||||
|
|| array_filter(
|
||||||
|
Reflection::getPropertiesFromReflectionClass($object->reflectionClass),
|
||||||
|
fn (ReflectionProperty $property): bool => (enum_exists($object->name) === false || in_array($property->name, ['value', 'name'], true) === false)
|
||||||
|
&& realpath($property->getDeclaringClass()->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
|
||||||
|
&& $property->isPromoted() === false
|
||||||
|
&& $property->getDocComment() === false,
|
||||||
|
) === [],
|
||||||
|
'to have properties with documentation / annotations',
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target use the "declare(strict_types=1)" declaration.
|
* Asserts that the given expectation target use the "declare(strict_types=1)" declaration.
|
||||||
*/
|
*/
|
||||||
@ -434,7 +509,7 @@ final class Expectation
|
|||||||
{
|
{
|
||||||
return Targeted::make(
|
return Targeted::make(
|
||||||
$this,
|
$this,
|
||||||
fn (ObjectDescription $object): bool => str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'),
|
fn (ObjectDescription $object): bool => (bool) preg_match('/^<\?php\s+declare\(.*?strict_types\s?=\s?1.*?\);/', (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')),
|
||||||
);
|
);
|
||||||
@ -502,17 +577,79 @@ final class Expectation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target has a specific method.
|
* Asserts that the given expectation target has a specific method.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $method
|
||||||
*/
|
*/
|
||||||
public function toHaveMethod(string $method): ArchExpectation
|
public function toHaveMethod(array|string $method): ArchExpectation
|
||||||
{
|
{
|
||||||
|
$methods = is_array($method) ? $method : [$method];
|
||||||
|
|
||||||
return Targeted::make(
|
return Targeted::make(
|
||||||
$this,
|
$this,
|
||||||
fn (ObjectDescription $object): bool => $object->reflectionClass->hasMethod($method),
|
fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => $object->reflectionClass->hasMethod($method))) === count($methods),
|
||||||
'to have method',
|
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')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target has a specific methods.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $methods
|
||||||
|
*/
|
||||||
|
public function toHaveMethods(array $methods): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toHaveMethod($methods);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not supported.
|
||||||
|
*/
|
||||||
|
public function toHavePublicMethodsBesides(): void
|
||||||
|
{
|
||||||
|
throw InvalidExpectation::fromMethods(['toHavePublicMethodsBesides']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not supported.
|
||||||
|
*/
|
||||||
|
public function toHavePublicMethods(): void
|
||||||
|
{
|
||||||
|
throw InvalidExpectation::fromMethods(['toHavePublicMethods']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not supported.
|
||||||
|
*/
|
||||||
|
public function toHaveProtectedMethodsBesides(): void
|
||||||
|
{
|
||||||
|
throw InvalidExpectation::fromMethods(['toHaveProtectedMethodsBesides']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not supported.
|
||||||
|
*/
|
||||||
|
public function toHaveProtectedMethods(): void
|
||||||
|
{
|
||||||
|
throw InvalidExpectation::fromMethods(['toHaveProtectedMethods']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not supported.
|
||||||
|
*/
|
||||||
|
public function toHavePrivateMethodsBesides(): void
|
||||||
|
{
|
||||||
|
throw InvalidExpectation::fromMethods(['toHavePrivateMethodsBesides']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not supported.
|
||||||
|
*/
|
||||||
|
public function toHavePrivateMethods(): void
|
||||||
|
{
|
||||||
|
throw InvalidExpectation::fromMethods(['toHavePrivateMethods']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target is enum.
|
* Asserts that the given expectation target is enum.
|
||||||
*/
|
*/
|
||||||
@ -578,8 +715,6 @@ final class Expectation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target to be subclass of the given class.
|
* Asserts that the given expectation target to be subclass of the given class.
|
||||||
*
|
|
||||||
* @param class-string $class
|
|
||||||
*/
|
*/
|
||||||
public function toExtend(string $class): ArchExpectation
|
public function toExtend(string $class): ArchExpectation
|
||||||
{
|
{
|
||||||
@ -604,6 +739,39 @@ final class Expectation
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target to use the given trait.
|
||||||
|
*/
|
||||||
|
public function toUseTrait(string $trait): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toUseTraits($trait);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target to use the given traits.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $traits
|
||||||
|
*/
|
||||||
|
public function toUseTraits(array|string $traits): ArchExpectation
|
||||||
|
{
|
||||||
|
$traits = is_array($traits) ? $traits : [$traits];
|
||||||
|
|
||||||
|
return Targeted::make(
|
||||||
|
$this,
|
||||||
|
function (ObjectDescription $object) use ($traits): bool {
|
||||||
|
foreach ($traits as $trait) {
|
||||||
|
if (! in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
"to use traits '".implode("', '", $traits)."'",
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target to not implement any interfaces.
|
* Asserts that the given expectation target to not implement any interfaces.
|
||||||
*/
|
*/
|
||||||
@ -620,7 +788,7 @@ final class Expectation
|
|||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target to only implement the given interfaces.
|
* Asserts that the given expectation target to only implement the given interfaces.
|
||||||
*
|
*
|
||||||
* @param array<int, class-string>|class-string $interfaces
|
* @param array<int, string>|string $interfaces
|
||||||
*/
|
*/
|
||||||
public function toOnlyImplement(array|string $interfaces): ArchExpectation
|
public function toOnlyImplement(array|string $interfaces): ArchExpectation
|
||||||
{
|
{
|
||||||
@ -664,7 +832,7 @@ final class Expectation
|
|||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target to implement the given interfaces.
|
* Asserts that the given expectation target to implement the given interfaces.
|
||||||
*
|
*
|
||||||
* @param array<int, class-string>|class-string $interfaces
|
* @param array<int, string>|string $interfaces
|
||||||
*/
|
*/
|
||||||
public function toImplement(array|string $interfaces): ArchExpectation
|
public function toImplement(array|string $interfaces): ArchExpectation
|
||||||
{
|
{
|
||||||
@ -704,7 +872,10 @@ final class Expectation
|
|||||||
return ToUseNothing::make($this);
|
return ToUseNothing::make($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toBeUsed(): never
|
/**
|
||||||
|
* Not supported.
|
||||||
|
*/
|
||||||
|
public function toBeUsed(): void
|
||||||
{
|
{
|
||||||
throw InvalidExpectation::fromMethods(['toBeUsed']);
|
throw InvalidExpectation::fromMethods(['toBeUsed']);
|
||||||
}
|
}
|
||||||
@ -848,8 +1019,6 @@ final class Expectation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target to have the given attribute.
|
* Asserts that the given expectation target to have the given attribute.
|
||||||
*
|
|
||||||
* @param class-string<Attribute> $attribute
|
|
||||||
*/
|
*/
|
||||||
public function toHaveAttribute(string $attribute): ArchExpectation
|
public function toHaveAttribute(string $attribute): ArchExpectation
|
||||||
{
|
{
|
||||||
@ -876,4 +1045,51 @@ final class Expectation
|
|||||||
{
|
{
|
||||||
return $this->toHaveMethod('__destruct');
|
return $this->toHaveMethod('__destruct');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target is a backed enum of given type.
|
||||||
|
*/
|
||||||
|
private function toBeBackedEnum(string $backingType): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this,
|
||||||
|
fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum()
|
||||||
|
&& (new ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
|
||||||
|
&& (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line
|
||||||
|
'to be '.$backingType.' backed enum',
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation targets are string backed enums.
|
||||||
|
*/
|
||||||
|
public function toBeStringBackedEnums(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toBeStringBackedEnum();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation targets are int backed enums.
|
||||||
|
*/
|
||||||
|
public function toBeIntBackedEnums(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toBeIntBackedEnum();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target is a string backed enum.
|
||||||
|
*/
|
||||||
|
public function toBeStringBackedEnum(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toBeBackedEnum('string');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target is an int backed enum.
|
||||||
|
*/
|
||||||
|
public function toBeIntBackedEnum(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toBeBackedEnum('int');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,9 @@ use function expect;
|
|||||||
*/
|
*/
|
||||||
final class EachExpectation
|
final class EachExpectation
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Indicates if the expectation is the opposite.
|
||||||
|
*/
|
||||||
private bool $opposite = false;
|
private bool $opposite = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,9 +27,7 @@ final class EachExpectation
|
|||||||
*
|
*
|
||||||
* @param Expectation<TValue> $original
|
* @param Expectation<TValue> $original
|
||||||
*/
|
*/
|
||||||
public function __construct(private readonly Expectation $original)
|
public function __construct(private readonly Expectation $original) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new expectation.
|
* Creates a new expectation.
|
||||||
|
|||||||
@ -25,8 +25,14 @@ final class HigherOrderExpectation
|
|||||||
*/
|
*/
|
||||||
private Expectation|EachExpectation $expectation;
|
private Expectation|EachExpectation $expectation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the expectation is the opposite.
|
||||||
|
*/
|
||||||
private bool $opposite = false;
|
private bool $opposite = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the expectation should reset the value.
|
||||||
|
*/
|
||||||
private bool $shouldReset = false;
|
private bool $shouldReset = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -18,9 +18,13 @@ use Pest\Exceptions\InvalidExpectation;
|
|||||||
use Pest\Expectation;
|
use Pest\Expectation;
|
||||||
use Pest\Support\Arr;
|
use Pest\Support\Arr;
|
||||||
use Pest\Support\Exporter;
|
use Pest\Support\Exporter;
|
||||||
|
use Pest\Support\Reflection;
|
||||||
use PHPUnit\Architecture\Elements\ObjectDescription;
|
use PHPUnit\Architecture\Elements\ObjectDescription;
|
||||||
use PHPUnit\Framework\AssertionFailedError;
|
use PHPUnit\Framework\AssertionFailedError;
|
||||||
use PHPUnit\Framework\ExpectationFailedException;
|
use PHPUnit\Framework\ExpectationFailedException;
|
||||||
|
use ReflectionMethod;
|
||||||
|
use ReflectionProperty;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@ -29,16 +33,14 @@ use PHPUnit\Framework\ExpectationFailedException;
|
|||||||
*
|
*
|
||||||
* @mixin Expectation<TValue>
|
* @mixin Expectation<TValue>
|
||||||
*/
|
*/
|
||||||
final class OppositeExpectation
|
final readonly class OppositeExpectation
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Creates a new opposite expectation.
|
* Creates a new opposite expectation.
|
||||||
*
|
*
|
||||||
* @param Expectation<TValue> $original
|
* @param Expectation<TValue> $original
|
||||||
*/
|
*/
|
||||||
public function __construct(private readonly Expectation $original)
|
public function __construct(private Expectation $original) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the value array not has the provided $keys.
|
* Asserts that the value array not has the provided $keys.
|
||||||
@ -77,6 +79,66 @@ final class OppositeExpectation
|
|||||||
), is_string($targets) ? [$targets] : $targets));
|
), is_string($targets) ? [$targets] : $targets));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target does not have the given permissions
|
||||||
|
*/
|
||||||
|
public function toHaveFileSystemPermissions(string $permissions): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this->original,
|
||||||
|
fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) !== $permissions,
|
||||||
|
sprintf('permissions not to be [%s]', $permissions),
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not supported.
|
||||||
|
*/
|
||||||
|
public function toHaveLineCountLessThan(): ArchExpectation
|
||||||
|
{
|
||||||
|
throw InvalidExpectation::fromMethods(['not', 'toHaveLineCountLessThan']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not supported.
|
||||||
|
*/
|
||||||
|
public function toHaveMethodsDocumented(): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this->original,
|
||||||
|
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|
||||||
|
|| array_filter(
|
||||||
|
Reflection::getMethodsFromReflectionClass($object->reflectionClass),
|
||||||
|
fn (ReflectionMethod $method): bool => (enum_exists($object->name) === false || in_array($method->name, ['from', 'tryFrom', 'cases'], true) === false)
|
||||||
|
&& realpath($method->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
|
||||||
|
&& $method->getDocComment() !== false,
|
||||||
|
) === [],
|
||||||
|
'to have methods without documentation / annotations',
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not supported.
|
||||||
|
*/
|
||||||
|
public function toHavePropertiesDocumented(): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this->original,
|
||||||
|
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|
||||||
|
|| array_filter(
|
||||||
|
Reflection::getPropertiesFromReflectionClass($object->reflectionClass),
|
||||||
|
fn (ReflectionProperty $property): bool => (enum_exists($object->name) === false || in_array($property->name, ['value', 'name'], true) === false)
|
||||||
|
&& realpath($property->getDeclaringClass()->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
|
||||||
|
&& $property->isPromoted() === false
|
||||||
|
&& $property->getDocComment() !== false,
|
||||||
|
) === [],
|
||||||
|
'to have properties without documentation / annotations',
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target does not use the "declare(strict_types=1)" declaration.
|
* Asserts that the given expectation target does not use the "declare(strict_types=1)" declaration.
|
||||||
*/
|
*/
|
||||||
@ -84,7 +146,7 @@ final class OppositeExpectation
|
|||||||
{
|
{
|
||||||
return Targeted::make(
|
return Targeted::make(
|
||||||
$this->original,
|
$this->original,
|
||||||
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'),
|
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')),
|
||||||
);
|
);
|
||||||
@ -152,17 +214,163 @@ final class OppositeExpectation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target does not have a specific method.
|
* Asserts that the given expectation target does not have a specific method.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $method
|
||||||
*/
|
*/
|
||||||
public function toHaveMethod(string $method): ArchExpectation
|
public function toHaveMethod(array|string $method): ArchExpectation
|
||||||
{
|
{
|
||||||
|
$methods = is_array($method) ? $method : [$method];
|
||||||
|
|
||||||
return Targeted::make(
|
return Targeted::make(
|
||||||
$this->original,
|
$this->original,
|
||||||
fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod($method),
|
fn (ObjectDescription $object): bool => array_filter(
|
||||||
'to not have method',
|
$methods,
|
||||||
|
fn (string $method): bool => $object->reflectionClass->hasMethod($method),
|
||||||
|
) === [],
|
||||||
|
'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 the given methods.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $methods
|
||||||
|
*/
|
||||||
|
public function toHaveMethods(array $methods): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toHaveMethod($methods);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target not to have the public methods besides the given methods.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $methods
|
||||||
|
*/
|
||||||
|
public function toHavePublicMethodsBesides(array|string $methods): ArchExpectation
|
||||||
|
{
|
||||||
|
$methods = is_array($methods) ? $methods : [$methods];
|
||||||
|
|
||||||
|
$state = new stdClass;
|
||||||
|
|
||||||
|
return Targeted::make(
|
||||||
|
$this->original,
|
||||||
|
function (ObjectDescription $object) use ($methods, &$state): bool {
|
||||||
|
$reflectionMethods = isset($object->reflectionClass)
|
||||||
|
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PUBLIC)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
foreach ($reflectionMethods as $reflectionMethod) {
|
||||||
|
if (! in_array($reflectionMethod->name, $methods, true)) {
|
||||||
|
$state->contains = 'public function '.$reflectionMethod->name;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
$methods === []
|
||||||
|
? 'not to have public methods'
|
||||||
|
: sprintf("not to have public methods besides '%s'", implode("', '", $methods)),
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target not to have the public methods.
|
||||||
|
*/
|
||||||
|
public function toHavePublicMethods(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toHavePublicMethodsBesides([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target not to have the protected methods besides the given methods.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $methods
|
||||||
|
*/
|
||||||
|
public function toHaveProtectedMethodsBesides(array|string $methods): ArchExpectation
|
||||||
|
{
|
||||||
|
$methods = is_array($methods) ? $methods : [$methods];
|
||||||
|
|
||||||
|
$state = new stdClass;
|
||||||
|
|
||||||
|
return Targeted::make(
|
||||||
|
$this->original,
|
||||||
|
function (ObjectDescription $object) use ($methods, &$state): bool {
|
||||||
|
$reflectionMethods = isset($object->reflectionClass)
|
||||||
|
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PROTECTED)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
foreach ($reflectionMethods as $reflectionMethod) {
|
||||||
|
if (! in_array($reflectionMethod->name, $methods, true)) {
|
||||||
|
$state->contains = 'protected function '.$reflectionMethod->name;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
$methods === []
|
||||||
|
? 'not to have protected methods'
|
||||||
|
: sprintf("not to have protected methods besides '%s'", implode("', '", $methods)),
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target not to have the protected methods.
|
||||||
|
*/
|
||||||
|
public function toHaveProtectedMethods(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toHaveProtectedMethodsBesides([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target not to have the private methods besides the given methods.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $methods
|
||||||
|
*/
|
||||||
|
public function toHavePrivateMethodsBesides(array|string $methods): ArchExpectation
|
||||||
|
{
|
||||||
|
$methods = is_array($methods) ? $methods : [$methods];
|
||||||
|
|
||||||
|
$state = new stdClass;
|
||||||
|
|
||||||
|
return Targeted::make(
|
||||||
|
$this->original,
|
||||||
|
function (ObjectDescription $object) use ($methods, &$state): bool {
|
||||||
|
$reflectionMethods = isset($object->reflectionClass)
|
||||||
|
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PRIVATE)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
foreach ($reflectionMethods as $reflectionMethod) {
|
||||||
|
if (! in_array($reflectionMethod->name, $methods, true)) {
|
||||||
|
$state->contains = 'private function '.$reflectionMethod->name;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
$methods === []
|
||||||
|
? 'not to have private methods'
|
||||||
|
: sprintf("not to have private methods besides '%s'", implode("', '", $methods)),
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target not to have the private methods.
|
||||||
|
*/
|
||||||
|
public function toHavePrivateMethods(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toHavePrivateMethodsBesides([]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target is not enum.
|
* Asserts that the given expectation target is not enum.
|
||||||
*/
|
*/
|
||||||
@ -228,8 +436,6 @@ final class OppositeExpectation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target to be not subclass of the given class.
|
* Asserts that the given expectation target to be not subclass of the given class.
|
||||||
*
|
|
||||||
* @param class-string $class
|
|
||||||
*/
|
*/
|
||||||
public function toExtend(string $class): ArchExpectation
|
public function toExtend(string $class): ArchExpectation
|
||||||
{
|
{
|
||||||
@ -254,10 +460,43 @@ final class OppositeExpectation
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target not to use the given trait.
|
||||||
|
*/
|
||||||
|
public function toUseTrait(string $trait): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toUseTraits($trait);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target not to use the given traits.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $traits
|
||||||
|
*/
|
||||||
|
public function toUseTraits(array|string $traits): ArchExpectation
|
||||||
|
{
|
||||||
|
$traits = is_array($traits) ? $traits : [$traits];
|
||||||
|
|
||||||
|
return Targeted::make(
|
||||||
|
$this->original,
|
||||||
|
function (ObjectDescription $object) use ($traits): bool {
|
||||||
|
foreach ($traits as $trait) {
|
||||||
|
if (in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
"not to use traits '".implode("', '", $traits)."'",
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target not to implement the given interfaces.
|
* Asserts that the given expectation target not to implement the given interfaces.
|
||||||
*
|
*
|
||||||
* @param array<int, class-string>|string $interfaces
|
* @param array<int, string>|string $interfaces
|
||||||
*/
|
*/
|
||||||
public function toImplement(array|string $interfaces): ArchExpectation
|
public function toImplement(array|string $interfaces): ArchExpectation
|
||||||
{
|
{
|
||||||
@ -294,10 +533,8 @@ final class OppositeExpectation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Not supported.
|
* Not supported.
|
||||||
*
|
|
||||||
* @param array<int, class-string>|string $interfaces
|
|
||||||
*/
|
*/
|
||||||
public function toOnlyImplement(array|string $interfaces): never
|
public function toOnlyImplement(): void
|
||||||
{
|
{
|
||||||
throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']);
|
throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']);
|
||||||
}
|
}
|
||||||
@ -330,10 +567,8 @@ final class OppositeExpectation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Not supported.
|
* Not supported.
|
||||||
*
|
|
||||||
* @param array<int, string>|string $targets
|
|
||||||
*/
|
*/
|
||||||
public function toOnlyUse(array|string $targets): never
|
public function toOnlyUse(): void
|
||||||
{
|
{
|
||||||
throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']);
|
throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']);
|
||||||
}
|
}
|
||||||
@ -341,7 +576,7 @@ final class OppositeExpectation
|
|||||||
/**
|
/**
|
||||||
* Not supported.
|
* Not supported.
|
||||||
*/
|
*/
|
||||||
public function toUseNothing(): never
|
public function toUseNothing(): void
|
||||||
{
|
{
|
||||||
throw InvalidExpectation::fromMethods(['not', 'toUseNothing']);
|
throw InvalidExpectation::fromMethods(['not', 'toUseNothing']);
|
||||||
}
|
}
|
||||||
@ -366,7 +601,7 @@ final class OppositeExpectation
|
|||||||
), is_string($targets) ? [$targets] : $targets));
|
), is_string($targets) ? [$targets] : $targets));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toOnlyBeUsedIn(): never
|
public function toOnlyBeUsedIn(): void
|
||||||
{
|
{
|
||||||
throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedIn']);
|
throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedIn']);
|
||||||
}
|
}
|
||||||
@ -374,7 +609,7 @@ final class OppositeExpectation
|
|||||||
/**
|
/**
|
||||||
* Asserts that the given expectation dependency is not used.
|
* Asserts that the given expectation dependency is not used.
|
||||||
*/
|
*/
|
||||||
public function toBeUsedInNothing(): never
|
public function toBeUsedInNothing(): void
|
||||||
{
|
{
|
||||||
throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']);
|
throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']);
|
||||||
}
|
}
|
||||||
@ -394,8 +629,6 @@ final class OppositeExpectation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target not to have the given attribute.
|
* Asserts that the given expectation target not to have the given attribute.
|
||||||
*
|
|
||||||
* @param class-string<Attribute> $attribute
|
|
||||||
*/
|
*/
|
||||||
public function toHaveAttribute(string $attribute): ArchExpectation
|
public function toHaveAttribute(string $attribute): ArchExpectation
|
||||||
{
|
{
|
||||||
@ -485,4 +718,51 @@ final class OppositeExpectation
|
|||||||
{
|
{
|
||||||
return $this->toHaveMethod('__destruct');
|
return $this->toHaveMethod('__destruct');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target is not a backed enum of given type.
|
||||||
|
*/
|
||||||
|
private function toBeBackedEnum(string $backingType): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this->original,
|
||||||
|
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum()
|
||||||
|
|| ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
|
||||||
|
|| (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line
|
||||||
|
'not to be '.$backingType.' backed enum',
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation targets are not string backed enums.
|
||||||
|
*/
|
||||||
|
public function toBeStringBackedEnums(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toBeStringBackedEnum();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation targets are not int backed enums.
|
||||||
|
*/
|
||||||
|
public function toBeIntBackedEnums(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toBeIntBackedEnum();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target is not a string backed enum.
|
||||||
|
*/
|
||||||
|
public function toBeStringBackedEnum(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toBeBackedEnum('string');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target is not an int backed enum.
|
||||||
|
*/
|
||||||
|
public function toBeIntBackedEnum(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toBeBackedEnum('int');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Factories\Annotations;
|
|
||||||
|
|
||||||
use Pest\Contracts\AddsAnnotations;
|
|
||||||
use Pest\Factories\Covers\CoversNothing as CoversNothingFactory;
|
|
||||||
use Pest\Factories\TestCaseMethodFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class CoversNothing implements AddsAnnotations
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
|
|
||||||
{
|
|
||||||
if (($method->covers[0] ?? null) instanceof CoversNothingFactory) {
|
|
||||||
$annotations[] = '@coversNothing';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $annotations;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Factories\Annotations;
|
|
||||||
|
|
||||||
use Pest\Contracts\AddsAnnotations;
|
|
||||||
use Pest\Factories\TestCaseMethodFactory;
|
|
||||||
use Pest\Support\Str;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class Depends implements AddsAnnotations
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
|
|
||||||
{
|
|
||||||
foreach ($method->depends as $depend) {
|
|
||||||
$depend = Str::evaluable($method->describing !== null ? Str::describe($method->describing, $depend) : $depend);
|
|
||||||
|
|
||||||
$annotations[] = "@depends $depend";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $annotations;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Factories\Annotations;
|
|
||||||
|
|
||||||
use Pest\Contracts\AddsAnnotations;
|
|
||||||
use Pest\Factories\TestCaseMethodFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class Groups implements AddsAnnotations
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
|
|
||||||
{
|
|
||||||
foreach ($method->groups as $group) {
|
|
||||||
$annotations[] = "@group $group";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $annotations;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Factories\Annotations;
|
|
||||||
|
|
||||||
use Pest\Contracts\AddsAnnotations;
|
|
||||||
use Pest\Factories\TestCaseMethodFactory;
|
|
||||||
|
|
||||||
final class TestDox implements AddsAnnotations
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* Escapes docblock according to
|
|
||||||
* https://manual.phpdoc.org/HTMLframesConverter/default/phpDocumentor/tutorial_phpDocumentor.howto.pkg.html#basics.desc
|
|
||||||
*
|
|
||||||
* Note: '@' escaping is not needed as it cannot be the first character of the line (it always starts with @testdox).
|
|
||||||
*/
|
|
||||||
assert($method->description !== null);
|
|
||||||
$methodDescription = str_replace('*/', '{@*}', $method->description);
|
|
||||||
|
|
||||||
$annotations[] = "@testdox $methodDescription";
|
|
||||||
|
|
||||||
return $annotations;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
src/Factories/Attribute.php
Normal file
19
src/Factories/Attribute.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Factories;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Attribute
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param iterable<int, string> $arguments
|
||||||
|
*/
|
||||||
|
public function __construct(public string $name, public iterable $arguments)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Factories\Attributes;
|
|
||||||
|
|
||||||
use Pest\Factories\TestCaseMethodFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
abstract class Attribute
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Determine if the attribute should be placed above the class instead of above the method.
|
|
||||||
*/
|
|
||||||
public static bool $above = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<int, string> $attributes
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
public function __invoke(TestCaseMethodFactory $method, array $attributes): array // @phpstan-ignore-line
|
|
||||||
{
|
|
||||||
return $attributes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Factories\Attributes;
|
|
||||||
|
|
||||||
use Pest\Factories\Covers\CoversClass;
|
|
||||||
use Pest\Factories\Covers\CoversFunction;
|
|
||||||
use Pest\Factories\TestCaseMethodFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class Covers extends Attribute
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Determine if the attribute should be placed above the class instead of above the method.
|
|
||||||
*/
|
|
||||||
public static bool $above = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds attributes regarding the "covers" feature.
|
|
||||||
*
|
|
||||||
* @param array<int, string> $attributes
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
public function __invoke(TestCaseMethodFactory $method, array $attributes): array
|
|
||||||
{
|
|
||||||
foreach ($method->covers as $covering) {
|
|
||||||
if ($covering instanceof CoversClass) {
|
|
||||||
// Prepend a backslash for FQN classes
|
|
||||||
if (str_contains($covering->class, '\\')) {
|
|
||||||
$covering->class = '\\'.$covering->class;
|
|
||||||
}
|
|
||||||
|
|
||||||
$attributes[] = "#[\PHPUnit\Framework\Attributes\CoversClass({$covering->class}::class)]";
|
|
||||||
} elseif ($covering instanceof CoversFunction) {
|
|
||||||
$attributes[] = "#[\PHPUnit\Framework\Attributes\CoversFunction('{$covering->function}')]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $attributes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -28,8 +28,8 @@ trait HigherOrderable
|
|||||||
*/
|
*/
|
||||||
private function bootHigherOrderable(): void
|
private function bootHigherOrderable(): void
|
||||||
{
|
{
|
||||||
$this->chains = new HigherOrderMessageCollection();
|
$this->chains = new HigherOrderMessageCollection;
|
||||||
$this->factoryProxies = new HigherOrderMessageCollection();
|
$this->factoryProxies = new HigherOrderMessageCollection;
|
||||||
$this->proxies = new HigherOrderMessageCollection();
|
$this->proxies = new HigherOrderMessageCollection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,5 @@ namespace Pest\Factories\Covers;
|
|||||||
*/
|
*/
|
||||||
final class CoversClass
|
final class CoversClass
|
||||||
{
|
{
|
||||||
public function __construct(public string $class)
|
public function __construct(public string $class) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,5 @@ namespace Pest\Factories\Covers;
|
|||||||
*/
|
*/
|
||||||
final class CoversFunction
|
final class CoversFunction
|
||||||
{
|
{
|
||||||
public function __construct(public string $function)
|
public function __construct(public string $function) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,4 @@ namespace Pest\Factories\Covers;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class CoversNothing
|
final class CoversNothing {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|||||||
@ -6,11 +6,12 @@ namespace Pest\Factories;
|
|||||||
|
|
||||||
use ParseError;
|
use ParseError;
|
||||||
use Pest\Concerns;
|
use Pest\Concerns;
|
||||||
use Pest\Contracts\AddsAnnotations;
|
|
||||||
use Pest\Contracts\HasPrintableTestCaseName;
|
use Pest\Contracts\HasPrintableTestCaseName;
|
||||||
|
use Pest\Evaluators\Attributes;
|
||||||
use Pest\Exceptions\DatasetMissing;
|
use Pest\Exceptions\DatasetMissing;
|
||||||
use Pest\Exceptions\ShouldNotHappen;
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
use Pest\Exceptions\TestAlreadyExist;
|
use Pest\Exceptions\TestAlreadyExist;
|
||||||
|
use Pest\Exceptions\TestClosureMustNotBeStatic;
|
||||||
use Pest\Exceptions\TestDescriptionMissing;
|
use Pest\Exceptions\TestDescriptionMissing;
|
||||||
use Pest\Factories\Concerns\HigherOrderable;
|
use Pest\Factories\Concerns\HigherOrderable;
|
||||||
use Pest\Support\Reflection;
|
use Pest\Support\Reflection;
|
||||||
@ -26,26 +27,12 @@ final class TestCaseFactory
|
|||||||
{
|
{
|
||||||
use HigherOrderable;
|
use HigherOrderable;
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of annotations.
|
|
||||||
*
|
|
||||||
* @var array<int, class-string<AddsAnnotations>>
|
|
||||||
*/
|
|
||||||
private const ANNOTATIONS = [
|
|
||||||
Annotations\Depends::class,
|
|
||||||
Annotations\Groups::class,
|
|
||||||
Annotations\CoversNothing::class,
|
|
||||||
Annotations\TestDox::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of attributes.
|
* The list of attributes.
|
||||||
*
|
*
|
||||||
* @var array<int, class-string<\Pest\Factories\Attributes\Attribute>>
|
* @var array<int, Attribute>
|
||||||
*/
|
*/
|
||||||
private const ATTRIBUTES = [
|
public array $attributes = [];
|
||||||
Attributes\Covers::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The FQN of the Test Case class.
|
* The FQN of the Test Case class.
|
||||||
@ -146,32 +133,21 @@ final class TestCaseFactory
|
|||||||
$className = 'InvalidTestName'.Str::random();
|
$className = 'InvalidTestName'.Str::random();
|
||||||
}
|
}
|
||||||
|
|
||||||
$classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => $attribute::$above);
|
$this->attributes = [
|
||||||
$methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => ! $attribute::$above);
|
new Attribute(
|
||||||
|
\PHPUnit\Framework\Attributes\TestDox::class,
|
||||||
|
[$this->filename],
|
||||||
|
),
|
||||||
|
...$this->attributes,
|
||||||
|
];
|
||||||
|
|
||||||
$classAttributes = [];
|
$attributesCode = Attributes::code($this->attributes);
|
||||||
|
|
||||||
foreach ($classAvailableAttributes as $attribute) {
|
|
||||||
$classAttributes = array_reduce(
|
|
||||||
$methods,
|
|
||||||
fn (array $carry, TestCaseMethodFactory $methodFactory): array => (new $attribute())->__invoke($methodFactory, $carry),
|
|
||||||
$classAttributes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$methodsCode = implode('', array_map(
|
$methodsCode = implode('', array_map(
|
||||||
fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(
|
fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(),
|
||||||
self::ANNOTATIONS,
|
|
||||||
$methodAvailableAttributes
|
|
||||||
),
|
|
||||||
$methods
|
$methods
|
||||||
));
|
));
|
||||||
|
|
||||||
$classAttributesCode = implode('', array_map(
|
|
||||||
static fn (string $attribute): string => sprintf("\n%s", $attribute),
|
|
||||||
array_unique($classAttributes),
|
|
||||||
));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$classCode = <<<PHP
|
$classCode = <<<PHP
|
||||||
namespace $namespace;
|
namespace $namespace;
|
||||||
@ -179,10 +155,7 @@ final class TestCaseFactory
|
|||||||
use Pest\Repositories\DatasetsRepository as __PestDatasets;
|
use Pest\Repositories\DatasetsRepository as __PestDatasets;
|
||||||
use Pest\TestSuite as __PestTestSuite;
|
use Pest\TestSuite as __PestTestSuite;
|
||||||
|
|
||||||
/**
|
$attributesCode
|
||||||
* @testdox $filename
|
|
||||||
*/
|
|
||||||
$classAttributesCode
|
|
||||||
#[\AllowDynamicProperties]
|
#[\AllowDynamicProperties]
|
||||||
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
|
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
|
||||||
$traitsCode
|
$traitsCode
|
||||||
@ -216,6 +189,14 @@ final class TestCaseFactory
|
|||||||
throw new TestAlreadyExist($method->filename, $method->description);
|
throw new TestAlreadyExist($method->filename, $method->description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$method->closure instanceof \Closure &&
|
||||||
|
(new \ReflectionFunction($method->closure))->isStatic()
|
||||||
|
) {
|
||||||
|
|
||||||
|
throw new TestClosureMustNotBeStatic($method);
|
||||||
|
}
|
||||||
|
|
||||||
if (! $method->receivesArguments()) {
|
if (! $method->receivesArguments()) {
|
||||||
if (! $method->closure instanceof \Closure) {
|
if (! $method->closure instanceof \Closure) {
|
||||||
throw ShouldNotHappen::fromMessage('The test closure may not be empty.');
|
throw ShouldNotHappen::fromMessage('The test closure may not be empty.');
|
||||||
|
|||||||
@ -5,13 +5,14 @@ declare(strict_types=1);
|
|||||||
namespace Pest\Factories;
|
namespace Pest\Factories;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Pest\Contracts\AddsAnnotations;
|
use Pest\Evaluators\Attributes;
|
||||||
use Pest\Exceptions\ShouldNotHappen;
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
use Pest\Factories\Concerns\HigherOrderable;
|
use Pest\Factories\Concerns\HigherOrderable;
|
||||||
use Pest\Repositories\DatasetsRepository;
|
use Pest\Repositories\DatasetsRepository;
|
||||||
use Pest\Support\Str;
|
use Pest\Support\Str;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use PHPUnit\Framework\Assert;
|
use PHPUnit\Framework\Assert;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,11 +22,23 @@ final class TestCaseMethodFactory
|
|||||||
{
|
{
|
||||||
use HigherOrderable;
|
use HigherOrderable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of attributes.
|
||||||
|
*
|
||||||
|
* @var array<int, Attribute>
|
||||||
|
*/
|
||||||
|
public array $attributes = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's describing, if any.
|
* The test's describing, if any.
|
||||||
*/
|
*/
|
||||||
public ?string $describing = null;
|
public ?string $describing = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test's description, if any.
|
||||||
|
*/
|
||||||
|
public ?string $description = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's number of repetitions.
|
* The test's number of repetitions.
|
||||||
*/
|
*/
|
||||||
@ -36,6 +49,34 @@ final class TestCaseMethodFactory
|
|||||||
*/
|
*/
|
||||||
public bool $todo = false;
|
public bool $todo = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The associated issue numbers.
|
||||||
|
*
|
||||||
|
* @var array<int, int>
|
||||||
|
*/
|
||||||
|
public array $issues = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test assignees.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
public array $assignees = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The associated PRs numbers.
|
||||||
|
*
|
||||||
|
* @var array<int, int>
|
||||||
|
*/
|
||||||
|
public array $prs = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test's notes.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
public array $notes = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's datasets.
|
* The test's datasets.
|
||||||
*
|
*
|
||||||
@ -58,31 +99,28 @@ final class TestCaseMethodFactory
|
|||||||
public array $groups = [];
|
public array $groups = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The covered classes and functions.
|
* @see This property is not actually used in the codebase, it's only here to make Rector happy.
|
||||||
*
|
|
||||||
* @var array<int, \Pest\Factories\Covers\CoversClass|\Pest\Factories\Covers\CoversFunction|\Pest\Factories\Covers\CoversNothing>
|
|
||||||
*/
|
*/
|
||||||
public array $covers = [];
|
public bool $__ran = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new test case method factory instance.
|
* Creates a new test case method factory instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $filename,
|
public string $filename,
|
||||||
public ?string $description,
|
|
||||||
public ?Closure $closure,
|
public ?Closure $closure,
|
||||||
) {
|
) {
|
||||||
$this->closure ??= function (): void {
|
$this->closure ??= function (): void {
|
||||||
Assert::getCount() > 0 ?: self::markTestIncomplete(); // @phpstan-ignore-line
|
(Assert::getCount() > 0 || $this->doesNotPerformAssertions()) ?: self::markTestIncomplete(); // @phpstan-ignore-line
|
||||||
};
|
};
|
||||||
|
|
||||||
$this->bootHigherOrderable();
|
$this->bootHigherOrderable();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the test's closure.
|
* Sets the test's hooks, and runs any proxy to the test case.
|
||||||
*/
|
*/
|
||||||
public function getClosure(TestCase $concrete): Closure
|
public function setUp(TestCase $concrete): void
|
||||||
{
|
{
|
||||||
$concrete::flush(); // @phpstan-ignore-line
|
$concrete::flush(); // @phpstan-ignore-line
|
||||||
|
|
||||||
@ -90,16 +128,32 @@ final class TestCaseMethodFactory
|
|||||||
throw ShouldNotHappen::fromMessage('Description can not be empty.');
|
throw ShouldNotHappen::fromMessage('Description can not be empty.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$closure = $this->closure;
|
|
||||||
|
|
||||||
$testCase = TestSuite::getInstance()->tests->get($this->filename);
|
$testCase = TestSuite::getInstance()->tests->get($this->filename);
|
||||||
|
|
||||||
|
assert($testCase instanceof TestCaseFactory);
|
||||||
$testCase->factoryProxies->proxy($concrete);
|
$testCase->factoryProxies->proxy($concrete);
|
||||||
$this->factoryProxies->proxy($concrete);
|
$this->factoryProxies->proxy($concrete);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes the test case.
|
||||||
|
*/
|
||||||
|
public function tearDown(TestCase $concrete): void
|
||||||
|
{
|
||||||
|
$concrete::flush(); // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the test's closure.
|
||||||
|
*/
|
||||||
|
public function getClosure(): Closure
|
||||||
|
{
|
||||||
|
$closure = $this->closure;
|
||||||
|
$testCase = TestSuite::getInstance()->tests->get($this->filename);
|
||||||
|
assert($testCase instanceof TestCaseFactory);
|
||||||
$method = $this;
|
$method = $this;
|
||||||
|
|
||||||
return function () use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line
|
return function (...$arguments) use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line
|
||||||
/* @var TestCase $this */
|
/* @var TestCase $this */
|
||||||
$testCase->proxies->proxy($this);
|
$testCase->proxies->proxy($this);
|
||||||
$method->proxies->proxy($this);
|
$method->proxies->proxy($this);
|
||||||
@ -107,7 +161,9 @@ final class TestCaseMethodFactory
|
|||||||
$testCase->chains->chain($this);
|
$testCase->chains->chain($this);
|
||||||
$method->chains->chain($this);
|
$method->chains->chain($this);
|
||||||
|
|
||||||
return \Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args());
|
$this->__ran = true;
|
||||||
|
|
||||||
|
return \Pest\Support\Closure::bind($closure, $this, self::class)(...$arguments);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,16 +172,13 @@ final class TestCaseMethodFactory
|
|||||||
*/
|
*/
|
||||||
public function receivesArguments(): bool
|
public function receivesArguments(): bool
|
||||||
{
|
{
|
||||||
return $this->datasets !== [] || $this->depends !== [];
|
return $this->datasets !== [] || $this->depends !== [] || $this->repetitions > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a PHPUnit method as a string ready for evaluation.
|
* Creates a PHPUnit method as a string ready for evaluation.
|
||||||
*
|
|
||||||
* @param array<int, class-string<AddsAnnotations>> $annotationsToUse
|
|
||||||
* @param array<int, class-string<\Pest\Factories\Attributes\Attribute>> $attributesToUse
|
|
||||||
*/
|
*/
|
||||||
public function buildForEvaluation(array $annotationsToUse, array $attributesToUse): string
|
public function buildForEvaluation(): string
|
||||||
{
|
{
|
||||||
if ($this->description === null) {
|
if ($this->description === null) {
|
||||||
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
||||||
@ -134,46 +187,49 @@ final class TestCaseMethodFactory
|
|||||||
$methodName = Str::evaluable($this->description);
|
$methodName = Str::evaluable($this->description);
|
||||||
|
|
||||||
$datasetsCode = '';
|
$datasetsCode = '';
|
||||||
$annotations = ['@test'];
|
|
||||||
$attributes = [];
|
|
||||||
|
|
||||||
foreach ($annotationsToUse as $annotation) {
|
$this->attributes = [
|
||||||
$annotations = (new $annotation())->__invoke($this, $annotations);
|
new Attribute(
|
||||||
}
|
\PHPUnit\Framework\Attributes\Test::class,
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
new Attribute(
|
||||||
|
\PHPUnit\Framework\Attributes\TestDox::class,
|
||||||
|
[str_replace('*/', '{@*}', $this->description)],
|
||||||
|
),
|
||||||
|
...$this->attributes,
|
||||||
|
];
|
||||||
|
|
||||||
foreach ($attributesToUse as $attribute) {
|
foreach ($this->depends as $depend) {
|
||||||
$attributes = (new $attribute())->__invoke($this, $attributes);
|
$depend = Str::evaluable($this->describing !== null ? Str::describe($this->describing, $depend) : $depend);
|
||||||
|
|
||||||
|
$this->attributes[] = new Attribute(
|
||||||
|
\PHPUnit\Framework\Attributes\Depends::class,
|
||||||
|
[$depend],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->datasets !== [] || $this->repetitions > 1) {
|
if ($this->datasets !== [] || $this->repetitions > 1) {
|
||||||
$dataProviderName = $methodName.'_dataset';
|
$dataProviderName = $methodName.'_dataset';
|
||||||
$annotations[] = "@dataProvider $dataProviderName";
|
$this->attributes[] = new Attribute(
|
||||||
|
DataProvider::class,
|
||||||
|
[$dataProviderName],
|
||||||
|
);
|
||||||
$datasetsCode = $this->buildDatasetForEvaluation($methodName, $dataProviderName);
|
$datasetsCode = $this->buildDatasetForEvaluation($methodName, $dataProviderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
$annotations = implode('', array_map(
|
$attributesCode = Attributes::code($this->attributes);
|
||||||
static fn (string $annotation): string => sprintf("\n * %s", $annotation), $annotations,
|
|
||||||
));
|
|
||||||
|
|
||||||
$attributes = implode('', array_map(
|
|
||||||
static fn (string $attribute): string => sprintf("\n %s", $attribute), $attributes,
|
|
||||||
));
|
|
||||||
|
|
||||||
return <<<PHP
|
return <<<PHP
|
||||||
|
$attributesCode
|
||||||
/**$annotations
|
public function $methodName(...\$arguments)
|
||||||
*/
|
|
||||||
$attributes
|
|
||||||
public function $methodName()
|
|
||||||
{
|
{
|
||||||
\$test = \Pest\TestSuite::getInstance()->tests->get(self::\$__filename)->getMethod(\$this->name())->getClosure(\$this);
|
|
||||||
|
|
||||||
return \$this->__runTest(
|
return \$this->__runTest(
|
||||||
\$test,
|
\$this->__test,
|
||||||
...func_get_args(),
|
...\$arguments,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
$datasetsCode
|
$datasetsCode
|
||||||
PHP;
|
PHP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,12 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Pest\Concerns\Expectable;
|
use Pest\Concerns\Expectable;
|
||||||
|
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\Mutate\Contracts\MutationTestRunner;
|
||||||
|
use Pest\Mutate\Repositories\ConfigurationRepository;
|
||||||
use Pest\PendingCalls\AfterEachCall;
|
use Pest\PendingCalls\AfterEachCall;
|
||||||
use Pest\PendingCalls\BeforeEachCall;
|
use Pest\PendingCalls\BeforeEachCall;
|
||||||
use Pest\PendingCalls\DescribeCall;
|
use Pest\PendingCalls\DescribeCall;
|
||||||
@ -13,6 +16,7 @@ use Pest\PendingCalls\TestCall;
|
|||||||
use Pest\PendingCalls\UsesCall;
|
use Pest\PendingCalls\UsesCall;
|
||||||
use Pest\Repositories\DatasetsRepository;
|
use Pest\Repositories\DatasetsRepository;
|
||||||
use Pest\Support\Backtrace;
|
use Pest\Support\Backtrace;
|
||||||
|
use Pest\Support\Container;
|
||||||
use Pest\Support\DatasetInfo;
|
use Pest\Support\DatasetInfo;
|
||||||
use Pest\Support\HigherOrderTapProxy;
|
use Pest\Support\HigherOrderTapProxy;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
@ -53,6 +57,8 @@ if (! function_exists('beforeEach')) {
|
|||||||
/**
|
/**
|
||||||
* Runs the given closure before each test in the current file.
|
* Runs the given closure before each test in the current file.
|
||||||
*
|
*
|
||||||
|
* @param-closure-this TestCase $closure
|
||||||
|
*
|
||||||
* @return HigherOrderTapProxy<Expectable|TestCall|TestCase>|Expectable|TestCall|TestCase|mixed
|
* @return HigherOrderTapProxy<Expectable|TestCall|TestCase>|Expectable|TestCall|TestCase|mixed
|
||||||
*/
|
*/
|
||||||
function beforeEach(?Closure $closure = null): BeforeEachCall
|
function beforeEach(?Closure $closure = null): BeforeEachCall
|
||||||
@ -108,12 +114,24 @@ if (! function_exists('uses')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! function_exists('pest')) {
|
||||||
|
/**
|
||||||
|
* Creates a new Pest configuration instance.
|
||||||
|
*/
|
||||||
|
function pest(): Configuration
|
||||||
|
{
|
||||||
|
return new Configuration(Backtrace::file());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (! function_exists('test')) {
|
if (! function_exists('test')) {
|
||||||
/**
|
/**
|
||||||
* Adds the given closure as a test. The first argument
|
* Adds the given closure as a test. The first argument
|
||||||
* is the test description; the second argument is
|
* is the test description; the second argument is
|
||||||
* a closure that contains the test expectations.
|
* a closure that contains the test expectations.
|
||||||
*
|
*
|
||||||
|
* @param-closure-this TestCase $closure
|
||||||
|
*
|
||||||
* @return Expectable|TestCall|TestCase|mixed
|
* @return Expectable|TestCall|TestCase|mixed
|
||||||
*/
|
*/
|
||||||
function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall
|
function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall
|
||||||
@ -134,6 +152,8 @@ if (! function_exists('it')) {
|
|||||||
* is the test description; the second argument is
|
* is the test description; the second argument is
|
||||||
* a closure that contains the test expectations.
|
* a closure that contains the test expectations.
|
||||||
*
|
*
|
||||||
|
* @param-closure-this TestCase $closure
|
||||||
|
*
|
||||||
* @return Expectable|TestCall|TestCase|mixed
|
* @return Expectable|TestCall|TestCase|mixed
|
||||||
*/
|
*/
|
||||||
function it(string $description, ?Closure $closure = null): TestCall
|
function it(string $description, ?Closure $closure = null): TestCall
|
||||||
@ -149,9 +169,7 @@ if (! function_exists('it')) {
|
|||||||
|
|
||||||
if (! function_exists('todo')) {
|
if (! function_exists('todo')) {
|
||||||
/**
|
/**
|
||||||
* Adds the given todo test. Internally, this test
|
* Creates a new test that is marked as "todo".
|
||||||
* is marked as incomplete. Yet, Collision, Pest's
|
|
||||||
* printer, will display it as a "todo" test.
|
|
||||||
*
|
*
|
||||||
* @return Expectable|TestCall|TestCase|mixed
|
* @return Expectable|TestCall|TestCase|mixed
|
||||||
*/
|
*/
|
||||||
@ -169,6 +187,8 @@ if (! function_exists('afterEach')) {
|
|||||||
/**
|
/**
|
||||||
* Runs the given closure after each test in the current file.
|
* Runs the given closure after each test in the current file.
|
||||||
*
|
*
|
||||||
|
* @param-closure-this TestCase $closure
|
||||||
|
*
|
||||||
* @return Expectable|HigherOrderTapProxy<Expectable|TestCall|TestCase>|TestCall|mixed
|
* @return Expectable|HigherOrderTapProxy<Expectable|TestCall|TestCase>|TestCall|mixed
|
||||||
*/
|
*/
|
||||||
function afterEach(?Closure $closure = null): AfterEachCall
|
function afterEach(?Closure $closure = null): AfterEachCall
|
||||||
@ -194,3 +214,32 @@ if (! function_exists('afterAll')) {
|
|||||||
TestSuite::getInstance()->afterAll->set($closure);
|
TestSuite::getInstance()->afterAll->set($closure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! function_exists('covers')) {
|
||||||
|
/**
|
||||||
|
* Specifies which classes, or functions, a test method covers.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $classesOrFunctions
|
||||||
|
*/
|
||||||
|
function covers(array|string ...$classesOrFunctions): void
|
||||||
|
{
|
||||||
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
|
$beforeEachCall = (new BeforeEachCall(TestSuite::getInstance(), $filename));
|
||||||
|
|
||||||
|
$beforeEachCall->covers(...$classesOrFunctions);
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,23 +4,30 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest;
|
namespace Pest;
|
||||||
|
|
||||||
|
use NunoMaduro\Collision\Writer;
|
||||||
use Pest\Contracts\Bootstrapper;
|
use Pest\Contracts\Bootstrapper;
|
||||||
|
use Pest\Exceptions\FatalException;
|
||||||
use Pest\Exceptions\NoDirtyTestsFound;
|
use Pest\Exceptions\NoDirtyTestsFound;
|
||||||
use Pest\Plugins\Actions\CallsAddsOutput;
|
use Pest\Plugins\Actions\CallsAddsOutput;
|
||||||
use Pest\Plugins\Actions\CallsBoot;
|
use Pest\Plugins\Actions\CallsBoot;
|
||||||
use Pest\Plugins\Actions\CallsHandleArguments;
|
use Pest\Plugins\Actions\CallsHandleArguments;
|
||||||
use Pest\Plugins\Actions\CallsShutdown;
|
use Pest\Plugins\Actions\CallsHandleOriginalArguments;
|
||||||
|
use Pest\Plugins\Actions\CallsTerminable;
|
||||||
use Pest\Support\Container;
|
use Pest\Support\Container;
|
||||||
|
use Pest\Support\Reflection;
|
||||||
|
use Pest\Support\View;
|
||||||
use PHPUnit\TestRunner\TestResult\Facade;
|
use PHPUnit\TestRunner\TestResult\Facade;
|
||||||
use PHPUnit\TextUI\Application;
|
use PHPUnit\TextUI\Application;
|
||||||
use PHPUnit\TextUI\Configuration\Registry;
|
use PHPUnit\TextUI\Configuration\Registry;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Throwable;
|
||||||
|
use Whoops\Exception\Inspector;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Kernel
|
final readonly class Kernel
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The Kernel bootstrappers.
|
* The Kernel bootstrappers.
|
||||||
@ -40,10 +47,10 @@ final class Kernel
|
|||||||
* Creates a new Kernel instance.
|
* Creates a new Kernel instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Application $application,
|
private Application $application,
|
||||||
private readonly OutputInterface $output,
|
private OutputInterface $output,
|
||||||
) {
|
) {
|
||||||
register_shutdown_function(fn () => $this->shutdown());
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,6 +66,13 @@ final class Kernel
|
|||||||
->add(OutputInterface::class, $output)
|
->add(OutputInterface::class, $output)
|
||||||
->add(Container::class, $container);
|
->add(Container::class, $container);
|
||||||
|
|
||||||
|
$kernel = new self(
|
||||||
|
new Application,
|
||||||
|
$output,
|
||||||
|
);
|
||||||
|
|
||||||
|
register_shutdown_function(fn () => $kernel->shutdown());
|
||||||
|
|
||||||
foreach (self::BOOTSTRAPPERS as $bootstrapper) {
|
foreach (self::BOOTSTRAPPERS as $bootstrapper) {
|
||||||
$bootstrapper = Container::getInstance()->get($bootstrapper);
|
$bootstrapper = Container::getInstance()->get($bootstrapper);
|
||||||
assert($bootstrapper instanceof Bootstrapper);
|
assert($bootstrapper instanceof Bootstrapper);
|
||||||
@ -68,11 +82,6 @@ final class Kernel
|
|||||||
|
|
||||||
CallsBoot::execute();
|
CallsBoot::execute();
|
||||||
|
|
||||||
$kernel = new self(
|
|
||||||
new Application(),
|
|
||||||
$output,
|
|
||||||
);
|
|
||||||
|
|
||||||
Container::getInstance()->add(self::class, $kernel);
|
Container::getInstance()->add(self::class, $kernel);
|
||||||
|
|
||||||
return $kernel;
|
return $kernel;
|
||||||
@ -81,14 +90,17 @@ final class Kernel
|
|||||||
/**
|
/**
|
||||||
* Runs the application, and returns the exit code.
|
* Runs the application, and returns the exit code.
|
||||||
*
|
*
|
||||||
* @param array<int, string> $args
|
* @param array<int, string> $originalArguments
|
||||||
|
* @param array<int, string> $arguments
|
||||||
*/
|
*/
|
||||||
public function handle(array $args): int
|
public function handle(array $originalArguments, array $arguments): int
|
||||||
{
|
{
|
||||||
$args = CallsHandleArguments::execute($args);
|
CallsHandleOriginalArguments::execute($originalArguments);
|
||||||
|
|
||||||
|
$arguments = CallsHandleArguments::execute($arguments);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->application->run($args);
|
$this->application->run($arguments);
|
||||||
} catch (NoDirtyTestsFound) {
|
} catch (NoDirtyTestsFound) {
|
||||||
$this->output->writeln([
|
$this->output->writeln([
|
||||||
'',
|
'',
|
||||||
@ -106,16 +118,54 @@ final class Kernel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shutdown the Kernel.
|
* Terminate the Kernel.
|
||||||
*/
|
*/
|
||||||
public function shutdown(): void
|
public function terminate(): void
|
||||||
{
|
{
|
||||||
$preBufferOutput = Container::getInstance()->get(KernelDump::class);
|
$preBufferOutput = Container::getInstance()->get(KernelDump::class);
|
||||||
|
|
||||||
assert($preBufferOutput instanceof KernelDump);
|
assert($preBufferOutput instanceof KernelDump);
|
||||||
|
|
||||||
$preBufferOutput->shutdown();
|
$preBufferOutput->terminate();
|
||||||
|
|
||||||
CallsShutdown::execute();
|
CallsTerminable::execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdowns unexpectedly the Kernel.
|
||||||
|
*/
|
||||||
|
public function shutdown(): void
|
||||||
|
{
|
||||||
|
$this->terminate();
|
||||||
|
|
||||||
|
if (is_array($error = error_get_last())) {
|
||||||
|
if (! in_array($error['type'], [E_ERROR, E_CORE_ERROR], true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = $error['message'];
|
||||||
|
$file = $error['file'];
|
||||||
|
$line = $error['line'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$writer = new Writer(null, $this->output);
|
||||||
|
|
||||||
|
$throwable = new FatalException($message);
|
||||||
|
|
||||||
|
Reflection::setPropertyValue($throwable, 'line', $line);
|
||||||
|
Reflection::setPropertyValue($throwable, 'file', $file);
|
||||||
|
|
||||||
|
$inspector = new Inspector($throwable);
|
||||||
|
|
||||||
|
$writer->write($inspector);
|
||||||
|
} catch (Throwable) { // @phpstan-ignore-line
|
||||||
|
View::render('components.badge', [
|
||||||
|
'type' => 'ERROR',
|
||||||
|
'content' => sprintf('%s in %s:%d', $message, $file, $line),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,9 +48,9 @@ final class KernelDump
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shutdown the output buffering.
|
* Terminate the output buffering.
|
||||||
*/
|
*/
|
||||||
public function shutdown(): void
|
public function terminate(): void
|
||||||
{
|
{
|
||||||
$this->disable();
|
$this->disable();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,25 +18,32 @@ use PHPUnit\Event\Test\Failed;
|
|||||||
use PHPUnit\Event\Test\MarkedIncomplete;
|
use PHPUnit\Event\Test\MarkedIncomplete;
|
||||||
use PHPUnit\Event\Test\Skipped;
|
use PHPUnit\Event\Test\Skipped;
|
||||||
use PHPUnit\Event\TestSuite\TestSuite;
|
use PHPUnit\Event\TestSuite\TestSuite;
|
||||||
|
use PHPUnit\Event\TestSuite\TestSuiteForTestMethodWithDataProvider;
|
||||||
use PHPUnit\Framework\Exception as FrameworkException;
|
use PHPUnit\Framework\Exception as FrameworkException;
|
||||||
use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult;
|
use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Converter
|
final readonly class Converter
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The prefix for the test suite name.
|
||||||
|
*/
|
||||||
private const PREFIX = 'P\\';
|
private const PREFIX = 'P\\';
|
||||||
|
|
||||||
private readonly StateGenerator $stateGenerator;
|
/**
|
||||||
|
* The state generator.
|
||||||
|
*/
|
||||||
|
private StateGenerator $stateGenerator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the Converter.
|
* Creates a new instance of the Converter.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly string $rootPath,
|
private string $rootPath,
|
||||||
) {
|
) {
|
||||||
$this->stateGenerator = new StateGenerator();
|
$this->stateGenerator = new StateGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,7 +136,7 @@ final class Converter
|
|||||||
|
|
||||||
// Format stacktrace as `at <path>`
|
// Format stacktrace as `at <path>`
|
||||||
$frames = array_map(
|
$frames = array_map(
|
||||||
fn (string $frame) => "at $frame",
|
fn (string $frame): string => "at $frame",
|
||||||
$frames
|
$frames
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -141,6 +148,13 @@ final class Converter
|
|||||||
*/
|
*/
|
||||||
public function getTestSuiteName(TestSuite $testSuite): string
|
public function getTestSuiteName(TestSuite $testSuite): string
|
||||||
{
|
{
|
||||||
|
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
|
||||||
|
$firstTest = $this->getFirstTest($testSuite);
|
||||||
|
if ($firstTest != null) {
|
||||||
|
return $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$name = $testSuite->name();
|
$name = $testSuite->name();
|
||||||
|
|
||||||
if (! str_starts_with($name, self::PREFIX)) {
|
if (! str_starts_with($name, self::PREFIX)) {
|
||||||
@ -162,6 +176,35 @@ final class Converter
|
|||||||
* Gets the test suite location.
|
* Gets the test suite location.
|
||||||
*/
|
*/
|
||||||
public function getTestSuiteLocation(TestSuite $testSuite): ?string
|
public function getTestSuiteLocation(TestSuite $testSuite): ?string
|
||||||
|
{
|
||||||
|
$firstTest = $this->getFirstTest($testSuite);
|
||||||
|
if ($firstTest == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$path = $firstTest->testDox()->prettifiedClassName();
|
||||||
|
$classRelativePath = $this->toRelativePath($path);
|
||||||
|
|
||||||
|
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
|
||||||
|
$methodName = $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
|
||||||
|
|
||||||
|
return "$classRelativePath::$methodName";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $classRelativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the prettified test method name without dataset-related suffix.
|
||||||
|
*/
|
||||||
|
private function getTestMethodNameWithoutDatasetSuffix(TestMethod $testMethod): string
|
||||||
|
{
|
||||||
|
return Str::beforeLast($testMethod->testDox()->prettifiedMethodName(), ' with data set ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the first test from the test suite.
|
||||||
|
*/
|
||||||
|
private function getFirstTest(TestSuite $testSuite): ?TestMethod
|
||||||
{
|
{
|
||||||
$tests = $testSuite->tests()->asArray();
|
$tests = $testSuite->tests()->asArray();
|
||||||
|
|
||||||
@ -175,9 +218,7 @@ final class Converter
|
|||||||
throw ShouldNotHappen::fromMessage('Not an instance of TestMethod');
|
throw ShouldNotHappen::fromMessage('Not an instance of TestMethod');
|
||||||
}
|
}
|
||||||
|
|
||||||
$path = $firstTest->testDox()->prettifiedClassName();
|
return $firstTest;
|
||||||
|
|
||||||
return $this->toRelativePath($path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,384 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\JUnit;
|
|
||||||
|
|
||||||
use DOMDocument;
|
|
||||||
use DOMElement;
|
|
||||||
use Pest\Logging\Converter;
|
|
||||||
use Pest\Logging\JUnit\Subscriber\TestErroredSubscriber;
|
|
||||||
use Pest\Logging\JUnit\Subscriber\TestFailedSubscriber;
|
|
||||||
use Pest\Logging\JUnit\Subscriber\TestFinishedSubscriber;
|
|
||||||
use Pest\Logging\JUnit\Subscriber\TestMarkedIncompleteSubscriber;
|
|
||||||
use Pest\Logging\JUnit\Subscriber\TestPreparedSubscriber;
|
|
||||||
use Pest\Logging\JUnit\Subscriber\TestRunnerExecutionFinishedSubscriber;
|
|
||||||
use Pest\Logging\JUnit\Subscriber\TestSkippedSubscriber;
|
|
||||||
use Pest\Logging\JUnit\Subscriber\TestSuiteFinishedSubscriber;
|
|
||||||
use Pest\Logging\JUnit\Subscriber\TestSuiteStartedSubscriber;
|
|
||||||
use PHPUnit\Event\EventFacadeIsSealedException;
|
|
||||||
use PHPUnit\Event\Facade;
|
|
||||||
use PHPUnit\Event\InvalidArgumentException;
|
|
||||||
use PHPUnit\Event\Telemetry\HRTime;
|
|
||||||
use PHPUnit\Event\Telemetry\Info;
|
|
||||||
use PHPUnit\Event\Test\Errored;
|
|
||||||
use PHPUnit\Event\Test\Failed;
|
|
||||||
use PHPUnit\Event\Test\Finished;
|
|
||||||
use PHPUnit\Event\Test\MarkedIncomplete;
|
|
||||||
use PHPUnit\Event\Test\Prepared;
|
|
||||||
use PHPUnit\Event\Test\Skipped;
|
|
||||||
use PHPUnit\Event\TestData\NoDataSetFromDataProviderException;
|
|
||||||
use PHPUnit\Event\TestSuite\Started;
|
|
||||||
use PHPUnit\Event\UnknownSubscriberTypeException;
|
|
||||||
use PHPUnit\TextUI\Output\Printer;
|
|
||||||
use PHPUnit\Util\Xml;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class JUnitLogger
|
|
||||||
{
|
|
||||||
private DOMDocument $document;
|
|
||||||
|
|
||||||
private DOMElement $root;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var DOMElement[]
|
|
||||||
*/
|
|
||||||
private array $testSuites = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-var array<int,int>
|
|
||||||
*/
|
|
||||||
private array $testSuiteTests = [0];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-var array<int,int>
|
|
||||||
*/
|
|
||||||
private array $testSuiteAssertions = [0];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-var array<int,int>
|
|
||||||
*/
|
|
||||||
private array $testSuiteErrors = [0];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-var array<int,int>
|
|
||||||
*/
|
|
||||||
private array $testSuiteFailures = [0];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-var array<int,int>
|
|
||||||
*/
|
|
||||||
private array $testSuiteSkipped = [0];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @psalm-var array<int,int|float>
|
|
||||||
*/
|
|
||||||
private array $testSuiteTimes = [0];
|
|
||||||
|
|
||||||
private int $testSuiteLevel = 0;
|
|
||||||
|
|
||||||
private ?DOMElement $currentTestCase = null;
|
|
||||||
|
|
||||||
private ?HRTime $time = null;
|
|
||||||
|
|
||||||
private bool $prepared = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws EventFacadeIsSealedException
|
|
||||||
* @throws UnknownSubscriberTypeException
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
private readonly Printer $printer,
|
|
||||||
private readonly Converter $converter,
|
|
||||||
) {
|
|
||||||
$this->registerSubscribers();
|
|
||||||
$this->createDocument();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function flush(): void
|
|
||||||
{
|
|
||||||
$this->printer->print((string) $this->document->saveXML());
|
|
||||||
|
|
||||||
$this->printer->flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSuiteStarted(Started $event): void
|
|
||||||
{
|
|
||||||
$testSuite = $this->document->createElement('testsuite');
|
|
||||||
$testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite()));
|
|
||||||
|
|
||||||
if ($event->testSuite()->isForTestClass()) {
|
|
||||||
$testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->testSuiteLevel > 0) {
|
|
||||||
$this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
|
|
||||||
} else {
|
|
||||||
$this->root->appendChild($testSuite);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->testSuiteLevel++;
|
|
||||||
$this->testSuites[$this->testSuiteLevel] = $testSuite;
|
|
||||||
$this->testSuiteTests[$this->testSuiteLevel] = 0;
|
|
||||||
$this->testSuiteAssertions[$this->testSuiteLevel] = 0;
|
|
||||||
$this->testSuiteErrors[$this->testSuiteLevel] = 0;
|
|
||||||
$this->testSuiteFailures[$this->testSuiteLevel] = 0;
|
|
||||||
$this->testSuiteSkipped[$this->testSuiteLevel] = 0;
|
|
||||||
$this->testSuiteTimes[$this->testSuiteLevel] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSuiteFinished(): void
|
|
||||||
{
|
|
||||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
||||||
'tests',
|
|
||||||
(string) $this->testSuiteTests[$this->testSuiteLevel],
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
||||||
'assertions',
|
|
||||||
(string) $this->testSuiteAssertions[$this->testSuiteLevel],
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
||||||
'errors',
|
|
||||||
(string) $this->testSuiteErrors[$this->testSuiteLevel],
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
||||||
'failures',
|
|
||||||
(string) $this->testSuiteFailures[$this->testSuiteLevel],
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
||||||
'skipped',
|
|
||||||
(string) $this->testSuiteSkipped[$this->testSuiteLevel],
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
||||||
'time',
|
|
||||||
sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]),
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($this->testSuiteLevel > 1) {
|
|
||||||
$this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel];
|
|
||||||
$this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel];
|
|
||||||
$this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel];
|
|
||||||
$this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel];
|
|
||||||
$this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel];
|
|
||||||
$this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->testSuiteLevel--;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
* @throws NoDataSetFromDataProviderException
|
|
||||||
*/
|
|
||||||
public function testPrepared(Prepared $event): void
|
|
||||||
{
|
|
||||||
$this->createTestCase($event);
|
|
||||||
$this->prepared = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
*/
|
|
||||||
public function testFinished(Finished $event): void
|
|
||||||
{
|
|
||||||
$this->handleFinish($event->telemetryInfo(), $event->numberOfAssertionsPerformed());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
* @throws NoDataSetFromDataProviderException
|
|
||||||
*/
|
|
||||||
public function testMarkedIncomplete(MarkedIncomplete $event): void
|
|
||||||
{
|
|
||||||
$this->handleIncompleteOrSkipped($event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
* @throws NoDataSetFromDataProviderException
|
|
||||||
*/
|
|
||||||
public function testSkipped(Skipped $event): void
|
|
||||||
{
|
|
||||||
$this->handleIncompleteOrSkipped($event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
* @throws NoDataSetFromDataProviderException
|
|
||||||
*/
|
|
||||||
public function testErrored(Errored $event): void
|
|
||||||
{
|
|
||||||
$this->handleFault($event, 'error');
|
|
||||||
|
|
||||||
$this->testSuiteErrors[$this->testSuiteLevel]++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
* @throws NoDataSetFromDataProviderException
|
|
||||||
*/
|
|
||||||
public function testFailed(Failed $event): void
|
|
||||||
{
|
|
||||||
$this->handleFault($event, 'failure');
|
|
||||||
|
|
||||||
$this->testSuiteFailures[$this->testSuiteLevel]++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
*/
|
|
||||||
private function handleFinish(Info $telemetryInfo, int $numberOfAssertionsPerformed): void
|
|
||||||
{
|
|
||||||
assert($this->currentTestCase instanceof \DOMElement);
|
|
||||||
assert($this->time instanceof \PHPUnit\Event\Telemetry\HRTime);
|
|
||||||
|
|
||||||
$time = $telemetryInfo->time()->duration($this->time)->asFloat();
|
|
||||||
|
|
||||||
$this->testSuiteAssertions[$this->testSuiteLevel] += $numberOfAssertionsPerformed;
|
|
||||||
|
|
||||||
$this->currentTestCase->setAttribute(
|
|
||||||
'assertions',
|
|
||||||
(string) $numberOfAssertionsPerformed,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->currentTestCase->setAttribute(
|
|
||||||
'time',
|
|
||||||
sprintf('%F', $time),
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->testSuites[$this->testSuiteLevel]->appendChild(
|
|
||||||
$this->currentTestCase,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->testSuiteTests[$this->testSuiteLevel]++;
|
|
||||||
$this->testSuiteTimes[$this->testSuiteLevel] += $time;
|
|
||||||
|
|
||||||
$this->currentTestCase = null;
|
|
||||||
$this->time = null;
|
|
||||||
$this->prepared = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws EventFacadeIsSealedException
|
|
||||||
* @throws UnknownSubscriberTypeException
|
|
||||||
*/
|
|
||||||
private function registerSubscribers(): void
|
|
||||||
{
|
|
||||||
$subscribers = [
|
|
||||||
new TestSuiteStartedSubscriber($this),
|
|
||||||
new TestSuiteFinishedSubscriber($this),
|
|
||||||
new TestPreparedSubscriber($this),
|
|
||||||
new TestFinishedSubscriber($this),
|
|
||||||
new TestErroredSubscriber($this),
|
|
||||||
new TestFailedSubscriber($this),
|
|
||||||
new TestMarkedIncompleteSubscriber($this),
|
|
||||||
new TestSkippedSubscriber($this),
|
|
||||||
new TestRunnerExecutionFinishedSubscriber($this),
|
|
||||||
];
|
|
||||||
|
|
||||||
Facade::instance()->registerSubscribers(...$subscribers);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createDocument(): void
|
|
||||||
{
|
|
||||||
$this->document = new DOMDocument('1.0', 'UTF-8');
|
|
||||||
$this->document->formatOutput = true;
|
|
||||||
|
|
||||||
$this->root = $this->document->createElement('testsuites');
|
|
||||||
$this->document->appendChild($this->root);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
* @throws NoDataSetFromDataProviderException
|
|
||||||
*/
|
|
||||||
private function handleFault(Errored|Failed $event, string $type): void
|
|
||||||
{
|
|
||||||
if (! $this->prepared) {
|
|
||||||
$this->createTestCase($event);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert($this->currentTestCase instanceof \DOMElement);
|
|
||||||
|
|
||||||
$throwable = $event->throwable();
|
|
||||||
|
|
||||||
$testName = $this->converter->getTestCaseMethodName($event->test());
|
|
||||||
$message = $this->converter->getExceptionMessage($throwable);
|
|
||||||
$details = $this->converter->getExceptionDetails($throwable);
|
|
||||||
|
|
||||||
$buffer = $testName;
|
|
||||||
$buffer .= trim(
|
|
||||||
$message.PHP_EOL.
|
|
||||||
$details,
|
|
||||||
);
|
|
||||||
|
|
||||||
$fault = $this->document->createElement(
|
|
||||||
$type,
|
|
||||||
Xml::prepareString($buffer),
|
|
||||||
);
|
|
||||||
|
|
||||||
$fault->setAttribute('type', $throwable->className());
|
|
||||||
|
|
||||||
$this->currentTestCase->appendChild($fault);
|
|
||||||
|
|
||||||
if (! $this->prepared) {
|
|
||||||
$this->handleFinish($event->telemetryInfo(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
* @throws NoDataSetFromDataProviderException
|
|
||||||
*/
|
|
||||||
private function handleIncompleteOrSkipped(MarkedIncomplete|Skipped $event): void
|
|
||||||
{
|
|
||||||
if (! $this->prepared) {
|
|
||||||
$this->createTestCase($event);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert($this->currentTestCase instanceof \DOMElement);
|
|
||||||
|
|
||||||
$skipped = $this->document->createElement('skipped');
|
|
||||||
|
|
||||||
$this->currentTestCase->appendChild($skipped);
|
|
||||||
|
|
||||||
$this->testSuiteSkipped[$this->testSuiteLevel]++;
|
|
||||||
|
|
||||||
if (! $this->prepared) {
|
|
||||||
$this->handleFinish($event->telemetryInfo(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
* @throws NoDataSetFromDataProviderException
|
|
||||||
*
|
|
||||||
* @psalm-assert !null $this->currentTestCase
|
|
||||||
*/
|
|
||||||
private function createTestCase(Errored|Failed|MarkedIncomplete|Prepared|Skipped $event): void
|
|
||||||
{
|
|
||||||
$testCase = $this->document->createElement('testcase');
|
|
||||||
|
|
||||||
$test = $event->test();
|
|
||||||
$file = $this->converter->getTestCaseLocation($test);
|
|
||||||
|
|
||||||
$testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test));
|
|
||||||
$testCase->setAttribute('file', $file);
|
|
||||||
|
|
||||||
if ($test->isTestMethod()) {
|
|
||||||
$className = $this->converter->getTrimmedTestClassName($test);
|
|
||||||
$testCase->setAttribute('class', $className);
|
|
||||||
$testCase->setAttribute('classname', str_replace('\\', '.', $className));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->currentTestCase = $testCase;
|
|
||||||
$this->time = $event->telemetryInfo()->time();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\JUnit\Subscriber;
|
|
||||||
|
|
||||||
use Pest\Logging\JUnit\JUnitLogger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
abstract class Subscriber
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Creates a new Subscriber instance.
|
|
||||||
*/
|
|
||||||
public function __construct(private readonly JUnitLogger $logger)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new JunitLogger instance.
|
|
||||||
*/
|
|
||||||
final protected function logger(): JUnitLogger
|
|
||||||
{
|
|
||||||
return $this->logger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\JUnit\Subscriber;
|
|
||||||
|
|
||||||
use PHPUnit\Event\Test\Errored;
|
|
||||||
use PHPUnit\Event\Test\ErroredSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class TestErroredSubscriber extends Subscriber implements ErroredSubscriber
|
|
||||||
{
|
|
||||||
public function notify(Errored $event): void
|
|
||||||
{
|
|
||||||
$this->logger()->testErrored($event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\JUnit\Subscriber;
|
|
||||||
|
|
||||||
use PHPUnit\Event\Test\Failed;
|
|
||||||
use PHPUnit\Event\Test\FailedSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class TestFailedSubscriber extends Subscriber implements FailedSubscriber
|
|
||||||
{
|
|
||||||
public function notify(Failed $event): void
|
|
||||||
{
|
|
||||||
$this->logger()->testFailed($event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\JUnit\Subscriber;
|
|
||||||
|
|
||||||
use PHPUnit\Event\Test\Finished;
|
|
||||||
use PHPUnit\Event\Test\FinishedSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class TestFinishedSubscriber extends Subscriber implements FinishedSubscriber
|
|
||||||
{
|
|
||||||
public function notify(Finished $event): void
|
|
||||||
{
|
|
||||||
$this->logger()->testFinished($event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\JUnit\Subscriber;
|
|
||||||
|
|
||||||
use PHPUnit\Event\Test\MarkedIncomplete;
|
|
||||||
use PHPUnit\Event\Test\MarkedIncompleteSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber
|
|
||||||
{
|
|
||||||
public function notify(MarkedIncomplete $event): void
|
|
||||||
{
|
|
||||||
$this->logger()->testMarkedIncomplete($event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\JUnit\Subscriber;
|
|
||||||
|
|
||||||
use PHPUnit\Event\Test\Prepared;
|
|
||||||
use PHPUnit\Event\Test\PreparedSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class TestPreparedSubscriber extends Subscriber implements PreparedSubscriber
|
|
||||||
{
|
|
||||||
public function notify(Prepared $event): void
|
|
||||||
{
|
|
||||||
$this->logger()->testPrepared($event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\JUnit\Subscriber;
|
|
||||||
|
|
||||||
use PHPUnit\Event\TestRunner\ExecutionFinished;
|
|
||||||
use PHPUnit\Event\TestRunner\ExecutionFinishedSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class TestRunnerExecutionFinishedSubscriber extends Subscriber implements ExecutionFinishedSubscriber
|
|
||||||
{
|
|
||||||
public function notify(ExecutionFinished $event): void
|
|
||||||
{
|
|
||||||
$this->logger()->flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\JUnit\Subscriber;
|
|
||||||
|
|
||||||
use PHPUnit\Event\Test\Skipped;
|
|
||||||
use PHPUnit\Event\Test\SkippedSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
|
|
||||||
{
|
|
||||||
public function notify(Skipped $event): void
|
|
||||||
{
|
|
||||||
$this->logger()->testSkipped($event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\JUnit\Subscriber;
|
|
||||||
|
|
||||||
use PHPUnit\Event\TestSuite\Finished;
|
|
||||||
use PHPUnit\Event\TestSuite\FinishedSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class TestSuiteFinishedSubscriber extends Subscriber implements FinishedSubscriber
|
|
||||||
{
|
|
||||||
public function notify(Finished $event): void
|
|
||||||
{
|
|
||||||
$this->logger()->testSuiteFinished();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\JUnit\Subscriber;
|
|
||||||
|
|
||||||
use PHPUnit\Event\TestSuite\Started;
|
|
||||||
use PHPUnit\Event\TestSuite\StartedSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class TestSuiteStartedSubscriber extends Subscriber implements StartedSubscriber
|
|
||||||
{
|
|
||||||
public function notify(Started $event): void
|
|
||||||
{
|
|
||||||
$this->logger()->testSuiteStarted($event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,6 +9,9 @@ namespace Pest\Logging\TeamCity;
|
|||||||
*/
|
*/
|
||||||
final class ServiceMessage
|
final class ServiceMessage
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The flow ID.
|
||||||
|
*/
|
||||||
private static ?int $flowId = null;
|
private static ?int $flowId = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,8 +20,7 @@ final class ServiceMessage
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly string $type,
|
private readonly string $type,
|
||||||
private readonly array $parameters,
|
private readonly array $parameters,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
public function toString(): string
|
public function toString(): string
|
||||||
{
|
{
|
||||||
@ -36,7 +38,7 @@ final class ServiceMessage
|
|||||||
{
|
{
|
||||||
return new self('testSuiteStarted', [
|
return new self('testSuiteStarted', [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'locationHint' => $location === null ? null : "file://$location",
|
'locationHint' => $location === null ? null : "pest_qn://$location",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,19 +9,17 @@ use Pest\Logging\TeamCity\TeamCityLogger;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
abstract class Subscriber
|
abstract class Subscriber // @pest-arch-ignore-line
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Creates a new Subscriber instance.
|
* Creates a new Subscriber instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(private readonly TeamCityLogger $logger)
|
public function __construct(private readonly TeamCityLogger $logger) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new TeamCityLogger instance.
|
* Creates a new TeamCityLogger instance.
|
||||||
*/
|
*/
|
||||||
final protected function logger(): TeamCityLogger
|
final protected function logger(): TeamCityLogger // @pest-arch-ignore-line
|
||||||
{
|
{
|
||||||
return $this->logger;
|
return $this->logger;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Logging\TeamCity\Subscriber;
|
|
||||||
|
|
||||||
use PHPUnit\Event\Test\MarkedIncomplete;
|
|
||||||
use PHPUnit\Event\Test\MarkedIncompleteSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber
|
|
||||||
{
|
|
||||||
public function notify(MarkedIncomplete $event): void
|
|
||||||
{
|
|
||||||
$this->logger()->testMarkedIncomplete($event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -12,7 +12,6 @@ use Pest\Logging\TeamCity\Subscriber\TestErroredSubscriber;
|
|||||||
use Pest\Logging\TeamCity\Subscriber\TestExecutionFinishedSubscriber;
|
use Pest\Logging\TeamCity\Subscriber\TestExecutionFinishedSubscriber;
|
||||||
use Pest\Logging\TeamCity\Subscriber\TestFailedSubscriber;
|
use Pest\Logging\TeamCity\Subscriber\TestFailedSubscriber;
|
||||||
use Pest\Logging\TeamCity\Subscriber\TestFinishedSubscriber;
|
use Pest\Logging\TeamCity\Subscriber\TestFinishedSubscriber;
|
||||||
use Pest\Logging\TeamCity\Subscriber\TestMarkedIncompleteSubscriber;
|
|
||||||
use Pest\Logging\TeamCity\Subscriber\TestPreparedSubscriber;
|
use Pest\Logging\TeamCity\Subscriber\TestPreparedSubscriber;
|
||||||
use Pest\Logging\TeamCity\Subscriber\TestSkippedSubscriber;
|
use Pest\Logging\TeamCity\Subscriber\TestSkippedSubscriber;
|
||||||
use Pest\Logging\TeamCity\Subscriber\TestSuiteFinishedSubscriber;
|
use Pest\Logging\TeamCity\Subscriber\TestSuiteFinishedSubscriber;
|
||||||
@ -28,7 +27,6 @@ use PHPUnit\Event\Test\ConsideredRisky;
|
|||||||
use PHPUnit\Event\Test\Errored;
|
use PHPUnit\Event\Test\Errored;
|
||||||
use PHPUnit\Event\Test\Failed;
|
use PHPUnit\Event\Test\Failed;
|
||||||
use PHPUnit\Event\Test\Finished;
|
use PHPUnit\Event\Test\Finished;
|
||||||
use PHPUnit\Event\Test\MarkedIncomplete;
|
|
||||||
use PHPUnit\Event\Test\Prepared;
|
use PHPUnit\Event\Test\Prepared;
|
||||||
use PHPUnit\Event\Test\Skipped;
|
use PHPUnit\Event\Test\Skipped;
|
||||||
use PHPUnit\Event\TestRunner\ExecutionFinished;
|
use PHPUnit\Event\TestRunner\ExecutionFinished;
|
||||||
@ -45,8 +43,14 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
*/
|
*/
|
||||||
final class TeamCityLogger
|
final class TeamCityLogger
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The current time.
|
||||||
|
*/
|
||||||
private ?HRTime $time = null;
|
private ?HRTime $time = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the summary test count has been printed.
|
||||||
|
*/
|
||||||
private bool $isSummaryTestCountPrinted = false;
|
private bool $isSummaryTestCountPrinted = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,7 +112,7 @@ final class TeamCityLogger
|
|||||||
$this->time = $event->telemetryInfo()->time();
|
$this->time = $event->telemetryInfo()->time();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMarkedIncomplete(MarkedIncomplete $event): never
|
public function testMarkedIncomplete(): never
|
||||||
{
|
{
|
||||||
throw ShouldNotHappen::fromMessage('testMarkedIncomplete not implemented.');
|
throw ShouldNotHappen::fromMessage('testMarkedIncomplete not implemented.');
|
||||||
}
|
}
|
||||||
@ -262,7 +266,6 @@ final class TeamCityLogger
|
|||||||
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 TestConsideredRiskySubscriber($this),
|
new TestConsideredRiskySubscriber($this),
|
||||||
new TestExecutionFinishedSubscriber($this),
|
new TestExecutionFinishedSubscriber($this),
|
||||||
|
|||||||
@ -7,6 +7,4 @@ namespace Pest\Matchers;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Any
|
final class Any {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|||||||
@ -131,7 +131,7 @@ final class Expectation
|
|||||||
*
|
*
|
||||||
* @return self<TValue>
|
* @return self<TValue>
|
||||||
*/
|
*/
|
||||||
public function toBeGreaterThan(int|float|DateTimeInterface $expected, string $message = ''): self
|
public function toBeGreaterThan(int|float|string|DateTimeInterface $expected, string $message = ''): self
|
||||||
{
|
{
|
||||||
Assert::assertGreaterThan($expected, $this->value, $message);
|
Assert::assertGreaterThan($expected, $this->value, $message);
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ final class Expectation
|
|||||||
*
|
*
|
||||||
* @return self<TValue>
|
* @return self<TValue>
|
||||||
*/
|
*/
|
||||||
public function toBeGreaterThanOrEqual(int|float|DateTimeInterface $expected, string $message = ''): self
|
public function toBeGreaterThanOrEqual(int|float|string|DateTimeInterface $expected, string $message = ''): self
|
||||||
{
|
{
|
||||||
Assert::assertGreaterThanOrEqual($expected, $this->value, $message);
|
Assert::assertGreaterThanOrEqual($expected, $this->value, $message);
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ final class Expectation
|
|||||||
*
|
*
|
||||||
* @return self<TValue>
|
* @return self<TValue>
|
||||||
*/
|
*/
|
||||||
public function toBeLessThan(int|float|DateTimeInterface $expected, string $message = ''): self
|
public function toBeLessThan(int|float|string|DateTimeInterface $expected, string $message = ''): self
|
||||||
{
|
{
|
||||||
Assert::assertLessThan($expected, $this->value, $message);
|
Assert::assertLessThan($expected, $this->value, $message);
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ final class Expectation
|
|||||||
*
|
*
|
||||||
* @return self<TValue>
|
* @return self<TValue>
|
||||||
*/
|
*/
|
||||||
public function toBeLessThanOrEqual(int|float|DateTimeInterface $expected, string $message = ''): self
|
public function toBeLessThanOrEqual(int|float|string|DateTimeInterface $expected, string $message = ''): self
|
||||||
{
|
{
|
||||||
Assert::assertLessThanOrEqual($expected, $this->value, $message);
|
Assert::assertLessThanOrEqual($expected, $this->value, $message);
|
||||||
|
|
||||||
@ -196,6 +196,24 @@ final class Expectation
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that $needle equal an element of the value.
|
||||||
|
*
|
||||||
|
* @return self<TValue>
|
||||||
|
*/
|
||||||
|
public function toContainEqual(mixed ...$needles): self
|
||||||
|
{
|
||||||
|
if (! is_iterable($this->value)) {
|
||||||
|
InvalidExpectationValue::expected('iterable');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($needles as $needle) {
|
||||||
|
Assert::assertContainsEquals($needle, $this->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the value starts with $expected.
|
* Asserts that the value starts with $expected.
|
||||||
*
|
*
|
||||||
@ -296,7 +314,7 @@ final class Expectation
|
|||||||
*
|
*
|
||||||
* @return self<TValue>
|
* @return self<TValue>
|
||||||
*/
|
*/
|
||||||
public function toHaveProperty(string $name, mixed $value = new Any(), string $message = ''): self
|
public function toHaveProperty(string $name, mixed $value = new Any, string $message = ''): self
|
||||||
{
|
{
|
||||||
$this->toBeObject();
|
$this->toBeObject();
|
||||||
|
|
||||||
@ -326,36 +344,6 @@ final class Expectation
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Asserts that the value has the method $name.
|
|
||||||
*
|
|
||||||
* @return self<TValue>
|
|
||||||
*/
|
|
||||||
public function toHaveMethod(string $name, string $message = ''): self
|
|
||||||
{
|
|
||||||
$this->toBeObject();
|
|
||||||
|
|
||||||
// @phpstan-ignore-next-line
|
|
||||||
Assert::assertTrue(method_exists($this->value, $name), $message);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asserts that the value has the provided methods $names.
|
|
||||||
*
|
|
||||||
* @param iterable<array-key, string> $names
|
|
||||||
* @return self<TValue>
|
|
||||||
*/
|
|
||||||
public function toHaveMethods(iterable $names, string $message = ''): self
|
|
||||||
{
|
|
||||||
foreach ($names as $name) {
|
|
||||||
$this->toHaveMethod($name, message: $message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that two variables have the same value.
|
* Asserts that two variables have the same value.
|
||||||
*
|
*
|
||||||
@ -449,6 +437,18 @@ final class Expectation
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the value is a list.
|
||||||
|
*
|
||||||
|
* @return self<TValue>
|
||||||
|
*/
|
||||||
|
public function toBeList(string $message = ''): self
|
||||||
|
{
|
||||||
|
Assert::assertIsList($this->value, $message);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the value is of type bool.
|
* Asserts that the value is of type bool.
|
||||||
*
|
*
|
||||||
@ -624,7 +624,7 @@ final class Expectation
|
|||||||
*
|
*
|
||||||
* @return self<TValue>
|
* @return self<TValue>
|
||||||
*/
|
*/
|
||||||
public function toHaveKey(string|int $key, mixed $value = new Any(), string $message = ''): self
|
public function toHaveKey(string|int $key, mixed $value = new Any, string $message = ''): self
|
||||||
{
|
{
|
||||||
if (is_object($this->value) && method_exists($this->value, 'toArray')) {
|
if (is_object($this->value) && method_exists($this->value, 'toArray')) {
|
||||||
$array = $this->value->toArray();
|
$array = $this->value->toArray();
|
||||||
|
|||||||
@ -5,19 +5,20 @@ declare(strict_types=1);
|
|||||||
namespace Pest;
|
namespace Pest;
|
||||||
|
|
||||||
use NunoMaduro\Collision\Writer;
|
use NunoMaduro\Collision\Writer;
|
||||||
|
use Pest\Exceptions\TestDescriptionMissing;
|
||||||
use Pest\Support\Container;
|
use Pest\Support\Container;
|
||||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use Whoops\Exception\Inspector;
|
use Whoops\Exception\Inspector;
|
||||||
|
|
||||||
final class Panic
|
final readonly class Panic
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Creates a new Panic instance.
|
* Creates a new Panic instance.
|
||||||
*/
|
*/
|
||||||
private function __construct(
|
private function __construct(
|
||||||
private readonly Throwable $throwable
|
private Throwable $throwable
|
||||||
) {
|
) {
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
@ -27,6 +28,10 @@ final class Panic
|
|||||||
*/
|
*/
|
||||||
public static function with(Throwable $throwable): never
|
public static function with(Throwable $throwable): never
|
||||||
{
|
{
|
||||||
|
if ($throwable instanceof TestDescriptionMissing && ! is_null($previous = $throwable->getPrevious())) {
|
||||||
|
$throwable = $previous;
|
||||||
|
}
|
||||||
|
|
||||||
$panic = new self($throwable);
|
$panic = new self($throwable);
|
||||||
|
|
||||||
$panic->handle();
|
$panic->handle();
|
||||||
@ -42,7 +47,7 @@ final class Panic
|
|||||||
try {
|
try {
|
||||||
$output = Container::getInstance()->get(OutputInterface::class);
|
$output = Container::getInstance()->get(OutputInterface::class);
|
||||||
} catch (Throwable) { // @phpstan-ignore-line
|
} catch (Throwable) { // @phpstan-ignore-line
|
||||||
$output = new ConsoleOutput();
|
$output = new ConsoleOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert($output instanceof OutputInterface);
|
assert($output instanceof OutputInterface);
|
||||||
|
|||||||
@ -39,7 +39,7 @@ final class AfterEachCall
|
|||||||
) {
|
) {
|
||||||
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
|
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
|
||||||
|
|
||||||
$this->proxies = new HigherOrderMessageCollection();
|
$this->proxies = new HigherOrderMessageCollection;
|
||||||
|
|
||||||
$this->describing = DescribeCall::describing();
|
$this->describing = DescribeCall::describing();
|
||||||
}
|
}
|
||||||
@ -54,7 +54,7 @@ final class AfterEachCall
|
|||||||
$proxies = $this->proxies;
|
$proxies = $this->proxies;
|
||||||
|
|
||||||
$afterEachTestCase = ChainableClosure::boundWhen(
|
$afterEachTestCase = ChainableClosure::boundWhen(
|
||||||
fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line
|
fn (): bool => is_null($describing) || $this->__describing === $describing,
|
||||||
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), // @phpstan-ignore-line
|
||||||
)->bindTo($this, self::class);
|
)->bindTo($this, self::class);
|
||||||
|
|
||||||
@ -65,7 +65,6 @@ final class AfterEachCall
|
|||||||
$this,
|
$this,
|
||||||
$afterEachTestCase,
|
$afterEachTestCase,
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Pest\PendingCalls;
|
namespace Pest\PendingCalls;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Pest\Exceptions\AfterBeforeTestFunction;
|
||||||
use Pest\PendingCalls\Concerns\Describable;
|
use Pest\PendingCalls\Concerns\Describable;
|
||||||
use Pest\Support\Backtrace;
|
use Pest\Support\Backtrace;
|
||||||
use Pest\Support\ChainableClosure;
|
use Pest\Support\ChainableClosure;
|
||||||
@ -14,6 +15,8 @@ use Pest\TestSuite;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
*
|
||||||
|
* @mixin TestCall
|
||||||
*/
|
*/
|
||||||
final class BeforeEachCall
|
final class BeforeEachCall
|
||||||
{
|
{
|
||||||
@ -44,8 +47,8 @@ final class BeforeEachCall
|
|||||||
) {
|
) {
|
||||||
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
|
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
|
||||||
|
|
||||||
$this->testCallProxies = new HigherOrderMessageCollection();
|
$this->testCallProxies = new HigherOrderMessageCollection;
|
||||||
$this->testCaseProxies = new HigherOrderMessageCollection();
|
$this->testCaseProxies = new HigherOrderMessageCollection;
|
||||||
|
|
||||||
$this->describing = DescribeCall::describing();
|
$this->describing = DescribeCall::describing();
|
||||||
}
|
}
|
||||||
@ -59,17 +62,22 @@ final class BeforeEachCall
|
|||||||
$testCaseProxies = $this->testCaseProxies;
|
$testCaseProxies = $this->testCaseProxies;
|
||||||
|
|
||||||
$beforeEachTestCall = function (TestCall $testCall) use ($describing): void {
|
$beforeEachTestCall = function (TestCall $testCall) use ($describing): void {
|
||||||
if ($describing !== $this->describing) {
|
|
||||||
return;
|
if ($this->describing !== null) {
|
||||||
}
|
if ($describing !== $this->describing) {
|
||||||
if ($describing !== $testCall->describing) {
|
return;
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
if ($describing !== $testCall->describing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->testCallProxies->chain($testCall);
|
$this->testCallProxies->chain($testCall);
|
||||||
};
|
};
|
||||||
|
|
||||||
$beforeEachTestCase = ChainableClosure::boundWhen(
|
$beforeEachTestCase = ChainableClosure::boundWhen(
|
||||||
fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line
|
fn (): bool => is_null($describing) || $this->__describing === $describing,
|
||||||
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), // @phpstan-ignore-line
|
||||||
)->bindTo($this, self::class);
|
)->bindTo($this, self::class);
|
||||||
|
|
||||||
@ -83,6 +91,18 @@ final class BeforeEachCall
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given closure after the test.
|
||||||
|
*/
|
||||||
|
public function after(Closure $closure): self
|
||||||
|
{
|
||||||
|
if ($this->describing === null) {
|
||||||
|
throw new AfterBeforeTestFunction($this->filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->__call('after', [$closure]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the calls to be used on the target.
|
* Saves the calls to be used on the target.
|
||||||
*
|
*
|
||||||
@ -91,7 +111,8 @@ final class BeforeEachCall
|
|||||||
public function __call(string $name, array $arguments): self
|
public function __call(string $name, array $arguments): self
|
||||||
{
|
{
|
||||||
if (method_exists(TestCall::class, $name)) {
|
if (method_exists(TestCall::class, $name)) {
|
||||||
$this->testCallProxies->add(Backtrace::file(), Backtrace::line(), $name, $arguments);
|
$this->testCallProxies
|
||||||
|
->add(Backtrace::file(), Backtrace::line(), $name, $arguments);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,5 +9,13 @@ namespace Pest\PendingCalls\Concerns;
|
|||||||
*/
|
*/
|
||||||
trait Describable
|
trait Describable
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Note: this is property is not used; however, it gets added automatically by rector php.
|
||||||
|
*/
|
||||||
|
public string $__describing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The describing of the test case.
|
||||||
|
*/
|
||||||
public ?string $describing = null;
|
public ?string $describing = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,11 @@ final class DescribeCall
|
|||||||
*/
|
*/
|
||||||
private static ?string $describing = null;
|
private static ?string $describing = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The describe "before each" call.
|
||||||
|
*/
|
||||||
|
private ?BeforeEachCall $currentBeforeEachCall = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Pending Call.
|
* Creates a new Pending Call.
|
||||||
*/
|
*/
|
||||||
@ -43,6 +48,8 @@ final class DescribeCall
|
|||||||
*/
|
*/
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
{
|
{
|
||||||
|
unset($this->currentBeforeEachCall);
|
||||||
|
|
||||||
self::$describing = $this->description;
|
self::$describing = $this->description;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -57,14 +64,18 @@ final class DescribeCall
|
|||||||
*
|
*
|
||||||
* @param array<int, mixed> $arguments
|
* @param array<int, mixed> $arguments
|
||||||
*/
|
*/
|
||||||
public function __call(string $name, array $arguments): BeforeEachCall
|
public function __call(string $name, array $arguments): self
|
||||||
{
|
{
|
||||||
$filename = Backtrace::file();
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
$beforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename);
|
if (! $this->currentBeforeEachCall instanceof \Pest\PendingCalls\BeforeEachCall) {
|
||||||
|
$this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename);
|
||||||
|
|
||||||
$beforeEachCall->describing = $this->description;
|
$this->currentBeforeEachCall->describing = $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
return $beforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line
|
$this->currentBeforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,13 +6,14 @@ namespace Pest\PendingCalls;
|
|||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Pest\Exceptions\InvalidArgumentException;
|
use Pest\Exceptions\InvalidArgumentException;
|
||||||
use Pest\Factories\Covers\CoversClass;
|
use Pest\Exceptions\TestDescriptionMissing;
|
||||||
use Pest\Factories\Covers\CoversFunction;
|
use Pest\Factories\Attribute;
|
||||||
use Pest\Factories\Covers\CoversNothing;
|
|
||||||
use Pest\Factories\TestCaseMethodFactory;
|
use Pest\Factories\TestCaseMethodFactory;
|
||||||
|
use Pest\Mutate\Repositories\ConfigurationRepository;
|
||||||
use Pest\PendingCalls\Concerns\Describable;
|
use Pest\PendingCalls\Concerns\Describable;
|
||||||
use Pest\Plugins\Only;
|
use Pest\Plugins\Only;
|
||||||
use Pest\Support\Backtrace;
|
use Pest\Support\Backtrace;
|
||||||
|
use Pest\Support\Container;
|
||||||
use Pest\Support\Exporter;
|
use Pest\Support\Exporter;
|
||||||
use Pest\Support\HigherOrderCallables;
|
use Pest\Support\HigherOrderCallables;
|
||||||
use Pest\Support\NullClosure;
|
use Pest\Support\NullClosure;
|
||||||
@ -30,6 +31,13 @@ final class TestCall
|
|||||||
{
|
{
|
||||||
use Describable;
|
use Describable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of test case factory attributes.
|
||||||
|
*
|
||||||
|
* @var array<int, Attribute>
|
||||||
|
*/
|
||||||
|
private array $testCaseFactoryAttributes = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Test Case Factory.
|
* The Test Case Factory.
|
||||||
*/
|
*/
|
||||||
@ -46,10 +54,10 @@ final class TestCall
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TestSuite $testSuite,
|
private readonly TestSuite $testSuite,
|
||||||
private readonly string $filename,
|
private readonly string $filename,
|
||||||
?string $description = null,
|
private ?string $description = null,
|
||||||
?Closure $closure = null
|
?Closure $closure = null
|
||||||
) {
|
) {
|
||||||
$this->testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure);
|
$this->testCaseMethod = new TestCaseMethodFactory($filename, $closure);
|
||||||
|
|
||||||
$this->descriptionLess = $description === null;
|
$this->descriptionLess = $description === null;
|
||||||
|
|
||||||
@ -58,6 +66,42 @@ final class TestCall
|
|||||||
$this->testSuite->beforeEach->get($this->filename)[0]($this);
|
$this->testSuite->beforeEach->get($this->filename)[0]($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given closure after the test.
|
||||||
|
*/
|
||||||
|
public function after(Closure $closure): self
|
||||||
|
{
|
||||||
|
if ($this->description === null) {
|
||||||
|
throw new TestDescriptionMissing($this->filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
$description = is_null($this->describing)
|
||||||
|
? $this->description
|
||||||
|
: Str::describe($this->describing, $this->description);
|
||||||
|
|
||||||
|
$filename = $this->filename;
|
||||||
|
|
||||||
|
$when = function () use ($closure, $filename, $description): void {
|
||||||
|
if ($this::$__filename !== $filename) { // @phpstan-ignore-line
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->__description !== $description) { // @phpstan-ignore-line
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->__ran !== true) { // @phpstan-ignore-line
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$closure->call($this);
|
||||||
|
};
|
||||||
|
|
||||||
|
new AfterEachCall($this->testSuite, $this->filename, $when->bindTo(new \stdClass));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the test fails with the given message.
|
* Asserts that the test fails with the given message.
|
||||||
*/
|
*/
|
||||||
@ -165,7 +209,10 @@ final class TestCall
|
|||||||
public function group(string ...$groups): self
|
public function group(string ...$groups): self
|
||||||
{
|
{
|
||||||
foreach ($groups as $group) {
|
foreach ($groups as $group) {
|
||||||
$this->testCaseMethod->groups[] = $group;
|
$this->testCaseMethod->attributes[] = new Attribute(
|
||||||
|
\PHPUnit\Framework\Attributes\Group::class,
|
||||||
|
[$group],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -176,7 +223,7 @@ final class TestCall
|
|||||||
*/
|
*/
|
||||||
public function only(): self
|
public function only(): self
|
||||||
{
|
{
|
||||||
Only::enable($this);
|
Only::enable($this, ...func_get_args()); // @phpstan-ignore-line
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@ -306,32 +353,186 @@ final class TestCall
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the test as "todo".
|
* Marks the test as "todo".
|
||||||
*/
|
*/
|
||||||
public function todo(): self
|
public function todo(// @phpstan-ignore-line
|
||||||
{
|
array|string|null $note = null,
|
||||||
|
array|string|null $assignee = null,
|
||||||
|
array|string|int|null $issue = null,
|
||||||
|
array|string|int|null $pr = null,
|
||||||
|
): self {
|
||||||
$this->skip('__TODO__');
|
$this->skip('__TODO__');
|
||||||
|
|
||||||
$this->testCaseMethod->todo = true;
|
$this->testCaseMethod->todo = true;
|
||||||
|
|
||||||
|
if ($issue !== null) {
|
||||||
|
$this->issue($issue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pr !== null) {
|
||||||
|
$this->pr($pr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($assignee !== null) {
|
||||||
|
$this->assignee($assignee);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($note !== null) {
|
||||||
|
$this->note($note);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the test as "work in progress".
|
||||||
|
*/
|
||||||
|
public function wip(// @phpstan-ignore-line
|
||||||
|
array|string|null $note = null,
|
||||||
|
array|string|null $assignee = null,
|
||||||
|
array|string|int|null $issue = null,
|
||||||
|
array|string|int|null $pr = null,
|
||||||
|
): self {
|
||||||
|
if ($issue !== null) {
|
||||||
|
$this->issue($issue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pr !== null) {
|
||||||
|
$this->pr($pr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($assignee !== null) {
|
||||||
|
$this->assignee($assignee);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($note !== null) {
|
||||||
|
$this->note($note);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the test as "done".
|
||||||
|
*/
|
||||||
|
public function done(// @phpstan-ignore-line
|
||||||
|
array|string|null $note = null,
|
||||||
|
array|string|null $assignee = null,
|
||||||
|
array|string|int|null $issue = null,
|
||||||
|
array|string|int|null $pr = null,
|
||||||
|
): self {
|
||||||
|
if ($issue !== null) {
|
||||||
|
$this->issue($issue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pr !== null) {
|
||||||
|
$this->pr($pr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($assignee !== null) {
|
||||||
|
$this->assignee($assignee);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($note !== null) {
|
||||||
|
$this->note($note);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates the test with the given issue(s).
|
||||||
|
*
|
||||||
|
* @param array<int, string|int>|string|int $number
|
||||||
|
*/
|
||||||
|
public function issue(array|string|int $number): self
|
||||||
|
{
|
||||||
|
$number = is_array($number) ? $number : [$number];
|
||||||
|
|
||||||
|
$number = array_map(fn (string|int $number): int => (int) ltrim((string) $number, '#'), $number);
|
||||||
|
|
||||||
|
$this->testCaseMethod->issues = array_merge($this->testCaseMethod->issues, $number);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates the test with the given ticket(s). (Alias for `issue`)
|
||||||
|
*
|
||||||
|
* @param array<int, string|int>|string|int $number
|
||||||
|
*/
|
||||||
|
public function ticket(array|string|int $number): self
|
||||||
|
{
|
||||||
|
return $this->issue($number);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the test assignee(s).
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $assignee
|
||||||
|
*/
|
||||||
|
public function assignee(array|string $assignee): self
|
||||||
|
{
|
||||||
|
$assignees = is_array($assignee) ? $assignee : [$assignee];
|
||||||
|
|
||||||
|
$this->testCaseMethod->assignees = array_unique(array_merge($this->testCaseMethod->assignees, $assignees));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates the test with the given pull request(s).
|
||||||
|
*
|
||||||
|
* @param array<int, string|int>|string|int $number
|
||||||
|
*/
|
||||||
|
public function pr(array|string|int $number): self
|
||||||
|
{
|
||||||
|
$number = is_array($number) ? $number : [$number];
|
||||||
|
|
||||||
|
$number = array_map(fn (string|int $number): int => (int) ltrim((string) $number, '#'), $number);
|
||||||
|
|
||||||
|
$this->testCaseMethod->prs = array_unique(array_merge($this->testCaseMethod->prs, $number));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a note to the test.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $note
|
||||||
|
*/
|
||||||
|
public function note(array|string $note): self
|
||||||
|
{
|
||||||
|
$notes = is_array($note) ? $note : [$note];
|
||||||
|
|
||||||
|
$this->testCaseMethod->notes = array_unique(array_merge($this->testCaseMethod->notes, $notes));
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the covered classes or methods.
|
* Sets the covered classes or methods.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $classesOrFunctions
|
||||||
*/
|
*/
|
||||||
public function covers(string ...$classesOrFunctions): self
|
public function covers(array|string ...$classesOrFunctions): self
|
||||||
{
|
{
|
||||||
foreach ($classesOrFunctions as $classOrFunction) {
|
/** @var array<int, string> $classesOrFunctions */
|
||||||
$isClass = class_exists($classOrFunction) || trait_exists($classOrFunction);
|
$classesOrFunctions = array_reduce($classesOrFunctions, fn ($carry, $item): array => is_array($item) ? array_merge($carry, $item) : array_merge($carry, [$item]), []); // @pest-ignore-type
|
||||||
$isMethod = function_exists($classOrFunction);
|
|
||||||
|
|
||||||
if (! $isClass && ! $isMethod) {
|
foreach ($classesOrFunctions as $classOrFunction) {
|
||||||
throw new InvalidArgumentException(sprintf('No class or method named "%s" has been found.', $classOrFunction));
|
$isClass = class_exists($classOrFunction) || interface_exists($classOrFunction) || enum_exists($classOrFunction);
|
||||||
|
$isTrait = trait_exists($classOrFunction);
|
||||||
|
$isFunction = function_exists($classOrFunction);
|
||||||
|
|
||||||
|
if (! $isClass && ! $isTrait && ! $isFunction) {
|
||||||
|
throw new InvalidArgumentException(sprintf('No class, trait or method named "%s" has been found.', $classOrFunction));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isClass) {
|
if ($isClass) {
|
||||||
$this->coversClass($classOrFunction);
|
$this->coversClass($classOrFunction);
|
||||||
|
} elseif ($isTrait) {
|
||||||
|
$this->coversTrait($classOrFunction);
|
||||||
} else {
|
} else {
|
||||||
$this->coversFunction($classOrFunction);
|
$this->coversFunction($classOrFunction);
|
||||||
}
|
}
|
||||||
@ -346,7 +547,41 @@ final class TestCall
|
|||||||
public function coversClass(string ...$classes): self
|
public function coversClass(string ...$classes): self
|
||||||
{
|
{
|
||||||
foreach ($classes as $class) {
|
foreach ($classes as $class) {
|
||||||
$this->testCaseMethod->covers[] = new CoversClass($class);
|
$this->testCaseFactoryAttributes[] = new Attribute(
|
||||||
|
\PHPUnit\Framework\Attributes\CoversClass::class,
|
||||||
|
[$class],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var ConfigurationRepository $configurationRepository */
|
||||||
|
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
|
||||||
|
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
|
||||||
|
|
||||||
|
if (! is_array($paths)) {
|
||||||
|
$configurationRepository->globalConfiguration('default')->class(...$classes); // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the covered classes.
|
||||||
|
*/
|
||||||
|
public function coversTrait(string ...$traits): self
|
||||||
|
{
|
||||||
|
foreach ($traits as $trait) {
|
||||||
|
$this->testCaseFactoryAttributes[] = new Attribute(
|
||||||
|
\PHPUnit\Framework\Attributes\CoversTrait::class,
|
||||||
|
[$trait],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var ConfigurationRepository $configurationRepository */
|
||||||
|
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
|
||||||
|
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
|
||||||
|
|
||||||
|
if (! is_array($paths)) {
|
||||||
|
$configurationRepository->globalConfiguration('default')->class(...$traits); // @phpstan-ignore-line
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -358,7 +593,10 @@ final class TestCall
|
|||||||
public function coversFunction(string ...$functions): self
|
public function coversFunction(string ...$functions): self
|
||||||
{
|
{
|
||||||
foreach ($functions as $function) {
|
foreach ($functions as $function) {
|
||||||
$this->testCaseMethod->covers[] = new CoversFunction($function);
|
$this->testCaseFactoryAttributes[] = new Attribute(
|
||||||
|
\PHPUnit\Framework\Attributes\CoversFunction::class,
|
||||||
|
[$function],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -369,7 +607,10 @@ final class TestCall
|
|||||||
*/
|
*/
|
||||||
public function coversNothing(): self
|
public function coversNothing(): self
|
||||||
{
|
{
|
||||||
$this->testCaseMethod->covers = [new CoversNothing()];
|
$this->testCaseMethod->attributes[] = new Attribute(
|
||||||
|
\PHPUnit\Framework\Attributes\CoversNothing::class,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@ -420,10 +661,11 @@ final class TestCall
|
|||||||
if ($this->descriptionLess) {
|
if ($this->descriptionLess) {
|
||||||
Exporter::default();
|
Exporter::default();
|
||||||
|
|
||||||
if ($this->testCaseMethod->description !== null) {
|
if ($this->description !== null) {
|
||||||
$this->testCaseMethod->description .= ' → ';
|
$this->description .= ' → ';
|
||||||
}
|
}
|
||||||
$this->testCaseMethod->description .= $arguments === null
|
|
||||||
|
$this->description .= $arguments === null
|
||||||
? $name
|
? $name
|
||||||
: sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
|
: sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
|
||||||
}
|
}
|
||||||
@ -436,11 +678,21 @@ final class TestCall
|
|||||||
*/
|
*/
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
{
|
{
|
||||||
|
if ($this->description === null) {
|
||||||
|
throw new TestDescriptionMissing($this->filename);
|
||||||
|
}
|
||||||
|
|
||||||
if (! is_null($this->describing)) {
|
if (! is_null($this->describing)) {
|
||||||
$this->testCaseMethod->describing = $this->describing;
|
$this->testCaseMethod->describing = $this->describing;
|
||||||
$this->testCaseMethod->description = Str::describe($this->describing, $this->testCaseMethod->description); // @phpstan-ignore-line
|
$this->testCaseMethod->description = Str::describe($this->describing, $this->description);
|
||||||
|
} else {
|
||||||
|
$this->testCaseMethod->description = $this->description;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->testSuite->tests->set($this->testCaseMethod);
|
$this->testSuite->tests->set($this->testCaseMethod);
|
||||||
|
|
||||||
|
if (! is_null($testCase = $this->testSuite->tests->get($this->filename))) {
|
||||||
|
$testCase->attributes = array_merge($testCase->attributes, $this->testCaseFactoryAttributes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,11 +48,14 @@ final class UsesCall
|
|||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly string $filename,
|
private readonly string $filename,
|
||||||
private readonly array $classAndTraits
|
private array $classAndTraits
|
||||||
) {
|
) {
|
||||||
$this->targets = [$filename];
|
$this->targets = [$filename];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use `pest()->printer()->compact()` instead.
|
||||||
|
*/
|
||||||
public function compact(): self
|
public function compact(): self
|
||||||
{
|
{
|
||||||
DefaultPrinter::compact(true);
|
DefaultPrinter::compact(true);
|
||||||
@ -61,10 +64,29 @@ final class UsesCall
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The directories or file where the
|
* Specifies the class or traits to use.
|
||||||
* class or traits should be used.
|
*
|
||||||
|
* @alias extend
|
||||||
*/
|
*/
|
||||||
public function in(string ...$targets): void
|
public function use(string ...$classAndTraits): self
|
||||||
|
{
|
||||||
|
return $this->extend(...$classAndTraits);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the class or traits to use.
|
||||||
|
*/
|
||||||
|
public function extend(string ...$classAndTraits): self
|
||||||
|
{
|
||||||
|
$this->classAndTraits = array_merge($this->classAndTraits, array_values($classAndTraits));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The directories or file where the class or traits should be used.
|
||||||
|
*/
|
||||||
|
public function in(string ...$targets): self
|
||||||
{
|
{
|
||||||
$targets = array_map(function (string $path): string {
|
$targets = array_map(function (string $path): string {
|
||||||
$startChar = DIRECTORY_SEPARATOR;
|
$startChar = DIRECTORY_SEPARATOR;
|
||||||
@ -78,7 +100,7 @@ final class UsesCall
|
|||||||
return str_starts_with($path, $startChar)
|
return str_starts_with($path, $startChar)
|
||||||
? $path
|
? $path
|
||||||
: implode(DIRECTORY_SEPARATOR, [
|
: implode(DIRECTORY_SEPARATOR, [
|
||||||
dirname($this->filename),
|
is_dir($this->filename) ? $this->filename : dirname($this->filename),
|
||||||
$path,
|
$path,
|
||||||
]);
|
]);
|
||||||
}, $targets);
|
}, $targets);
|
||||||
@ -92,6 +114,8 @@ final class UsesCall
|
|||||||
|
|
||||||
return $accumulator;
|
return $accumulator;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,7 +6,7 @@ namespace Pest;
|
|||||||
|
|
||||||
function version(): string
|
function version(): string
|
||||||
{
|
{
|
||||||
return '2.32.0';
|
return '3.0.8';
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDirectory(string $file = ''): string
|
function testDirectory(string $file = ''): string
|
||||||
|
|||||||
31
src/Plugins/Actions/CallsHandleOriginalArguments.php
Normal file
31
src/Plugins/Actions/CallsHandleOriginalArguments.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Actions;
|
||||||
|
|
||||||
|
use Pest\Contracts\Plugins;
|
||||||
|
use Pest\Plugin\Loader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class CallsHandleOriginalArguments
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Executes the Plugin action.
|
||||||
|
*
|
||||||
|
* Transform the input arguments by passing it to the relevant plugins.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $argv
|
||||||
|
*/
|
||||||
|
public static function execute(array $argv): void
|
||||||
|
{
|
||||||
|
$plugins = Loader::getPlugins(Plugins\HandlesOriginalArguments::class);
|
||||||
|
|
||||||
|
/** @var Plugins\HandlesOriginalArguments $plugin */
|
||||||
|
foreach ($plugins as $plugin) {
|
||||||
|
$plugin->handleOriginalArguments($argv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,20 +10,20 @@ use Pest\Plugin\Loader;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class CallsShutdown
|
final class CallsTerminable
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Executes the Plugin action.
|
* Executes the Plugin action.
|
||||||
*
|
*
|
||||||
* Provides an opportunity for any plugins to shutdown.
|
* Provides an opportunity for any plugins to terminate.
|
||||||
*/
|
*/
|
||||||
public static function execute(): void
|
public static function execute(): void
|
||||||
{
|
{
|
||||||
$plugins = Loader::getPlugins(Plugins\Shutdownable::class);
|
$plugins = Loader::getPlugins(Plugins\Terminable::class);
|
||||||
|
|
||||||
/** @var Plugins\Shutdownable $plugin */
|
/** @var Plugins\Terminable $plugin */
|
||||||
foreach ($plugins as $plugin) {
|
foreach ($plugins as $plugin) {
|
||||||
$plugin->shutdown();
|
$plugin->terminate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,6 +6,10 @@ namespace Pest\Plugins;
|
|||||||
|
|
||||||
use Pest\Contracts\Plugins\HandlesArguments;
|
use Pest\Contracts\Plugins\HandlesArguments;
|
||||||
use Pest\Plugins\Concerns\HandleArguments;
|
use Pest\Plugins\Concerns\HandleArguments;
|
||||||
|
use PHPUnit\TextUI\CliArguments\Builder as CliConfigurationBuilder;
|
||||||
|
use PHPUnit\TextUI\CliArguments\XmlConfigurationFileFinder;
|
||||||
|
use PHPUnit\TextUI\XmlConfiguration\DefaultConfiguration;
|
||||||
|
use PHPUnit\TextUI\XmlConfiguration\Loader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@ -31,9 +35,19 @@ final class Cache implements HandlesArguments
|
|||||||
public function handleArguments(array $arguments): array
|
public function handleArguments(array $arguments): array
|
||||||
{
|
{
|
||||||
if (! $this->hasArgument('--cache-directory', $arguments)) {
|
if (! $this->hasArgument('--cache-directory', $arguments)) {
|
||||||
$arguments = $this->pushArgument('--cache-directory', $arguments);
|
|
||||||
|
|
||||||
$arguments = $this->pushArgument((string) realpath(self::TEMPORARY_FOLDER), $arguments);
|
$cliConfiguration = (new CliConfigurationBuilder)->fromParameters([]);
|
||||||
|
$configurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration);
|
||||||
|
$xmlConfiguration = DefaultConfiguration::create();
|
||||||
|
|
||||||
|
if (is_string($configurationFile)) {
|
||||||
|
$xmlConfiguration = (new Loader)->load($configurationFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $xmlConfiguration->phpunit()->hasCacheDirectory()) {
|
||||||
|
$arguments = $this->pushArgument('--cache-directory', $arguments);
|
||||||
|
$arguments = $this->pushArgument((string) realpath(self::TEMPORARY_FOLDER), $arguments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->hasArgument('--parallel', $arguments)) {
|
if (! $this->hasArgument('--parallel', $arguments)) {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ trait HandleArguments
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str_starts_with($arg, "$argument=")) {
|
if (str_starts_with((string) $arg, "$argument=")) { // @phpstan-ignore-line
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user