mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 15:57:21 +01:00
Compare commits
615 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4aab77a34 | |||
| 709ecb1ba2 | |||
| 6afb36519d | |||
| 150bb9478d | |||
| bf3178473d | |||
| d2eb94d723 | |||
| 9688b83a3d | |||
| 675372c794 | |||
| c18636b3d5 | |||
| 145294a4a3 | |||
| c2cabaeae6 | |||
| 918a8fc169 | |||
| 5d32dd0641 | |||
| 982353fb38 | |||
| 2eefa8b88d | |||
| 787d5492ac | |||
| 06a0bd9b0b | |||
| 91afc81222 | |||
| 179d46ce97 | |||
| fa2bc1e536 | |||
| eaeb133c77 | |||
| cf57ea1f94 | |||
| 0b7f4f2384 | |||
| 2903a7e621 | |||
| b8964375c7 | |||
| bdcb883829 | |||
| 8a7e7f39ef | |||
| 67f217852c | |||
| 1bad148487 | |||
| e24f137b8e | |||
| 6d9189f3f5 | |||
| 6968094e2b | |||
| 9510d4a2f9 | |||
| cd2eb3504b | |||
| 7c639cdbbd | |||
| 1513ede73b | |||
| 8c65197881 | |||
| a6cd83665c | |||
| 0c57142c03 | |||
| 3f65af9fdf | |||
| 42d89814e3 | |||
| 1e3156a5b6 | |||
| 97713c0832 | |||
| 62b0e3c9df | |||
| 647de2f1cf | |||
| 0a7bff0d24 | |||
| 7618434580 | |||
| 1e0bb88b73 | |||
| 83b76d7c2e | |||
| 5a870b3940 | |||
| 1115c64186 | |||
| e38a271ca2 | |||
| 43703ab40a | |||
| 86452765a4 | |||
| b8a1b7e5cc | |||
| 5fe79d9c18 | |||
| 2744da4292 | |||
| 87f4e5e7b3 | |||
| bb3decf3cc | |||
| 4e2987d438 | |||
| a25158bce8 | |||
| 49e77b1d4c | |||
| 989e43d1a0 | |||
| 7cd42aafd8 | |||
| 48a1de273f | |||
| 970e16e949 | |||
| 432ff221c6 | |||
| a55da85dd2 | |||
| f291cd1603 | |||
| 5de0c2254a | |||
| b98ce0ced3 | |||
| 28772c2609 | |||
| 452ffaf8df | |||
| e8338405b5 | |||
| 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 | |||
| ac5d6c1f67 | |||
| 5aa3b91d56 | |||
| 9a01504b76 | |||
| 0ab636e436 | |||
| b9d2be87a2 | |||
| fef02594db | |||
| e135e2671f | |||
| 6d74965727 | |||
| 146e141b2a | |||
| 6fed7545c0 | |||
| be407ac904 | |||
| 9ce52ee7ce | |||
| 3ff41bcb68 | |||
| 5332858782 | |||
| 2b094b4188 | |||
| 3457841a9b | |||
| 5258e569c1 | |||
| abb416c2ff | |||
| dd4d5bbd4e | |||
| ab64912c70 | |||
| 1506d8bb27 | |||
| 5aa13b8e97 | |||
| b143ed7aac | |||
| 26dd5f298f | |||
| d939ee938e | |||
| 515de3972f | |||
| bf573b3cac | |||
| 53dc9ffa06 | |||
| 04d2fa5ce8 | |||
| 7764a7a162 | |||
| 727a427837 | |||
| b1c59ec2e6 | |||
| f69a3cf832 | |||
| ed0bf1786f | |||
| 2d1d8a81e1 | |||
| d515cf965e | |||
| dc1e4f040d | |||
| 5e1e701ce5 | |||
| f004591c5a | |||
| 86a96dd157 | |||
| 97dc32f9d2 | |||
| a3ab065343 | |||
| c390721ac3 | |||
| f83d758d4b | |||
| e00aba539a | |||
| 7799500d06 | |||
| c099991cd9 | |||
| e27d2e7394 | |||
| 14fb992ef2 | |||
| 4550a344d3 | |||
| 8efd25ef65 | |||
| 117694f210 | |||
| e5dc6f0ae2 | |||
| 8f738f5d49 | |||
| 1e2ca40c5b | |||
| 4522cb5dcb | |||
| 9ee4191020 | |||
| cc65009d0a | |||
| 453133d382 | |||
| dd0dddffd4 | |||
| 9a8f6e6414 | |||
| 4ece95a040 | |||
| 0cc09380bc | |||
| 809fb855de | |||
| aa14f2e200 | |||
| e319bdb6d3 | |||
| fb7340b556 | |||
| 0528fec083 | |||
| 1cbaaf6e12 | |||
| dc862f60b2 | |||
| ff04d54247 | |||
| 330cf05177 | |||
| 42b5fa914c | |||
| 3b1026b7d7 | |||
| b6151e0d01 | |||
| d6db2c13c1 | |||
| 07b6ff6c04 | |||
| ac5da9e3f7 | |||
| 90fb8c602c | |||
| 3974a65a18 | |||
| 2a54b5819d | |||
| 8be46b57a0 | |||
| 7177791f1e | |||
| c743b10a87 | |||
| 83f8de17c8 | |||
| da20a62e49 | |||
| c8d3e1a9fa | |||
| f7705fe1c1 | |||
| 4f35dbc607 | |||
| 2e01776272 | |||
| cf23dfa477 | |||
| ab4787c667 | |||
| bd6b166a62 | |||
| 17340947b3 | |||
| f235d84d95 | |||
| 3c0d780696 | |||
| 16768fca9f | |||
| 95ec0a82b2 | |||
| 15cd7187e9 | |||
| 0a680dd06e | |||
| 152892cc38 | |||
| 9aad417fb2 | |||
| b58e0cba66 | |||
| 74864c60e1 | |||
| fd4f161edd | |||
| e0939e3e99 | |||
| 2cbecd10e6 | |||
| 2cdd5e3ba0 | |||
| 811ef27ee4 | |||
| 22a7fd0656 | |||
| 698c276cbe | |||
| 6340656ece | |||
| 2d5840f947 | |||
| b8bb3684a3 | |||
| b8cd563569 | |||
| 9fb64599de | |||
| 502f37d280 | |||
| 29cfa8ec35 | |||
| 86c107ae5e | |||
| a63cd2e4f5 | |||
| 7249b59e52 | |||
| 5c94d9994e | |||
| bb0a5d8323 | |||
| b126e8e6e4 | |||
| 677129d23d | |||
| cef5c36885 | |||
| a343ba4a29 | |||
| 21b30b22a7 | |||
| 449c4b6c5e | |||
| 6513ad6ced | |||
| 12421c846e | |||
| a312cecede | |||
| 4be97ed314 | |||
| 5101b9dce3 | |||
| 67e452e9ed | |||
| ecff90da1c | |||
| 2ffafd445d | |||
| 6068ef6150 | |||
| 3ee5c29a00 | |||
| 8c0b933fcd | |||
| 991e02649a | |||
| 79f5973e5a | |||
| 37c40cb735 | |||
| 28ee2917f1 | |||
| a8b785f69e | |||
| 56610d886d | |||
| be0d9e964b | |||
| 6bc9da3fe1 | |||
| 6f54462070 | |||
| 876629b744 | |||
| 5e74e5a19d | |||
| 0d114e21fd | |||
| 95b65fe72b | |||
| bc08f2cb55 | |||
| 6c73a3d90b | |||
| c08f33638a | |||
| 6c93390c9c | |||
| b53e396aac | |||
| 8b327aa8b4 | |||
| d0c6f9bc60 | |||
| b5e066939b | |||
| 7892237408 | |||
| 74df53c72b | |||
| ee26457705 | |||
| 09e6a0944a | |||
| bdee46043a | |||
| 3e25168777 | |||
| 21b8507252 | |||
| d8e283777e | |||
| 2b0aa4b9c9 | |||
| 040eb8142d | |||
| d1aeabc9da | |||
| e4ec2b3efa | |||
| dedcc6b887 | |||
| 2b0ed2bc45 | |||
| 9c859ae7c4 | |||
| ae0a230046 | |||
| 644fade478 | |||
| c9e919dd40 | |||
| 42323e27b1 | |||
| 3927177b23 | |||
| 038fd80428 | |||
| cc6c5bf199 | |||
| b88d9e8ff2 | |||
| 0fc232bbc7 | |||
| 7dcd42d113 | |||
| e79ffc6bad | |||
| 8ea425b266 | |||
| 3a0f6a1d09 | |||
| b9b90295fa | |||
| 9dabecacbf | |||
| 04fa6b6372 | |||
| a0d2856f51 | |||
| bbac28c9f4 | |||
| e888f3613b | |||
| e69899559d | |||
| e6fe968d44 | |||
| 678898efe7 | |||
| 6c3d8829ce | |||
| 8ea7b2b802 |
42
.github/workflows/integration-tests.yml
vendored
42
.github/workflows/integration-tests.yml
vendored
@ -1,42 +0,0 @@
|
|||||||
name: Integration Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
ci:
|
|
||||||
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
||||||
php: ['8.1', '8.2']
|
|
||||||
dependency-version: [prefer-lowest, prefer-stable]
|
|
||||||
|
|
||||||
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: ${{ matrix.php }}
|
|
||||||
tools: composer:v2
|
|
||||||
coverage: none
|
|
||||||
|
|
||||||
- name: Setup Problem Matches
|
|
||||||
run: |
|
|
||||||
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
|
|
||||||
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
|
||||||
|
|
||||||
- name: Install PHP dependencies
|
|
||||||
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
|
|
||||||
|
|
||||||
- name: Integration Tests
|
|
||||||
run: composer test:integration
|
|
||||||
|
|
||||||
9
.github/workflows/static.yml
vendored
9
.github/workflows/static.yml
vendored
@ -13,25 +13,26 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
dependency-version: [prefer-lowest, prefer-stable]
|
dependency-version: [prefer-lowest, prefer-stable]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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
|
||||||
|
|||||||
23
.github/workflows/tests.yml
vendored
23
.github/workflows/tests.yml
vendored
@ -3,25 +3,25 @@ name: Tests
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
tests:
|
||||||
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
|
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
php: ['8.1', '8.2', '8.3']
|
symfony: ['7.1']
|
||||||
dependency-version: [prefer-lowest, prefer-stable]
|
php: ['8.2', '8.3', '8.4']
|
||||||
|
dependency_version: [prefer-lowest, prefer-stable]
|
||||||
|
|
||||||
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}
|
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
@ -36,11 +36,14 @@ jobs:
|
|||||||
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||||
|
|
||||||
- name: Install PHP dependencies
|
- name: Install PHP dependencies
|
||||||
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
|
shell: bash
|
||||||
|
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
|
||||||
|
|
||||||
- name: Unit Tests
|
- name: Unit Tests
|
||||||
run: composer test:unit
|
run: composer test:unit
|
||||||
|
|
||||||
- name: Unit Tests in Parallel
|
- name: Parallel Tests
|
||||||
run: composer test:parallel
|
run: composer test:parallel
|
||||||
|
|
||||||
|
- name: Integration Tests
|
||||||
|
run: composer test:integration
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,3 +12,5 @@ coverage.xml
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.STREAM.md
|
||||||
|
|
||||||
|
|||||||
256
CHANGELOG.md
256
CHANGELOG.md
@ -1,256 +0,0 @@
|
|||||||
# Release Notes for 2.x
|
|
||||||
|
|
||||||
## Unreleased
|
|
||||||
|
|
||||||
## [v2.16.0 (2023-08-21)](https://github.com/pestphp/pest/compare/v2.15.0...v2.16.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `toBeDigits` ([#911](https://github.com/pestphp/pest/pull/911))
|
|
||||||
- `toBeCamelCase`, `toBeKebabCase`, `toBeSnakeCase`, `toBeStudlyCase`, `toHaveSnakeCaseKeys`, `toHaveKebabCaseKeys`, `toHaveCamelCaseKeys`, `toHaveStudlyCaseKeys`` ([#921](https://github.com/pestphp/pest/pull/921))
|
|
||||||
- native functions support on `arch` expectations, e.g: `expect('sleep')->toBeUsed();` ([#4](https://github.com/pestphp/pest-plugin-arch/pull/4))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- `phpunit.xml` stub ([#915](https://github.com/pestphp/pest/pull/915))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Nested sequences ([#895](https://github.com/pestphp/pest/pull/895))
|
|
||||||
|
|
||||||
## [v2.15.0 (2023-08-17)](https://github.com/pestphp/pest/compare/v2.14.1...v2.15.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- PHP 8.3 support ([0b261ef](https://github.com/pestphp/pest/commit/0b261ef97b7ceed20cbeeb2b0b41e08e0a8fcaa1))
|
|
||||||
|
|
||||||
## [v2.14.1 (2023-08-16)](https://github.com/pestphp/pest/compare/v2.14.0...v2.14.1)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Bumps PHPUnit to `^10.3.2` ([e012517](https://github.com/pestphp/pest/commit/e012517b1643002b36a68096f4a5e26682b1e175))
|
|
||||||
|
|
||||||
## [v2.14.0 (2023-08-14)](https://github.com/pestphp/pest/compare/v2.13.0...v2.14.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `toBeUppercase()`, `toBeLowercase()`, `toBeAlphaNumeric()`, `toBeAlpha()` ([#906](https://github.com/pestphp/pest/pull/906))
|
|
||||||
|
|
||||||
## [v2.13.0 (2023-08-09)](https://github.com/pestphp/pest/compare/v2.12.2...v2.13.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `expect()->ddWhen` and `expect()->ddUnless` ([306b7eb](https://github.com/pestphp/pest/commit/306b7eb2a6a57e570d58228b46501ad9ba4062b4))
|
|
||||||
|
|
||||||
## [v2.12.2 (2023-08-07)](https://github.com/pestphp/pest/compare/v2.12.0...v2.12.2)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Running tests from `uses` parent class ([#898](https://github.com/pestphp/pest/pull/898))
|
|
||||||
|
|
||||||
## [v2.12.0 (2023-08-02)](https://github.com/pestphp/pest/compare/v2.11.0...v2.12.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Allows multiple `toMatchSnapshot` per test ([#881](https://github.com/pestphp/pest/pull/881))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Bumps PHPUnit to `^10.2.7` ([43107c1](https://github.com/pestphp/pest/commit/43107c17436e41e23018ae31705c688168c14784))
|
|
||||||
|
|
||||||
## [v2.11.0 (2023-08-01)](https://github.com/pestphp/pest/compare/v2.10.1...v2.11.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `toBeInvokable`expectation ([#891](https://github.com/pestphp/pest/pull/891))
|
|
||||||
|
|
||||||
## [v2.10.1 (2023-07-31)](https://github.com/pestphp/pest/compare/v2.10.0...v2.10.1)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `not->toHaveSuffix` and `toHavePrefix` expectations ([#888](https://github.com/pestphp/pest/pull/888))
|
|
||||||
|
|
||||||
## [v2.10.0 (2023-07-31)](https://github.com/pestphp/pest/compare/v2.9.5...v2.10.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `repeat` feature ([f3f35a2](https://github.com/pestphp/pest/commit/f3f35a2ed119f63eefd323a8c66d3387e908df3f))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `-v` option ([86a6b32](https://github.com/pestphp/pest/commit/86a6b3271518742dc39761228687a5107551d279))
|
|
||||||
|
|
||||||
## [v2.9.5 (2023-07-22)](https://github.com/pestphp/pest/compare/v2.9.4...v2.9.5)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Assertions count on arch expectations ([632ffc2](https://github.com/pestphp/pest/commit/632ffc2f8e1fe45f739b12b818426ae14700079e))
|
|
||||||
|
|
||||||
## [v2.9.4 (2023-07-22)](https://github.com/pestphp/pest/compare/v2.9.3...v2.9.4)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Output on `describe` `beforeEach` failure ([5637dfa](https://github.com/pestphp/pest/commit/5637dfa75d1a331adc810935536cde7c3196af06))
|
|
||||||
|
|
||||||
## [v2.9.3 (2023-07-20)](https://github.com/pestphp/pest/compare/v2.9.2...v2.9.3)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Snapshots directory on Windows environments ([cf52752](https://github.com/pestphp/pest/commit/cf5275293fe693ec2cf4dbadbadae01daaa08169))
|
|
||||||
|
|
||||||
## [v2.9.2 (2023-07-20)](https://github.com/pestphp/pest/compare/v2.9.1...v2.9.2)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `beforeEach` on Windows environments ([a37a3b9](https://github.com/pestphp/pest/commit/a37a3b9f9931bc1ab1ea9e1d6d38dfb55dde3f74))
|
|
||||||
|
|
||||||
## [v2.9.1 (2023-07-20)](https://github.com/pestphp/pest/compare/v2.9.0...v2.9.1)
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
|
|
||||||
- Bumps PHPUnit to `^10.2.6` ([8fdb0b3](https://github.com/pestphp/pest/commit/8fdb0b3d32ce9ee12bd182f22751c2b41a53e97b))
|
|
||||||
|
|
||||||
## [v2.9.0 (2023-07-19)](https://github.com/pestphp/pest/compare/v2.8.1...v2.9.0)
|
|
||||||
|
|
||||||
> "Spicy Summer" is the codename assigned to Pest 2.9, for full details check our announcement: [https://pestphp.com/docs/pest-spicy-summer-release](https://pestphp.com/docs/pest-spicy-summer-release)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Built-in Snapshot Testing ([c828756](https://github.com/pestphp/pest/commit/c8287567eb8c3dbea5845b2a6f70804b094b4b60))
|
|
||||||
- Describe Blocks ([c828756](https://github.com/pestphp/pest/commit/c8287567eb8c3dbea5845b2a6f70804b094b4b60))
|
|
||||||
- Architectural Testing++ ([c828756](https://github.com/pestphp/pest/commit/c8287567eb8c3dbea5845b2a6f70804b094b4b60))
|
|
||||||
- Type Coverage Plugin ([c828756](https://github.com/pestphp/pest/commit/c8287567eb8c3dbea5845b2a6f70804b094b4b60))
|
|
||||||
- Drift Plugin ([c828756](https://github.com/pestphp/pest/commit/c8287567eb8c3dbea5845b2a6f70804b094b4b60))
|
|
||||||
|
|
||||||
## [v2.8.1 (2023-06-20)](https://github.com/pestphp/pest/compare/v2.8.0...v2.8.1)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Fixes "Cannot find TestCase object on call stack" ([eb7bb34](https://github.com/pestphp/pest/commit/eb7bb348253f412e806a6ba6f0df46c0435d0dfe))
|
|
||||||
|
|
||||||
## [v2.8.0 (2023-06-19)](https://github.com/pestphp/pest/compare/v2.7.0...v2.8.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Support for `globs` in `uses` ([#829](https://github.com/pestphp/pest/pull/829))
|
|
||||||
|
|
||||||
## [v2.7.0 (2023-06-15)](https://github.com/pestphp/pest/compare/v2.6.3...v2.7.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Support for unexpected output on printer ([eb9f31e](https://github.com/pestphp/pest/commit/eb9f31edeb00a88c449874f3d48156128a00fff8))
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
- Bumps PHPUnit to `^10.2.2` ([0e5470b](https://github.com/pestphp/pest/commit/0e5470b192b259ba2db7c02a50371216c98fc0a6))
|
|
||||||
|
|
||||||
## [v2.6.3 (2023-06-07)](https://github.com/pestphp/pest/compare/v2.6.2...v2.6.3)
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
- Bumps PHPUnit to `^10.2.1` ([73a859e](https://github.com/pestphp/pest/commit/73a859ee563fe96944ba39b191dceca28ef703c2))
|
|
||||||
|
|
||||||
## [v2.6.2 (2023-06-02)](https://github.com/pestphp/pest/compare/v2.6.1...v2.6.2)
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
- Bumps PHPUnit to `^10.2.0` ([a0041f1](https://github.com/pestphp/pest/commit/a0041f139cba94fe5d15318c38e275f2e2fb3350))
|
|
||||||
|
|
||||||
## [v2.6.1 (2023-04-12)](https://github.com/pestphp/pest/compare/v2.6.0...v2.6.1)
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
- PHPStorm issue output problem for tests throwing an exception before the first assertion ([#809](https://github.com/pestphp/pest/pull/809))
|
|
||||||
- Allow traits to be covered ([#804](https://github.com/pestphp/pest/pull/804))
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
- Bumps PHPUnit to `^10.1.3` ([c993252](https://github.com/pestphp/pest/commit/c99325275acf1fd3759b487b93ec50473f706709))
|
|
||||||
|
|
||||||
## [v2.6.0 (2023-04-05)](https://github.com/pestphp/pest/compare/v2.5.2...v2.6.0)
|
|
||||||
|
|
||||||
### Adds
|
|
||||||
- Allows `toThrow` to be used against an exception instance ([#797](https://github.com/pestphp/pest/pull/797))
|
|
||||||
|
|
||||||
## [v2.5.2 (2023-04-19)](https://github.com/pestphp/pest/compare/v2.5.1...v2.5.2)
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
- Removes `myclabs/php-enuma` dependency ([1a05df1](https://github.com/pestphp/pest/commit/1a05df14d0ce7d12583df26ff716807db6f81f13))
|
|
||||||
|
|
||||||
## [v2.5.1 (2023-04-18)](https://github.com/pestphp/pest/compare/v2.5.0...v2.5.1)
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
- Bumps PHPUnit to `^10.1.1` ([ec6a817](https://github.com/pestphp/pest/commit/ec6a81735af19f5463d24545df97535d77697ec6))
|
|
||||||
|
|
||||||
## [v2.5.0 (2023-04-14)](https://github.com/pestphp/pest/compare/v2.4.0...v2.5.0)
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
- Bumps PHPUnit to `^10.1.0` ([#780](https://github.com/pestphp/pest/pull/780))
|
|
||||||
|
|
||||||
## [v2.4.0 (2023-04-03)](https://github.com/pestphp/pest/compare/v2.3.0...v2.4.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- `skipOnWindows()`, `skipOnMac()`, and `skipOnLinux()` ([#757](https://github.com/pestphp/pest/pull/757))
|
|
||||||
- source architecture testing violation ([#1](https://github.com/pestphp/pest-plugin-arch/pull/1))([8e66263](https://github.com/pestphp/pest-plugin-arch/commit/8e66263104304e99e3d6ceda25c7ed679b27fb03))
|
|
||||||
- `toHaveProperties` may now also check values ([#760](https://github.com/pestphp/pest/pull/760))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Tests on `tests/Helpers` directory not being executed ([#753](https://github.com/pestphp/pest/pull/753))
|
|
||||||
- Teamcity count ([#747](https://github.com/pestphp/pest/pull/747))
|
|
||||||
- Parallel execution when class extends class with same name ([#748](https://github.com/pestphp/pest/pull/748))
|
|
||||||
- Wording on `uses()` hint ([#745](https://github.com/pestphp/pest/pull/745/files))
|
|
||||||
|
|
||||||
## [v2.3.0 (2023-03-28)](https://github.com/pestphp/pest/compare/v2.2.3...v2.3.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Better error handler about missing uses ([#743](https://github.com/pestphp/pest/pull/743))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Inconsistent spelling of `dataset` ([#739](https://github.com/pestphp/pest/pull/739))
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
- Bumps PHPUnit to `^10.0.19` ([3d7e621](https://github.com/pestphp/pest/commit/3d7e621b7dfc03f0b2d9dcf6eb06c26bc383f502))
|
|
||||||
|
|
||||||
## [v2.2.3 (2023-03-24)](https://github.com/pestphp/pest/compare/v2.2.2...v2.2.3)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Unnecessary dataset on dataset arguments mismatch ([#736](https://github.com/pestphp/pest/pull/736))
|
|
||||||
- Parallel arguments on plugins order ([#703](https://github.com/pestphp/pest/pull/703))
|
|
||||||
- Arch plugin runtime exceptions on bad phpdocs ([2f2b51c](https://github.com/pestphp/pest/commit/2f2b51ce3d1b000be9d6add0e785fd0044931b3b))
|
|
||||||
|
|
||||||
## [v2.2.2 (2023-03-23)](https://github.com/pestphp/pest/compare/v2.2.1...v2.2.2)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Edge case in parallel execution test description ([3ce6408](https://github.com/pestphp/pest/commit/3ce640819541ca6022b250e000f336d87c3e7889))
|
|
||||||
|
|
||||||
## [v2.2.1 (2023-03-22)](https://github.com/pestphp/pest/compare/v2.2.0...v2.2.1)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Collision between tests names with underscores ([#724](https://github.com/pestphp/pest/pull/724))
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
- Bumps PHPUnit to `^10.0.18` ([1408cff](https://github.com/pestphp/pest/commit/1408cffc028690057e44f00038f9390f776e6bfb))
|
|
||||||
|
|
||||||
## [v2.2.0 (2023-03-22)](https://github.com/pestphp/pest/compare/v2.1.0...v2.2.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Improved error messages on dataset arguments mismatch ([#698](https://github.com/pestphp/pest/pull/698))
|
|
||||||
- Allows the usage of `DateTimeInterface` on multiple expectations ([#716](https://github.com/pestphp/pest/pull/716))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- `--dirty` option on Windows environments ([#721](https://github.com/pestphp/pest/pull/721))
|
|
||||||
- Parallel exit code when `phpunit.xml` is outdated ([14dd5cb](https://github.com/pestphp/pest/commit/14dd5cb57b9432300ac4e8095f069941cb43bdb5))
|
|
||||||
|
|
||||||
## [v2.1.0 (2023-03-21)](https://github.com/pestphp/pest/compare/v2.0.2...v2.1.0)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- `only` test case method ([bcd1503](https://github.com/pestphp/pest/commit/bcd1503cade938853a55c1283b02b6b820ea0b69))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Issues with different characters on test names ([715](https://github.com/pestphp/pest/pull/715))
|
|
||||||
|
|
||||||
## [v2.0.2 (2023-03-20)](https://github.com/pestphp/pest/compare/v2.0.1...v2.0.2)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- `Pest.php` not being loaded in certain scenarios ([b887116](https://github.com/pestphp/pest/commit/b887116e5ce9a69403ad620cad20f0a029474eb5))
|
|
||||||
|
|
||||||
## [v2.0.1 (2023-03-20)](https://github.com/pestphp/pest/compare/v2.0.0...v2.0.1)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Wrong `version` configuration key on `composer.json` ([8f91f40](https://github.com/pestphp/pest/commit/8f91f40e8ea8b35e04b7989bed6a8f9439e2a2d6))
|
|
||||||
|
|
||||||
## [v2.0.0 (2023-03-20)](https://github.com/pestphp/pest/compare/v1.22.6...v2.0.0)
|
|
||||||
|
|
||||||
Please consult the [upgrade guide](https://pestphp.com/docs/upgrade-guide) and [release notes](https://pestphp.com/docs/announcing-pest2) in the official Pest documentation.
|
|
||||||
@ -42,7 +42,7 @@ composer test
|
|||||||
|
|
||||||
Check types:
|
Check types:
|
||||||
```bash
|
```bash
|
||||||
composer test:types
|
composer test:type:check
|
||||||
```
|
```
|
||||||
|
|
||||||
Unit tests:
|
Unit tests:
|
||||||
@ -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.
|
||||||
|
|||||||
35
README.md
35
README.md
@ -1,7 +1,7 @@
|
|||||||
<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=3.x&label=Tests%203.x"></a>
|
||||||
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
|
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
|
||||||
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a>
|
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a>
|
||||||
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
|
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
|
||||||
@ -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)**
|
||||||
@ -19,21 +22,27 @@
|
|||||||
|
|
||||||
We cannot thank our sponsors enough for their incredible support in funding Pest's development. Their contributions have been instrumental in making Pest the best it can be. For those who are interested in becoming a sponsor, please visit Nuno Maduro's Sponsor page at **[github.com/sponsors/nunomaduro](https://github.com/sponsors/nunomaduro)**.
|
We cannot thank our sponsors enough for their incredible support in funding Pest's development. Their contributions have been instrumental in making Pest the best it can be. For those who are interested in becoming a sponsor, please visit Nuno Maduro's Sponsor page at **[github.com/sponsors/nunomaduro](https://github.com/sponsors/nunomaduro)**.
|
||||||
|
|
||||||
|
|
||||||
### Platinum Sponsors
|
### Platinum Sponsors
|
||||||
|
|
||||||
- **[Forge](https://forge.laravel.com)**
|
- **[Laracasts](https://laracasts.com/?ref=pestphp)**
|
||||||
- **[LoadForge](https://loadforge.com)**
|
|
||||||
- **[Spatie](https://spatie.be)**
|
### Gold Sponsors
|
||||||
- **[Worksome](https://www.worksome.com/)**
|
|
||||||
|
- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
|
||||||
|
- **[LaraJobs](https://larajobs.com/?ref=pestphp)**
|
||||||
|
- **[Brokerchooser](https://brokerchooser.com/?ref=pestphp)**
|
||||||
|
- **[Forge](https://forge.laravel.com/?ref=pestphp)**
|
||||||
|
|
||||||
### Premium Sponsors
|
### Premium Sponsors
|
||||||
|
|
||||||
- [Akaunting](https://akaunting.com)
|
- [Akaunting](https://akaunting.com/?ref=pestphp)
|
||||||
- [Codecourse](https://codecourse.com/)
|
- [Codecourse](https://codecourse.com/?ref=pestphp)
|
||||||
- [Laracasts](https://laracasts.com/)
|
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
|
||||||
- [Localazy](https://localazy.com)
|
- [Localazy](https://localazy.com/?ref=pestphp)
|
||||||
- [Fathom Analytics](https://usefathom.com/)
|
- [Route4Me](https://www.route4me.com/?ref=pestphp)
|
||||||
- [Meema](https://meema.io)
|
- [Spatie](https://spatie.be/?ref=pestphp)
|
||||||
- [Zapiet](https://www.zapiet.com)
|
- [Worksome](https://www.worksome.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,13 +2,13 @@
|
|||||||
|
|
||||||
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}...master) and update the [changelog](CHANGELOG.md) file with the main changes for this release
|
- 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 CHANGELOG and 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"`
|
||||||
- Push the changes to GitHub
|
- Push the changes to GitHub
|
||||||
- Check that the CI is passing as expected: [github.com/pestphp/pest/actions](https://github.com/pestphp/pest/actions)
|
- Check that the CI is passing as expected: [github.com/pestphp/pest/actions](https://github.com/pestphp/pest/actions)
|
||||||
- Tag and push the tag with `git tag vX.X.X && git push --tags`
|
- Tag and push the tag with `git tag vX.X.X && git push --tags`
|
||||||
|
|||||||
125
bin/pest
125
bin/pest
@ -1,9 +1,15 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php declare(strict_types=1);
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Pest\Kernel;
|
use Pest\Kernel;
|
||||||
use Pest\Panic;
|
use Pest\Panic;
|
||||||
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,39 +19,102 @@ 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]);
|
||||||
|
} elseif ($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 ($value === '--todos') {
|
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]);
|
||||||
|
} elseif ($value === '--assignee') {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
|
||||||
|
if (isset($arguments[$key + 1])) {
|
||||||
|
unset($arguments[$key + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--issue=')) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
} elseif ($value === '--issue') {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
|
||||||
|
if (isset($arguments[$key + 1])) {
|
||||||
|
unset($arguments[$key + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--ticket=')) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
} elseif ($value === '--ticket') {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
|
||||||
|
if (isset($arguments[$key + 1])) {
|
||||||
|
unset($arguments[$key + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--pr=')) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
} elseif ($value === '--pr') {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
|
||||||
|
if (isset($arguments[$key + 1])) {
|
||||||
|
unset($arguments[$key + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--pull-request=')) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
} elseif ($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']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,7 +135,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
|
|
||||||
// Get $rootPath based on $autoloadPath
|
// Get $rootPath based on $autoloadPath
|
||||||
$rootPath = dirname($autoloadPath, 2);
|
$rootPath = dirname($autoloadPath, 2);
|
||||||
$input = new ArgvInput();
|
$input = new ArgvInput;
|
||||||
|
|
||||||
$testSuite = TestSuite::getInstance(
|
$testSuite = TestSuite::getInstance(
|
||||||
$rootPath,
|
$rootPath,
|
||||||
@ -78,7 +147,31 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($todo) {
|
if ($todo) {
|
||||||
$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';
|
||||||
@ -88,9 +181,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);
|
||||||
|
|
||||||
@ -32,10 +32,13 @@ $bootPest = (static function (): void {
|
|||||||
'status-file:',
|
'status-file:',
|
||||||
'progress-file:',
|
'progress-file:',
|
||||||
'unexpected-output-file:',
|
'unexpected-output-file:',
|
||||||
'testresult-file:',
|
'test-result-file:',
|
||||||
|
'result-cache-file:',
|
||||||
'teamcity-file:',
|
'teamcity-file:',
|
||||||
'testdox-file:',
|
'testdox-file:',
|
||||||
'testdox-color',
|
'testdox-color',
|
||||||
|
'testdox-columns:',
|
||||||
|
'testdox-summary',
|
||||||
'phpunit-argv:',
|
'phpunit-argv:',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -61,7 +64,8 @@ $bootPest = (static function (): void {
|
|||||||
|
|
||||||
assert(isset($getopt['progress-file']) && is_string($getopt['progress-file']));
|
assert(isset($getopt['progress-file']) && is_string($getopt['progress-file']));
|
||||||
assert(isset($getopt['unexpected-output-file']) && is_string($getopt['unexpected-output-file']));
|
assert(isset($getopt['unexpected-output-file']) && is_string($getopt['unexpected-output-file']));
|
||||||
assert(isset($getopt['testresult-file']) && is_string($getopt['testresult-file']));
|
assert(isset($getopt['test-result-file']) && is_string($getopt['test-result-file']));
|
||||||
|
assert(! isset($getopt['result-cache-file']) || is_string($getopt['result-cache-file']));
|
||||||
assert(! isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file']));
|
assert(! isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file']));
|
||||||
assert(! isset($getopt['testdox-file']) || is_string($getopt['testdox-file']));
|
assert(! isset($getopt['testdox-file']) || is_string($getopt['testdox-file']));
|
||||||
|
|
||||||
@ -77,10 +81,12 @@ $bootPest = (static function (): void {
|
|||||||
$phpunitArgv,
|
$phpunitArgv,
|
||||||
$getopt['progress-file'],
|
$getopt['progress-file'],
|
||||||
$getopt['unexpected-output-file'],
|
$getopt['unexpected-output-file'],
|
||||||
$getopt['testresult-file'],
|
$getopt['test-result-file'],
|
||||||
|
$getopt['result-cache-file'] ?? null,
|
||||||
$getopt['teamcity-file'] ?? null,
|
$getopt['teamcity-file'] ?? null,
|
||||||
$getopt['testdox-file'] ?? null,
|
$getopt['testdox-file'] ?? null,
|
||||||
isset($getopt['testdox-color']),
|
isset($getopt['testdox-color']),
|
||||||
|
$getopt['testdox-columns'] ?? null,
|
||||||
);
|
);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@ -17,17 +17,20 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1.0",
|
"php": "^8.2.0",
|
||||||
"brianium/paratest": "^7.2.6",
|
"brianium/paratest": "^7.7.0",
|
||||||
"nunomaduro/collision": "^7.8.1",
|
"nunomaduro/collision": "^8.6.0",
|
||||||
"nunomaduro/termwind": "^1.15.1",
|
"nunomaduro/termwind": "^2.3.0",
|
||||||
"pestphp/pest-plugin": "^2.1.1",
|
"pestphp/pest-plugin": "^3.0.0",
|
||||||
"pestphp/pest-plugin-arch": "^2.3.3",
|
"pestphp/pest-plugin-arch": "^3.0.0",
|
||||||
"phpunit/phpunit": "^10.3.2"
|
"pestphp/pest-plugin-mutate": "^3.0.5",
|
||||||
|
"phpunit/phpunit": "^11.5.3"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"webmozart/assert": "<1.11.0",
|
"filp/whoops": "<2.16.0",
|
||||||
"phpunit/phpunit": ">10.3.2"
|
"phpunit/phpunit": ">11.5.3",
|
||||||
|
"sebastian/exporter": "<6.0.0",
|
||||||
|
"webmozart/assert": "<1.11.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@ -50,9 +53,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"pestphp/pest-dev-tools": "^2.16.0",
|
"pestphp/pest-dev-tools": "^3.3.0",
|
||||||
"pestphp/pest-plugin-type-coverage": "^2.2.0",
|
"pestphp/pest-plugin-type-coverage": "^3.2.3",
|
||||||
"symfony/process": "^6.3.4"
|
"symfony/process": "^7.2.0"
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
@ -72,11 +75,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=10",
|
"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",
|
||||||
@ -91,6 +94,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",
|
||||||
@ -108,6 +113,11 @@
|
|||||||
"Pest\\Plugins\\Version",
|
"Pest\\Plugins\\Version",
|
||||||
"Pest\\Plugins\\Parallel"
|
"Pest\\Plugins\\Parallel"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"phpstan": {
|
||||||
|
"includes": [
|
||||||
|
"extension.neon"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,14 @@
|
|||||||
ARG PHP=8.1
|
ARG PHP=8.1
|
||||||
FROM php:${PHP}-cli-alpine
|
FROM php:${PHP}-cli-alpine
|
||||||
|
|
||||||
RUN apk update \
|
RUN apk update && apk add \
|
||||||
&& apk add zip libzip-dev icu-dev git
|
zip libzip-dev icu-dev git
|
||||||
|
|
||||||
RUN docker-php-ext-configure zip
|
RUN docker-php-ext-install zip intl
|
||||||
RUN docker-php-ext-install zip
|
|
||||||
RUN docker-php-ext-enable zip
|
|
||||||
|
|
||||||
RUN docker-php-ext-configure intl
|
RUN apk add --no-cache linux-headers autoconf build-base
|
||||||
RUN docker-php-ext-install intl
|
|
||||||
RUN docker-php-ext-enable intl
|
|
||||||
|
|
||||||
RUN apk add --no-cache $PHPIZE_DEPS linux-headers
|
|
||||||
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
|
||||||
|
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
|||||||
4
extension.neon
Normal file
4
extension.neon
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
parameters:
|
||||||
|
universalObjectCratesClasses:
|
||||||
|
- Pest\Support\HigherOrderTapProxy
|
||||||
|
- Pest\Expectation
|
||||||
@ -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
|
||||||
@ -36,7 +68,7 @@ final class ThrowableBuilder
|
|||||||
$previous = self::from($previous);
|
$previous = self::from($previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
$trace = Filter::getFilteredStacktrace($t);
|
$trace = Filter::stackTraceFromThrowableAsString($t);
|
||||||
|
|
||||||
if ($t instanceof RenderableOnCollisionEditor && $frame = $t->toCollisionEditor()) {
|
if ($t instanceof RenderableOnCollisionEditor && $frame = $t->toCollisionEditor()) {
|
||||||
$file = $frame->getFile();
|
$file = $frame->getFile();
|
||||||
|
|||||||
458
overrides/Logging/JUnit/JunitXmlLogger.php
Normal file
458
overrides/Logging/JUnit/JunitXmlLogger.php
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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\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\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
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
$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,17 +81,12 @@ 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>
|
||||||
*/
|
*/
|
||||||
private array $times = [];
|
private array $times = [];
|
||||||
|
|
||||||
public function __construct(string $filepath = null)
|
public function __construct(?string $filepath = null)
|
||||||
{
|
{
|
||||||
if ($filepath !== null && is_dir($filepath)) {
|
if ($filepath !== null && is_dir($filepath)) {
|
||||||
$filepath .= DIRECTORY_SEPARATOR.self::DEFAULT_RESULT_CACHE_FILENAME;
|
$filepath .= DIRECTORY_SEPARATOR.self::DEFAULT_RESULT_CACHE_FILENAME;
|
||||||
@ -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);
|
||||||
})();
|
})();
|
||||||
@ -132,7 +138,7 @@ final class TestSuiteLoader
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($class->isAbstract() || ($class->getFileName() !== $suiteClassFile)) {
|
if ($class->isAbstract() || ($suiteClassFile !== $class->getFileName())) {
|
||||||
if (! str_contains($class->getFileName(), 'TestCaseFactory.php')) {
|
if (! str_contains($class->getFileName(), 'TestCaseFactory.php')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
includes:
|
includes:
|
||||||
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
||||||
- vendor/ergebnis/phpstan-rules/rules.neon
|
|
||||||
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
|
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
@ -12,12 +11,4 @@ parameters:
|
|||||||
reportUnmatchedIgnoredErrors: true
|
reportUnmatchedIgnoredErrors: true
|
||||||
|
|
||||||
ignoreErrors:
|
ignoreErrors:
|
||||||
- "#has a nullable return type declaration.#"
|
|
||||||
- "#Language construct isset\\(\\) should not be used.#"
|
|
||||||
- "#is not allowed to extend#"
|
|
||||||
- "#is concrete, but does not have a Test suffix#"
|
|
||||||
- "#with a nullable type declaration#"
|
|
||||||
- "#type mixed is not subtype of native#"
|
- "#type mixed is not subtype of native#"
|
||||||
- "# with null as default value#"
|
|
||||||
- "#has parameter \\$closure with default value.#"
|
|
||||||
- "#has parameter \\$description with default value.#"
|
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
170
src/ArchPresets/Laravel.php
Normal file
170
src/ArchPresets/Laravel.php
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?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()
|
||||||
|
->ignoring('App\Enums\Concerns');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Features')
|
||||||
|
->toBeClasses()
|
||||||
|
->ignoring('App\Features\Concerns');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Features')
|
||||||
|
->toHaveMethod('resolve');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Exceptions')
|
||||||
|
->classes()
|
||||||
|
->toImplement('Throwable')
|
||||||
|
->ignoring('App\Exceptions\Handler');
|
||||||
|
|
||||||
|
$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', 'middleware']);
|
||||||
|
|
||||||
|
$this->expectations[] = expect([
|
||||||
|
'dd',
|
||||||
|
'ddd',
|
||||||
|
'dump',
|
||||||
|
'env',
|
||||||
|
'exit',
|
||||||
|
'ray',
|
||||||
|
])->not->toBeUsed();
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Policies')
|
||||||
|
->classes()
|
||||||
|
->toHaveSuffix('Policy');
|
||||||
|
}
|
||||||
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/ArchPresets/Strict.php
Normal file
33
src/ArchPresets/Strict.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?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->toUseStrictEquality(),
|
||||||
|
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->toBeFinal(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->expectations[] = expect([
|
||||||
|
'sleep',
|
||||||
|
'usleep',
|
||||||
|
])->not->toBeUsed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -78,6 +78,8 @@ final class BootFiles implements Bootstrapper
|
|||||||
|
|
||||||
private function bootDatasets(string $testsPath): void
|
private function bootDatasets(string $testsPath): void
|
||||||
{
|
{
|
||||||
|
assert(strlen($testsPath) > 0);
|
||||||
|
|
||||||
$files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');
|
$files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');
|
||||||
|
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
|
|||||||
@ -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,16 +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',
|
'53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php',
|
||||||
'Runner/ResultCache/DefaultResultCache.php',
|
'77ffb7647b583bd82e37962c6fbdc4b04d3344d8a2c1ed103e625ed1ff7cb5c2' => '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',
|
'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php',
|
||||||
'Event/Value/ThrowableBuilder.php',
|
'8cfcb4999af79463eca51a42058e502ea4ddc776cba5677bf2f8eb6093e21a5c' => 'Event/Value/ThrowableBuilder.php',
|
||||||
|
'86cd9bcaa53cdd59c5b13e58f30064a015c549501e7629d93b96893d4dee1eb1' => '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.
|
||||||
@ -31,9 +31,8 @@ final class BootSubscribers implements Bootstrapper
|
|||||||
* 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,9 +36,40 @@ trait Testable
|
|||||||
private static string $__latestDescription;
|
private static string $__latestDescription;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's describing, if any.
|
* The test's assignees.
|
||||||
*/
|
*/
|
||||||
public ?string $__describing = null;
|
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.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
public array $__describing = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the test has ran or not.
|
||||||
|
*/
|
||||||
|
public bool $__ran = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's test closure.
|
* The test's test closure.
|
||||||
@ -67,15 +101,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 +113,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,13 +236,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 = $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 = [];
|
||||||
@ -210,6 +269,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();
|
||||||
|
|
||||||
@ -219,13 +282,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);
|
||||||
|
|
||||||
@ -233,11 +296,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -248,7 +316,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);
|
||||||
}
|
}
|
||||||
@ -263,7 +331,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');
|
||||||
@ -285,11 +358,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,9 +381,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;
|
||||||
@ -321,11 +394,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -336,7 +419,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) {
|
||||||
@ -377,4 +468,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.
|
||||||
@ -23,17 +23,18 @@ final class Thanks
|
|||||||
* @var array<string, string>
|
* @var array<string, string>
|
||||||
*/
|
*/
|
||||||
private const FUNDING_MESSAGES = [
|
private const FUNDING_MESSAGES = [
|
||||||
'Star the project on GitHub' => 'https://github.com/pestphp/pest',
|
'Star' => 'https://github.com/pestphp/pest',
|
||||||
'Tweet about the project' => 'https://twitter.com/pestphp',
|
'News' => 'https://twitter.com/pestphp',
|
||||||
'Sponsor the project' => 'https://github.com/sponsors/nunomaduro',
|
'Videos' => 'https://youtube.com/@nunomaduro',
|
||||||
|
'Sponsor' => 'https://github.com/sponsors/nunomaduro',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
) {
|
) {
|
||||||
// ..
|
// ..
|
||||||
}
|
}
|
||||||
@ -49,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(
|
||||||
@ -71,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
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class NoDirtyTestsFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace, Panicable
|
final class NoDirtyTestsFound extends InvalidArgumentException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Renders the panic on the given output.
|
* Renders the panic on the given output.
|
||||||
|
|||||||
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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest;
|
namespace Pest;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
use BadMethodCallException;
|
use BadMethodCallException;
|
||||||
use Closure;
|
use Closure;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
@ -29,12 +30,14 @@ 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @template TValue
|
* @template TValue
|
||||||
*
|
*
|
||||||
* @property OppositeExpectation $not Creates the opposite expectation.
|
* @property OppositeExpectation $not Creates the opposite expectation.
|
||||||
@ -191,7 +194,7 @@ final class Expectation
|
|||||||
*
|
*
|
||||||
* @return EachExpectation<TValue>
|
* @return EachExpectation<TValue>
|
||||||
*/
|
*/
|
||||||
public function each(callable $callback = null): EachExpectation
|
public function each(?callable $callback = null): EachExpectation
|
||||||
{
|
{
|
||||||
if (! is_iterable($this->value)) {
|
if (! is_iterable($this->value)) {
|
||||||
throw new BadMethodCallException('Expectation value is not iterable.');
|
throw new BadMethodCallException('Expectation value is not iterable.');
|
||||||
@ -220,7 +223,7 @@ final class Expectation
|
|||||||
throw new BadMethodCallException('Expectation value is not iterable.');
|
throw new BadMethodCallException('Expectation value is not iterable.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($callbacks) == 0) {
|
if ($callbacks === []) {
|
||||||
throw new InvalidArgumentException('No sequence expectations defined.');
|
throw new InvalidArgumentException('No sequence expectations defined.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,7 +241,7 @@ final class Expectation
|
|||||||
$index = isset($callbacks[$index + 1]) ? $index + 1 : 0;
|
$index = isset($callbacks[$index + 1]) ? $index + 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($callbacks) > $valuesCount) {
|
if ($valuesCount < count($callbacks)) {
|
||||||
throw new OutOfRangeException('Sequence expectations are more than the iterable items.');
|
throw new OutOfRangeException('Sequence expectations are more than the iterable items.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,7 +264,7 @@ final class Expectation
|
|||||||
$matched = false;
|
$matched = false;
|
||||||
|
|
||||||
foreach ($expressions as $key => $callback) {
|
foreach ($expressions as $key => $callback) {
|
||||||
if ($subject != $key) {
|
if ($subject != $key) { // @pest-arch-ignore-line
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,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;
|
||||||
@ -371,7 +380,7 @@ final class Expectation
|
|||||||
if (self::hasExtend($name)) {
|
if (self::hasExtend($name)) {
|
||||||
$extend = self::$extends[$name]->bindTo($this, Expectation::class);
|
$extend = self::$extends[$name]->bindTo($this, Expectation::class);
|
||||||
|
|
||||||
if ($extend != false) {
|
if ($extend != false) { // @pest-arch-ignore-line
|
||||||
return $extend;
|
return $extend;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -415,7 +424,7 @@ final class Expectation
|
|||||||
*/
|
*/
|
||||||
public function any(): Any
|
public function any(): Any
|
||||||
{
|
{
|
||||||
return new Any();
|
return new Any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -428,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.
|
||||||
*/
|
*/
|
||||||
@ -435,12 +509,25 @@ 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*(\/\*[\s\S]*?\*\/|\/\/[^\r\n]*(?:\r?\n|$)|\s)*declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;/m', (string) file_get_contents($object->path)),
|
||||||
'to use strict types',
|
'to use strict types',
|
||||||
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target uses strict equality.
|
||||||
|
*/
|
||||||
|
public function toUseStrictEquality(): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this,
|
||||||
|
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' == ') && ! str_contains((string) file_get_contents($object->path), ' != '),
|
||||||
|
'to use strict equality',
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' == ') || str_contains($line, ' != ')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target is final.
|
* Asserts that the given expectation target is final.
|
||||||
*/
|
*/
|
||||||
@ -501,6 +588,81 @@ final class Expectation
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target has a specific method.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $method
|
||||||
|
*/
|
||||||
|
public function toHaveMethod(array|string $method): ArchExpectation
|
||||||
|
{
|
||||||
|
$methods = is_array($method) ? $method : [$method];
|
||||||
|
|
||||||
|
return Targeted::make(
|
||||||
|
$this,
|
||||||
|
fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => $object->reflectionClass->hasMethod($method))) === count($methods),
|
||||||
|
sprintf("to have method '%s'", implode("', '", $methods)),
|
||||||
|
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.
|
||||||
*/
|
*/
|
||||||
@ -523,7 +685,7 @@ final class Expectation
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation targets is an class.
|
* Asserts that the given expectation target is a class.
|
||||||
*/
|
*/
|
||||||
public function toBeClass(): ArchExpectation
|
public function toBeClass(): ArchExpectation
|
||||||
{
|
{
|
||||||
@ -566,8 +728,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
|
||||||
{
|
{
|
||||||
@ -592,6 +752,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.
|
||||||
*/
|
*/
|
||||||
@ -608,7 +801,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
|
||||||
{
|
{
|
||||||
@ -652,7 +845,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
|
||||||
{
|
{
|
||||||
@ -692,7 +885,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']);
|
||||||
}
|
}
|
||||||
@ -833,4 +1029,80 @@ final class Expectation
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target to have the given attribute.
|
||||||
|
*/
|
||||||
|
public function toHaveAttribute(string $attribute): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this,
|
||||||
|
fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) !== [],
|
||||||
|
"to have attribute '{$attribute}'",
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target has a constructor method.
|
||||||
|
*/
|
||||||
|
public function toHaveConstructor(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toHaveMethod('__construct');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target has a destructor method.
|
||||||
|
*/
|
||||||
|
public function toHaveDestructor(): ArchExpectation
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest\Expectations;
|
namespace Pest\Expectations;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
use Pest\Arch\Contracts\ArchExpectation;
|
use Pest\Arch\Contracts\ArchExpectation;
|
||||||
use Pest\Arch\Expectations\Targeted;
|
use Pest\Arch\Expectations\Targeted;
|
||||||
use Pest\Arch\Expectations\ToBeUsedIn;
|
use Pest\Arch\Expectations\ToBeUsedIn;
|
||||||
@ -17,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
|
||||||
@ -28,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.
|
||||||
@ -76,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.
|
||||||
*/
|
*/
|
||||||
@ -83,12 +146,25 @@ 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')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target does not use the strict equality operator.
|
||||||
|
*/
|
||||||
|
public function toUseStrictEquality(): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this->original,
|
||||||
|
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' === ') && ! str_contains((string) file_get_contents($object->path), ' !== '),
|
||||||
|
'to use strict equality',
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' === ') || str_contains($line, ' !== ')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target is not final.
|
* Asserts that the given expectation target is not final.
|
||||||
*/
|
*/
|
||||||
@ -149,6 +225,165 @@ final class OppositeExpectation
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target does not have a specific method.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $method
|
||||||
|
*/
|
||||||
|
public function toHaveMethod(array|string $method): ArchExpectation
|
||||||
|
{
|
||||||
|
$methods = is_array($method) ? $method : [$method];
|
||||||
|
|
||||||
|
return Targeted::make(
|
||||||
|
$this->original,
|
||||||
|
fn (ObjectDescription $object): bool => array_filter(
|
||||||
|
$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')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
*/
|
*/
|
||||||
@ -171,7 +406,7 @@ final class OppositeExpectation
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation targets is an class.
|
* Asserts that the given expectation targets is not class.
|
||||||
*/
|
*/
|
||||||
public function toBeClass(): ArchExpectation
|
public function toBeClass(): ArchExpectation
|
||||||
{
|
{
|
||||||
@ -213,9 +448,7 @@ final class OppositeExpectation
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the given expectation target to be 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
|
||||||
{
|
{
|
||||||
@ -240,10 +473,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
|
||||||
{
|
{
|
||||||
@ -280,10 +546,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']);
|
||||||
}
|
}
|
||||||
@ -316,10 +580,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']);
|
||||||
}
|
}
|
||||||
@ -327,7 +589,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']);
|
||||||
}
|
}
|
||||||
@ -352,7 +614,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']);
|
||||||
}
|
}
|
||||||
@ -360,7 +622,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']);
|
||||||
}
|
}
|
||||||
@ -378,6 +640,19 @@ final class OppositeExpectation
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target not to have the given attribute.
|
||||||
|
*/
|
||||||
|
public function toHaveAttribute(string $attribute): ArchExpectation
|
||||||
|
{
|
||||||
|
return Targeted::make(
|
||||||
|
$this->original,
|
||||||
|
fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) === [],
|
||||||
|
"to not have attribute '{$attribute}'",
|
||||||
|
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle dynamic method calls into the original expectation.
|
* Handle dynamic method calls into the original expectation.
|
||||||
*
|
*
|
||||||
@ -440,4 +715,67 @@ final class OppositeExpectation
|
|||||||
implode(' ', array_map(fn (mixed $argument): string => $toString($argument), $arguments)),
|
implode(' ', array_map(fn (mixed $argument): string => $toString($argument), $arguments)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target does not have a constructor method.
|
||||||
|
*/
|
||||||
|
public function toHaveConstructor(): ArchExpectation
|
||||||
|
{
|
||||||
|
return $this->toHaveMethod('__construct');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the given expectation target does not have a destructor method.
|
||||||
|
*/
|
||||||
|
public function toHaveDestructor(): ArchExpectation
|
||||||
|
{
|
||||||
|
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($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,31 +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.
|
||||||
@ -142,36 +129,25 @@ final class TestCaseFactory
|
|||||||
$namespace = implode('\\', $partsFQN);
|
$namespace = implode('\\', $partsFQN);
|
||||||
$baseClass = sprintf('\%s', $this->class);
|
$baseClass = sprintf('\%s', $this->class);
|
||||||
|
|
||||||
if ('' === trim($className)) {
|
if (trim($className) === '') {
|
||||||
$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
|
||||||
@ -193,7 +166,7 @@ final class TestCaseFactory
|
|||||||
}
|
}
|
||||||
PHP;
|
PHP;
|
||||||
|
|
||||||
eval($classCode); // @phpstan-ignore-line
|
eval($classCode);
|
||||||
} catch (ParseError $caught) {
|
} catch (ParseError $caught) {
|
||||||
throw new RuntimeException(sprintf(
|
throw new RuntimeException(sprintf(
|
||||||
"Unable to create test case for test file at %s. \n %s",
|
"Unable to create test case for test file at %s. \n %s",
|
||||||
@ -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.');
|
||||||
@ -241,7 +222,7 @@ final class TestCaseFactory
|
|||||||
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Str::evaluable($method->description) === $methodName) {
|
if ($methodName === Str::evaluable($method->description)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -259,7 +240,7 @@ final class TestCaseFactory
|
|||||||
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Str::evaluable($method->description) === $methodName) {
|
if ($methodName === Str::evaluable($method->description)) {
|
||||||
return $method;
|
return $method;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -22,9 +23,23 @@ final class TestCaseMethodFactory
|
|||||||
use HigherOrderable;
|
use HigherOrderable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's describing, if any.
|
* The list of attributes.
|
||||||
|
*
|
||||||
|
* @var array<int, Attribute>
|
||||||
*/
|
*/
|
||||||
public ?string $describing = null;
|
public array $attributes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test's describing, if any.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
public array $describing = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test's description, if any.
|
||||||
|
*/
|
||||||
|
public ?string $description = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test's number of repetitions.
|
* The test's number of repetitions.
|
||||||
@ -36,6 +51,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 +101,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 +130,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 +163,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 +174,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 +189,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 === [] ? $depend : Str::describe($this->describing, $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;
|
||||||
@ -39,7 +43,7 @@ if (! function_exists('beforeAll')) {
|
|||||||
*/
|
*/
|
||||||
function beforeAll(Closure $closure): void
|
function beforeAll(Closure $closure): void
|
||||||
{
|
{
|
||||||
if (! is_null(DescribeCall::describing())) {
|
if (DescribeCall::describing() !== []) {
|
||||||
$filename = Backtrace::file();
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
throw new BeforeAllWithinDescribe($filename);
|
throw new BeforeAllWithinDescribe($filename);
|
||||||
@ -53,9 +57,11 @@ 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
|
||||||
{
|
{
|
||||||
$filename = Backtrace::file();
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
@ -108,15 +114,27 @@ 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
|
||||||
{
|
{
|
||||||
if ($description === null && TestSuite::getInstance()->test instanceof \PHPUnit\Framework\TestCase) {
|
if ($description === null && TestSuite::getInstance()->test instanceof \PHPUnit\Framework\TestCase) {
|
||||||
return new HigherOrderTapProxy(TestSuite::getInstance()->test);
|
return new HigherOrderTapProxy(TestSuite::getInstance()->test);
|
||||||
@ -134,9 +152,11 @@ 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
|
||||||
{
|
{
|
||||||
$description = sprintf('it %s', $description);
|
$description = sprintf('it %s', $description);
|
||||||
|
|
||||||
@ -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,9 +187,11 @@ 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
|
||||||
{
|
{
|
||||||
$filename = Backtrace::file();
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
@ -185,7 +205,7 @@ if (! function_exists('afterAll')) {
|
|||||||
*/
|
*/
|
||||||
function afterAll(Closure $closure): void
|
function afterAll(Closure $closure): void
|
||||||
{
|
{
|
||||||
if (! is_null(DescribeCall::describing())) {
|
if (DescribeCall::describing() !== []) {
|
||||||
$filename = Backtrace::file();
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
throw new AfterAllWithinDescribe($filename);
|
throw new AfterAllWithinDescribe($filename);
|
||||||
@ -194,3 +214,67 @@ 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 case 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('mutates')) {
|
||||||
|
/**
|
||||||
|
* Specifies which classes, enums, or traits a test case mutates.
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $targets
|
||||||
|
*/
|
||||||
|
function mutates(array|string ...$targets): void
|
||||||
|
{
|
||||||
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
|
$beforeEachCall = (new BeforeEachCall(TestSuite::getInstance(), $filename));
|
||||||
|
$beforeEachCall->group('__pest_mutate_only');
|
||||||
|
|
||||||
|
/** @var MutationTestRunner $runner */
|
||||||
|
$runner = Container::getInstance()->get(MutationTestRunner::class);
|
||||||
|
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */
|
||||||
|
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
|
||||||
|
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
|
||||||
|
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
|
||||||
|
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
|
||||||
|
|
||||||
|
if ($runner->isEnabled() && ! $everything && ! is_array($classes) && ! is_array($paths)) {
|
||||||
|
$beforeEachCall->only('__pest_mutate_only');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var ConfigurationRepository $configurationRepository */
|
||||||
|
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
|
||||||
|
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
|
||||||
|
|
||||||
|
if (! is_array($paths)) {
|
||||||
|
$configurationRepository->globalConfiguration('default')->class(...$targets); // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ final class KernelDump
|
|||||||
*/
|
*/
|
||||||
public function disable(): void
|
public function disable(): void
|
||||||
{
|
{
|
||||||
@ob_clean(); // @phpstan-ignore-line
|
@ob_clean();
|
||||||
|
|
||||||
if ($this->buffer !== '') {
|
if ($this->buffer !== '') {
|
||||||
$this->flush();
|
$this->flush();
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Pest\Logging\TeamCity;
|
namespace Pest\Logging;
|
||||||
|
|
||||||
use NunoMaduro\Collision\Adapters\Phpunit\State;
|
use NunoMaduro\Collision\Adapters\Phpunit\State;
|
||||||
use Pest\Exceptions\ShouldNotHappen;
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
@ -11,6 +11,7 @@ use Pest\Support\Str;
|
|||||||
use PHPUnit\Event\Code\Test;
|
use PHPUnit\Event\Code\Test;
|
||||||
use PHPUnit\Event\Code\TestMethod;
|
use PHPUnit\Event\Code\TestMethod;
|
||||||
use PHPUnit\Event\Code\Throwable;
|
use PHPUnit\Event\Code\Throwable;
|
||||||
|
use PHPUnit\Event\Test\AfterLastTestMethodErrored;
|
||||||
use PHPUnit\Event\Test\BeforeFirstTestMethodErrored;
|
use PHPUnit\Event\Test\BeforeFirstTestMethodErrored;
|
||||||
use PHPUnit\Event\Test\ConsideredRisky;
|
use PHPUnit\Event\Test\ConsideredRisky;
|
||||||
use PHPUnit\Event\Test\Errored;
|
use PHPUnit\Event\Test\Errored;
|
||||||
@ -18,25 +19,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 +137,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 +149,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 instanceof \PHPUnit\Event\Code\TestMethod) {
|
||||||
|
return $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$name = $testSuite->name();
|
$name = $testSuite->name();
|
||||||
|
|
||||||
if (! str_starts_with($name, self::PREFIX)) {
|
if (! str_starts_with($name, self::PREFIX)) {
|
||||||
@ -150,10 +165,47 @@ final class Converter
|
|||||||
return Str::after($name, self::PREFIX);
|
return Str::after($name, self::PREFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the trimmed test class name.
|
||||||
|
*/
|
||||||
|
public function getTrimmedTestClassName(TestMethod $test): string
|
||||||
|
{
|
||||||
|
return Str::after($test->className(), self::PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 instanceof \PHPUnit\Event\Code\TestMethod) {
|
||||||
|
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();
|
||||||
|
|
||||||
@ -167,9 +219,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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -205,8 +255,9 @@ final class Converter
|
|||||||
$numberOfNotPassedTests = count(
|
$numberOfNotPassedTests = count(
|
||||||
array_unique(
|
array_unique(
|
||||||
array_map(
|
array_map(
|
||||||
function (BeforeFirstTestMethodErrored|Errored|Failed|Skipped|ConsideredRisky|MarkedIncomplete $event): string {
|
function (AfterLastTestMethodErrored|BeforeFirstTestMethodErrored|Errored|Failed|Skipped|ConsideredRisky|MarkedIncomplete $event): string {
|
||||||
if ($event instanceof BeforeFirstTestMethodErrored) {
|
if ($event instanceof BeforeFirstTestMethodErrored
|
||||||
|
|| $event instanceof AfterLastTestMethodErrored) {
|
||||||
return $event->testClassName();
|
return $event->testClassName();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +65,7 @@ final class ServiceMessage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $duration in milliseconds
|
* @param int $duration in milliseconds
|
||||||
*/
|
*/
|
||||||
public static function testFinished(string $name, int $duration): self
|
public static function testFinished(string $name, int $duration): self
|
||||||
{
|
{
|
||||||
@ -106,7 +108,7 @@ final class ServiceMessage
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function testIgnored(string $name, string $message, string $details = null): self
|
public static function testIgnored(string $name, string $message, ?string $details = null): self
|
||||||
{
|
{
|
||||||
return new self('testIgnored', [
|
return new self('testIgnored', [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,12 +6,12 @@ namespace Pest\Logging\TeamCity;
|
|||||||
|
|
||||||
use NunoMaduro\Collision\Adapters\Phpunit\Style;
|
use NunoMaduro\Collision\Adapters\Phpunit\Style;
|
||||||
use Pest\Exceptions\ShouldNotHappen;
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
|
use Pest\Logging\Converter;
|
||||||
use Pest\Logging\TeamCity\Subscriber\TestConsideredRiskySubscriber;
|
use Pest\Logging\TeamCity\Subscriber\TestConsideredRiskySubscriber;
|
||||||
use Pest\Logging\TeamCity\Subscriber\TestErroredSubscriber;
|
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;
|
||||||
@ -27,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;
|
||||||
@ -44,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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,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.');
|
||||||
}
|
}
|
||||||
@ -261,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 {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ use Pest\Matchers\Any;
|
|||||||
use Pest\Support\Arr;
|
use Pest\Support\Arr;
|
||||||
use Pest\Support\Exporter;
|
use Pest\Support\Exporter;
|
||||||
use Pest\Support\NullClosure;
|
use Pest\Support\NullClosure;
|
||||||
|
use Pest\Support\Str;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use PHPUnit\Framework\Assert;
|
use PHPUnit\Framework\Assert;
|
||||||
use PHPUnit\Framework\Constraint\Constraint;
|
use PHPUnit\Framework\Constraint\Constraint;
|
||||||
@ -130,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);
|
||||||
|
|
||||||
@ -142,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);
|
||||||
|
|
||||||
@ -154,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);
|
||||||
|
|
||||||
@ -166,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);
|
||||||
|
|
||||||
@ -195,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.
|
||||||
*
|
*
|
||||||
@ -295,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();
|
||||||
|
|
||||||
@ -313,43 +332,13 @@ final class Expectation
|
|||||||
/**
|
/**
|
||||||
* Asserts that the value contains the provided properties $names.
|
* Asserts that the value contains the provided properties $names.
|
||||||
*
|
*
|
||||||
* @param iterable<array-key, string> $names
|
* @param iterable<string, mixed>|iterable<int, string> $names
|
||||||
* @return self<TValue>
|
* @return self<TValue>
|
||||||
*/
|
*/
|
||||||
public function toHaveProperties(iterable $names, string $message = ''): self
|
public function toHaveProperties(iterable $names, string $message = ''): self
|
||||||
{
|
{
|
||||||
foreach ($names as $name => $value) {
|
foreach ($names as $name => $value) {
|
||||||
is_int($name) ? $this->toHaveProperty($value, message: $message) : $this->toHaveProperty($name, $value, $message);
|
is_int($name) ? $this->toHaveProperty($value, message: $message) : $this->toHaveProperty($name, $value, $message); // @phpstan-ignore-line
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
return $this;
|
||||||
@ -448,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.
|
||||||
*
|
*
|
||||||
@ -623,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();
|
||||||
@ -843,6 +844,7 @@ final class Expectation
|
|||||||
|
|
||||||
$string = match (true) {
|
$string = match (true) {
|
||||||
is_string($this->value) => $this->value,
|
is_string($this->value) => $this->value,
|
||||||
|
is_object($this->value) && method_exists($this->value, 'toSnapshot') => $this->value->toSnapshot(),
|
||||||
is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(),
|
is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(),
|
||||||
is_object($this->value) && method_exists($this->value, 'toString') => $this->value->toString(),
|
is_object($this->value) && method_exists($this->value, 'toString') => $this->value->toString(),
|
||||||
$this->value instanceof \Illuminate\Testing\TestResponse => $this->value->getContent(), // @phpstan-ignore-line
|
$this->value instanceof \Illuminate\Testing\TestResponse => $this->value->getContent(), // @phpstan-ignore-line
|
||||||
@ -918,7 +920,7 @@ final class Expectation
|
|||||||
* @param (Closure(Throwable): mixed)|string $exception
|
* @param (Closure(Throwable): mixed)|string $exception
|
||||||
* @return self<TValue>
|
* @return self<TValue>
|
||||||
*/
|
*/
|
||||||
public function toThrow(callable|string|Throwable $exception, string $exceptionMessage = null, string $message = ''): self
|
public function toThrow(callable|string|Throwable $exception, ?string $exceptionMessage = null, string $message = ''): self
|
||||||
{
|
{
|
||||||
$callback = NullClosure::create();
|
$callback = NullClosure::create();
|
||||||
|
|
||||||
@ -926,7 +928,7 @@ final class Expectation
|
|||||||
$callback = $exception;
|
$callback = $exception;
|
||||||
$parameters = (new ReflectionFunction($exception))->getParameters();
|
$parameters = (new ReflectionFunction($exception))->getParameters();
|
||||||
|
|
||||||
if (1 !== count($parameters)) {
|
if (count($parameters) !== 1) {
|
||||||
throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.');
|
throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -950,7 +952,7 @@ final class Expectation
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (! class_exists($exception)) {
|
if (! class_exists($exception)) {
|
||||||
if ($e instanceof Error && $e->getMessage() === "Class \"$exception\" not found") {
|
if ($e instanceof Error && "Class \"$exception\" not found" === $e->getMessage()) {
|
||||||
Assert::assertTrue(true);
|
Assert::assertTrue(true);
|
||||||
|
|
||||||
throw $e;
|
throw $e;
|
||||||
@ -966,6 +968,7 @@ final class Expectation
|
|||||||
}
|
}
|
||||||
|
|
||||||
Assert::assertInstanceOf($exception, $e, $message);
|
Assert::assertInstanceOf($exception, $e, $message);
|
||||||
|
|
||||||
$callback($e);
|
$callback($e);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -1111,4 +1114,49 @@ final class Expectation
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the value is UUID.
|
||||||
|
*
|
||||||
|
* @return self<TValue>
|
||||||
|
*/
|
||||||
|
public function toBeUuid(string $message = ''): self
|
||||||
|
{
|
||||||
|
if (! is_string($this->value)) {
|
||||||
|
InvalidExpectationValue::expected('string');
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert::assertTrue(Str::isUuid($this->value), $message);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the value is between 2 specified values
|
||||||
|
*
|
||||||
|
* @return self<TValue>
|
||||||
|
*/
|
||||||
|
public function toBeBetween(int|float|DateTimeInterface $lowestValue, int|float|DateTimeInterface $highestValue, string $message = ''): self
|
||||||
|
{
|
||||||
|
Assert::assertGreaterThanOrEqual($lowestValue, $this->value, $message);
|
||||||
|
Assert::assertLessThanOrEqual($highestValue, $this->value, $message);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the value is a url
|
||||||
|
*
|
||||||
|
* @return self<TValue>
|
||||||
|
*/
|
||||||
|
public function toBeUrl(string $message = ''): self
|
||||||
|
{
|
||||||
|
if ($message === '') {
|
||||||
|
$message = "Failed asserting that {$this->value} is a url.";
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert::assertTrue(Str::isUrl((string) $this->value), $message);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ namespace Pest\PendingCalls;
|
|||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Pest\PendingCalls\Concerns\Describable;
|
use Pest\PendingCalls\Concerns\Describable;
|
||||||
|
use Pest\Support\Arr;
|
||||||
use Pest\Support\Backtrace;
|
use Pest\Support\Backtrace;
|
||||||
use Pest\Support\ChainableClosure;
|
use Pest\Support\ChainableClosure;
|
||||||
use Pest\Support\HigherOrderMessageCollection;
|
use Pest\Support\HigherOrderMessageCollection;
|
||||||
@ -35,11 +36,11 @@ final class AfterEachCall
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TestSuite $testSuite,
|
private readonly TestSuite $testSuite,
|
||||||
private readonly string $filename,
|
private readonly string $filename,
|
||||||
Closure $closure = null
|
?Closure $closure = null
|
||||||
) {
|
) {
|
||||||
$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,8 +55,8 @@ final class AfterEachCall
|
|||||||
$proxies = $this->proxies;
|
$proxies = $this->proxies;
|
||||||
|
|
||||||
$afterEachTestCase = ChainableClosure::boundWhen(
|
$afterEachTestCase = ChainableClosure::boundWhen(
|
||||||
fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line
|
fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true),
|
||||||
ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line
|
ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class),
|
||||||
)->bindTo($this, self::class);
|
)->bindTo($this, self::class);
|
||||||
|
|
||||||
assert($afterEachTestCase instanceof Closure);
|
assert($afterEachTestCase instanceof Closure);
|
||||||
@ -65,7 +66,6 @@ final class AfterEachCall
|
|||||||
$this,
|
$this,
|
||||||
$afterEachTestCase,
|
$afterEachTestCase,
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -5,7 +5,9 @@ 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\Arr;
|
||||||
use Pest\Support\Backtrace;
|
use Pest\Support\Backtrace;
|
||||||
use Pest\Support\ChainableClosure;
|
use Pest\Support\ChainableClosure;
|
||||||
use Pest\Support\HigherOrderMessageCollection;
|
use Pest\Support\HigherOrderMessageCollection;
|
||||||
@ -14,6 +16,8 @@ use Pest\TestSuite;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
*
|
||||||
|
* @mixin TestCall
|
||||||
*/
|
*/
|
||||||
final class BeforeEachCall
|
final class BeforeEachCall
|
||||||
{
|
{
|
||||||
@ -40,12 +44,12 @@ final class BeforeEachCall
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly TestSuite $testSuite,
|
public readonly TestSuite $testSuite,
|
||||||
private readonly string $filename,
|
private readonly string $filename,
|
||||||
Closure $closure = null
|
?Closure $closure = null
|
||||||
) {
|
) {
|
||||||
$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,18 +63,23 @@ 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 !== []) {
|
||||||
}
|
if (Arr::last($describing) !== Arr::last($this->describing)) {
|
||||||
if ($describing !== $testCall->describing) {
|
return;
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
if (! in_array(Arr::last($describing), $testCall->describing, true)) {
|
||||||
|
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 => $describing === [] || in_array(Arr::last($describing), $this->__describing, true),
|
||||||
ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line
|
ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class),
|
||||||
)->bindTo($this, self::class);
|
)->bindTo($this, self::class);
|
||||||
|
|
||||||
assert($beforeEachTestCase instanceof Closure);
|
assert($beforeEachTestCase instanceof Closure);
|
||||||
@ -83,6 +92,18 @@ final class BeforeEachCall
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given closure after the test.
|
||||||
|
*/
|
||||||
|
public function after(Closure $closure): self
|
||||||
|
{
|
||||||
|
if ($this->describing === []) {
|
||||||
|
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 +112,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,17 @@ namespace Pest\PendingCalls\Concerns;
|
|||||||
*/
|
*/
|
||||||
trait Describable
|
trait Describable
|
||||||
{
|
{
|
||||||
public ?string $describing = null;
|
/**
|
||||||
|
* Note: this is property is not used; however, it gets added automatically by rector php.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
public array $__describing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The describing of the test case.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
public array $describing = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,8 +15,15 @@ final class DescribeCall
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The current describe call.
|
* The current describe call.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
private static ?string $describing = null;
|
private static array $describing = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The describe "before each" call.
|
||||||
|
*/
|
||||||
|
private ?BeforeEachCall $currentBeforeEachCall = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Pending Call.
|
* Creates a new Pending Call.
|
||||||
@ -32,8 +39,10 @@ final class DescribeCall
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* What is the current describing.
|
* What is the current describing.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
*/
|
*/
|
||||||
public static function describing(): ?string
|
public static function describing(): array
|
||||||
{
|
{
|
||||||
return self::$describing;
|
return self::$describing;
|
||||||
}
|
}
|
||||||
@ -43,12 +52,14 @@ final class DescribeCall
|
|||||||
*/
|
*/
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
{
|
{
|
||||||
self::$describing = $this->description;
|
unset($this->currentBeforeEachCall);
|
||||||
|
|
||||||
|
self::$describing[] = $this->description;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
($this->tests)();
|
($this->tests)();
|
||||||
} finally {
|
} finally {
|
||||||
self::$describing = null;
|
array_pop(self::$describing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,14 +68,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,29 +5,40 @@ declare(strict_types=1);
|
|||||||
namespace Pest\PendingCalls;
|
namespace Pest\PendingCalls;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Pest\Concerns\Testable;
|
||||||
use Pest\Exceptions\InvalidArgumentException;
|
use Pest\Exceptions\InvalidArgumentException;
|
||||||
use Pest\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;
|
||||||
|
use Pest\Support\Str;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
|
use PHPUnit\Framework\AssertionFailedError;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*
|
*
|
||||||
* @mixin HigherOrderCallables|TestCase
|
* @mixin HigherOrderCallables|TestCase|Testable
|
||||||
*/
|
*/
|
||||||
final class TestCall
|
final class TestCall // @phpstan-ignore-line
|
||||||
{
|
{
|
||||||
use Describable;
|
use Describable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of test case factory attributes.
|
||||||
|
*
|
||||||
|
* @var array<int, Attribute>
|
||||||
|
*/
|
||||||
|
private array $testCaseFactoryAttributes = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Test Case Factory.
|
* The Test Case Factory.
|
||||||
*/
|
*/
|
||||||
@ -44,10 +55,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;
|
||||||
|
|
||||||
@ -56,10 +67,54 @@ 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 = $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.
|
||||||
|
*/
|
||||||
|
public function fails(?string $message = null): self
|
||||||
|
{
|
||||||
|
return $this->throws(AssertionFailedError::class, $message);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the test throws the given `$exceptionClass` when called.
|
* Asserts that the test throws the given `$exceptionClass` when called.
|
||||||
*/
|
*/
|
||||||
public function throws(string|int $exception, string $exceptionMessage = null, int $exceptionCode = null): self
|
public function throws(string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self
|
||||||
{
|
{
|
||||||
if (is_int($exception)) {
|
if (is_int($exception)) {
|
||||||
$exceptionCode = $exception;
|
$exceptionCode = $exception;
|
||||||
@ -91,7 +146,7 @@ final class TestCall
|
|||||||
*
|
*
|
||||||
* @param (callable(): bool)|bool $condition
|
* @param (callable(): bool)|bool $condition
|
||||||
*/
|
*/
|
||||||
public function throwsIf(callable|bool $condition, string|int $exception, string $exceptionMessage = null, int $exceptionCode = null): self
|
public function throwsIf(callable|bool $condition, string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self
|
||||||
{
|
{
|
||||||
$condition = is_callable($condition)
|
$condition = is_callable($condition)
|
||||||
? $condition
|
? $condition
|
||||||
@ -104,6 +159,24 @@ final class TestCall
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the test throws the given `$exceptionClass` when called if the given condition is false.
|
||||||
|
*
|
||||||
|
* @param (callable(): bool)|bool $condition
|
||||||
|
*/
|
||||||
|
public function throwsUnless(callable|bool $condition, string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self
|
||||||
|
{
|
||||||
|
$condition = is_callable($condition)
|
||||||
|
? $condition
|
||||||
|
: static fn (): bool => $condition;
|
||||||
|
|
||||||
|
if (! $condition()) {
|
||||||
|
return $this->throws($exception, $exceptionMessage, $exceptionCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the current test multiple times with
|
* Runs the current test multiple times with
|
||||||
* each item of the given `iterable`.
|
* each item of the given `iterable`.
|
||||||
@ -137,7 +210,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;
|
||||||
@ -148,7 +224,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;
|
||||||
}
|
}
|
||||||
@ -180,12 +256,37 @@ final class TestCall
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips the current test on the given PHP version.
|
||||||
|
*/
|
||||||
|
public function skipOnPhp(string $version): self
|
||||||
|
{
|
||||||
|
if (mb_strlen($version) < 2) {
|
||||||
|
throw new InvalidArgumentException('The version must start with [<] or [>].');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($version, '>=') || str_starts_with($version, '<=')) {
|
||||||
|
$operator = substr($version, 0, 2);
|
||||||
|
$version = substr($version, 2);
|
||||||
|
} elseif (str_starts_with($version, '>') || str_starts_with($version, '<')) {
|
||||||
|
$operator = $version[0];
|
||||||
|
$version = substr($version, 1);
|
||||||
|
// ensure starts with number:
|
||||||
|
} elseif (is_numeric($version[0])) {
|
||||||
|
$operator = '==';
|
||||||
|
} else {
|
||||||
|
throw new InvalidArgumentException('The version must start with [<, >, <=, >=] or a number.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->skip(version_compare(PHP_VERSION, $version, $operator), sprintf('This test is skipped on PHP [%s%s].', $operator, $version));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skips the current test if the given test is running on Windows.
|
* Skips the current test if the given test is running on Windows.
|
||||||
*/
|
*/
|
||||||
public function skipOnWindows(): self
|
public function skipOnWindows(): self
|
||||||
{
|
{
|
||||||
return $this->skipOn('Windows', 'This test is skipped on [Windows].');
|
return $this->skipOnOs('Windows', 'This test is skipped on [Windows].');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -193,7 +294,7 @@ final class TestCall
|
|||||||
*/
|
*/
|
||||||
public function skipOnMac(): self
|
public function skipOnMac(): self
|
||||||
{
|
{
|
||||||
return $this->skipOn('Darwin', 'This test is skipped on [Mac].');
|
return $this->skipOnOs('Darwin', 'This test is skipped on [Mac].');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -201,19 +302,43 @@ final class TestCall
|
|||||||
*/
|
*/
|
||||||
public function skipOnLinux(): self
|
public function skipOnLinux(): self
|
||||||
{
|
{
|
||||||
return $this->skipOn('Linux', 'This test is skipped on [Linux].');
|
return $this->skipOnOs('Linux', 'This test is skipped on [Linux].');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skips the current test if the given test is running on the given operating systems.
|
* Skips the current test if the given test is running on the given operating systems.
|
||||||
*/
|
*/
|
||||||
private function skipOn(string $osFamily, string $message): self
|
private function skipOnOs(string $osFamily, string $message): self
|
||||||
{
|
{
|
||||||
return PHP_OS_FAMILY === $osFamily
|
return $osFamily === PHP_OS_FAMILY
|
||||||
? $this->skip($message)
|
? $this->skip($message)
|
||||||
: $this;
|
: $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips the current test unless the given test is running on Windows.
|
||||||
|
*/
|
||||||
|
public function onlyOnWindows(): self
|
||||||
|
{
|
||||||
|
return $this->skipOnMac()->skipOnLinux();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips the current test unless the given test is running on Mac.
|
||||||
|
*/
|
||||||
|
public function onlyOnMac(): self
|
||||||
|
{
|
||||||
|
return $this->skipOnWindows()->skipOnLinux();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips the current test unless the given test is running on Linux.
|
||||||
|
*/
|
||||||
|
public function onlyOnLinux(): self
|
||||||
|
{
|
||||||
|
return $this->skipOnWindows()->skipOnMac();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repeats the current test the given number of times.
|
* Repeats the current test the given number of times.
|
||||||
*/
|
*/
|
||||||
@ -229,32 +354,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);
|
||||||
}
|
}
|
||||||
@ -269,7 +548,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;
|
||||||
@ -281,7 +594,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;
|
||||||
@ -292,7 +608,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;
|
||||||
}
|
}
|
||||||
@ -332,7 +651,7 @@ final class TestCall
|
|||||||
*
|
*
|
||||||
* @param array<int, mixed>|null $arguments
|
* @param array<int, mixed>|null $arguments
|
||||||
*/
|
*/
|
||||||
private function addChain(string $file, int $line, string $name, array $arguments = null): self
|
private function addChain(string $file, int $line, string $name, ?array $arguments = null): self
|
||||||
{
|
{
|
||||||
$exporter = Exporter::default();
|
$exporter = Exporter::default();
|
||||||
|
|
||||||
@ -343,10 +662,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));
|
||||||
}
|
}
|
||||||
@ -359,11 +679,21 @@ final class TestCall
|
|||||||
*/
|
*/
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
{
|
{
|
||||||
if (! is_null($this->describing)) {
|
if ($this->description === null) {
|
||||||
|
throw new TestDescriptionMissing($this->filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->describing !== []) {
|
||||||
$this->testCaseMethod->describing = $this->describing;
|
$this->testCaseMethod->describing = $this->describing;
|
||||||
$this->testCaseMethod->description = sprintf('`%s` → %s', $this->describing, $this->testCaseMethod->description);
|
$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,10 +6,10 @@ namespace Pest;
|
|||||||
|
|
||||||
function version(): string
|
function version(): string
|
||||||
{
|
{
|
||||||
return '2.16.1';
|
return '3.7.3';
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDirectory(string $file = ''): string
|
function testDirectory(string $file = ''): string
|
||||||
{
|
{
|
||||||
return TestSuite::getInstance()->testPath.'/'.$file;
|
return TestSuite::getInstance()->testPath.DIRECTORY_SEPARATOR.$file;
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
@ -30,10 +34,21 @@ final class Cache implements HandlesArguments
|
|||||||
*/
|
*/
|
||||||
public function handleArguments(array $arguments): array
|
public function handleArguments(array $arguments): array
|
||||||
{
|
{
|
||||||
$arguments = $this->pushArgument(
|
if (! $this->hasArgument('--cache-directory', $arguments)) {
|
||||||
sprintf('--cache-directory=%s', 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)) {
|
||||||
return $this->pushArgument('--cache-result', $arguments);
|
return $this->pushArgument('--cache-result', $arguments);
|
||||||
|
|||||||
@ -16,7 +16,17 @@ trait HandleArguments
|
|||||||
*/
|
*/
|
||||||
public function hasArgument(string $argument, array $arguments): bool
|
public function hasArgument(string $argument, array $arguments): bool
|
||||||
{
|
{
|
||||||
return in_array($argument, $arguments, true);
|
foreach ($arguments as $arg) {
|
||||||
|
if ($arg === $argument) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with((string) $arg, "$argument=")) { // @phpstan-ignore-line
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
96
src/Plugins/Configuration.php
Normal file
96
src/Plugins/Configuration.php
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins;
|
||||||
|
|
||||||
|
use DOMDocument;
|
||||||
|
use Pest\Contracts\Plugins\HandlesArguments;
|
||||||
|
use Pest\Contracts\Plugins\Terminable;
|
||||||
|
use Pest\Plugins\Concerns\HandleArguments;
|
||||||
|
use PHPUnit\TextUI\CliArguments\Builder as CliConfigurationBuilder;
|
||||||
|
use PHPUnit\TextUI\CliArguments\XmlConfigurationFileFinder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Configuration implements HandlesArguments, Terminable
|
||||||
|
{
|
||||||
|
use HandleArguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base PHPUnit file.
|
||||||
|
*/
|
||||||
|
public const BASE_PHPUNIT_FILE = __DIR__
|
||||||
|
.DIRECTORY_SEPARATOR
|
||||||
|
.'..'
|
||||||
|
.DIRECTORY_SEPARATOR
|
||||||
|
.'..'
|
||||||
|
.DIRECTORY_SEPARATOR
|
||||||
|
.'resources/base-phpunit.xml';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the arguments, adding the cache directory and the cache result arguments.
|
||||||
|
*/
|
||||||
|
public function handleArguments(array $arguments): array
|
||||||
|
{
|
||||||
|
if ($this->hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) {
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments = $this->pushArgument('--configuration', $arguments);
|
||||||
|
|
||||||
|
return $this->pushArgument((string) realpath($this->fromGeneratedConfigurationFile()), $arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configuration file from the generated configuration file.
|
||||||
|
*/
|
||||||
|
private function fromGeneratedConfigurationFile(): string
|
||||||
|
{
|
||||||
|
$path = $this->getTempPhpunitXmlPath();
|
||||||
|
if (file_exists($path)) {
|
||||||
|
unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$doc = new DOMDocument;
|
||||||
|
$doc->load(self::BASE_PHPUNIT_FILE);
|
||||||
|
|
||||||
|
$contents = $doc->saveXML();
|
||||||
|
|
||||||
|
assert(is_int(file_put_contents($path, $contents)));
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the configuration file is custom.
|
||||||
|
*/
|
||||||
|
private function hasCustomConfigurationFile(): bool
|
||||||
|
{
|
||||||
|
$cliConfiguration = (new CliConfigurationBuilder)->fromParameters([]);
|
||||||
|
$configurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration);
|
||||||
|
|
||||||
|
return is_string($configurationFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the temporary phpunit.xml path.
|
||||||
|
*/
|
||||||
|
private function getTempPhpunitXmlPath(): string
|
||||||
|
{
|
||||||
|
return getcwd().'/.pest.xml';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Terminates the plugin.
|
||||||
|
*/
|
||||||
|
public function terminate(): void
|
||||||
|
{
|
||||||
|
$path = $this->getTempPhpunitXmlPath();
|
||||||
|
|
||||||
|
if (file_exists($path)) {
|
||||||
|
unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,6 +27,11 @@ final class Coverage implements AddsOutput, HandlesArguments
|
|||||||
*/
|
*/
|
||||||
private const MIN_OPTION = 'min';
|
private const MIN_OPTION = 'min';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const EXACTLY_OPTION = 'exactly';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether it should show the coverage or not.
|
* Whether it should show the coverage or not.
|
||||||
*/
|
*/
|
||||||
@ -37,6 +42,11 @@ final class Coverage implements AddsOutput, HandlesArguments
|
|||||||
*/
|
*/
|
||||||
public float $coverageMin = 0.0;
|
public float $coverageMin = 0.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The exactly coverage.
|
||||||
|
*/
|
||||||
|
public ?float $coverageExactly = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Plugin instance.
|
* Creates a new Plugin instance.
|
||||||
*/
|
*/
|
||||||
@ -51,7 +61,7 @@ final class Coverage implements AddsOutput, HandlesArguments
|
|||||||
public function handleArguments(array $originals): array
|
public function handleArguments(array $originals): array
|
||||||
{
|
{
|
||||||
$arguments = [...[''], ...array_values(array_filter($originals, function (string $original): bool {
|
$arguments = [...[''], ...array_values(array_filter($originals, function (string $original): bool {
|
||||||
foreach ([self::COVERAGE_OPTION, self::MIN_OPTION] as $option) {
|
foreach ([self::COVERAGE_OPTION, self::MIN_OPTION, self::EXACTLY_OPTION] as $option) {
|
||||||
if ($original === sprintf('--%s', $option)) {
|
if ($original === sprintf('--%s', $option)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -73,6 +83,7 @@ final class Coverage implements AddsOutput, HandlesArguments
|
|||||||
$inputs = [];
|
$inputs = [];
|
||||||
$inputs[] = new InputOption(self::COVERAGE_OPTION, null, InputOption::VALUE_NONE);
|
$inputs[] = new InputOption(self::COVERAGE_OPTION, null, InputOption::VALUE_NONE);
|
||||||
$inputs[] = new InputOption(self::MIN_OPTION, null, InputOption::VALUE_REQUIRED);
|
$inputs[] = new InputOption(self::MIN_OPTION, null, InputOption::VALUE_REQUIRED);
|
||||||
|
$inputs[] = new InputOption(self::EXACTLY_OPTION, null, InputOption::VALUE_REQUIRED);
|
||||||
|
|
||||||
$input = new ArgvInput($arguments, new InputDefinition($inputs));
|
$input = new ArgvInput($arguments, new InputDefinition($inputs));
|
||||||
if ((bool) $input->getOption(self::COVERAGE_OPTION)) {
|
if ((bool) $input->getOption(self::COVERAGE_OPTION)) {
|
||||||
@ -106,6 +117,13 @@ final class Coverage implements AddsOutput, HandlesArguments
|
|||||||
$this->coverageMin = (float) $minOption;
|
$this->coverageMin = (float) $minOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($input->getOption(self::EXACTLY_OPTION) !== null) {
|
||||||
|
/** @var int|float $exactlyOption */
|
||||||
|
$exactlyOption = $input->getOption(self::EXACTLY_OPTION);
|
||||||
|
|
||||||
|
$this->coverageExactly = (float) $exactlyOption;
|
||||||
|
}
|
||||||
|
|
||||||
return $originals;
|
return $originals;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +132,10 @@ final class Coverage implements AddsOutput, HandlesArguments
|
|||||||
*/
|
*/
|
||||||
public function addOutput(int $exitCode): int
|
public function addOutput(int $exitCode): int
|
||||||
{
|
{
|
||||||
|
if (Parallel::isWorker()) {
|
||||||
|
return $exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
if ($exitCode === 0 && $this->coverage) {
|
if ($exitCode === 0 && $this->coverage) {
|
||||||
if (! \Pest\Support\Coverage::isAvailable()) {
|
if (! \Pest\Support\Coverage::isAvailable()) {
|
||||||
$this->output->writeln(
|
$this->output->writeln(
|
||||||
@ -123,14 +145,26 @@ final class Coverage implements AddsOutput, HandlesArguments
|
|||||||
}
|
}
|
||||||
|
|
||||||
$coverage = \Pest\Support\Coverage::report($this->output);
|
$coverage = \Pest\Support\Coverage::report($this->output);
|
||||||
|
|
||||||
$exitCode = (int) ($coverage < $this->coverageMin);
|
$exitCode = (int) ($coverage < $this->coverageMin);
|
||||||
|
|
||||||
if ($exitCode === 1) {
|
if ($exitCode === 0 && $this->coverageExactly !== null) {
|
||||||
|
$comparableCoverage = $this->computeComparableCoverage($coverage);
|
||||||
|
$comparableCoverageExactly = $this->computeComparableCoverage($this->coverageExactly);
|
||||||
|
|
||||||
|
$exitCode = $comparableCoverage === $comparableCoverageExactly ? 0 : 1;
|
||||||
|
|
||||||
|
if ($exitCode === 1) {
|
||||||
|
$this->output->writeln(sprintf(
|
||||||
|
"\n <fg=white;bg=red;options=bold> FAIL </> Code coverage not exactly <fg=white;options=bold> %s %%</>, currently <fg=red;options=bold> %s %%</>.",
|
||||||
|
number_format($this->coverageExactly, 1),
|
||||||
|
number_format(floor($coverage * 10) / 10, 1),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} elseif ($exitCode === 1) {
|
||||||
$this->output->writeln(sprintf(
|
$this->output->writeln(sprintf(
|
||||||
"\n <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected:<fg=red;options=bold> %s %%</>. Minimum:<fg=white;options=bold> %s %%</>.",
|
"\n <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected <fg=white;options=bold> %s %%</>, currently <fg=red;options=bold> %s %%</>.",
|
||||||
number_format($coverage, 1),
|
number_format($this->coverageMin, 1),
|
||||||
number_format($this->coverageMin, 1)
|
number_format(floor($coverage * 10) / 10, 1)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,4 +173,12 @@ final class Coverage implements AddsOutput, HandlesArguments
|
|||||||
|
|
||||||
return $exitCode;
|
return $exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the comparable coverage to a percentage with one decimal.
|
||||||
|
*/
|
||||||
|
private function computeComparableCoverage(float $coverage): float
|
||||||
|
{
|
||||||
|
return floor($coverage * 10) / 10;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ final class Environment implements HandlesArguments
|
|||||||
/**
|
/**
|
||||||
* Gets the environment name.
|
* Gets the environment name.
|
||||||
*/
|
*/
|
||||||
public static function name(string $name = null): string
|
public static function name(?string $name = null): string
|
||||||
{
|
{
|
||||||
if (is_string($name)) {
|
if (is_string($name)) {
|
||||||
self::$name = $name;
|
self::$name = $name;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ use function Pest\version;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Help implements HandlesArguments
|
final readonly class Help implements HandlesArguments
|
||||||
{
|
{
|
||||||
use Concerns\HandleArguments;
|
use Concerns\HandleArguments;
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ final class Help implements HandlesArguments
|
|||||||
* Creates a new Plugin instance.
|
* Creates a new Plugin instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly OutputInterface $output
|
private OutputInterface $output
|
||||||
) {
|
) {
|
||||||
// ..
|
// ..
|
||||||
}
|
}
|
||||||
@ -61,6 +61,10 @@ final class Help implements HandlesArguments
|
|||||||
|
|
||||||
assert(is_string($argument));
|
assert(is_string($argument));
|
||||||
|
|
||||||
|
if (trim($argument) === '--process-isolation') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
View::render('components.two-column-detail', [
|
View::render('components.two-column-detail', [
|
||||||
'left' => $this->colorizeOptions($argument),
|
'left' => $this->colorizeOptions($argument),
|
||||||
'right' => preg_replace(['/</', '/>/'], ['[', ']'], $description),
|
'right' => preg_replace(['/</', '/>/'], ['[', ']'], $description),
|
||||||
@ -93,10 +97,9 @@ final class Help implements HandlesArguments
|
|||||||
*/
|
*/
|
||||||
private function getContent(): array
|
private function getContent(): array
|
||||||
{
|
{
|
||||||
$helpReflection = new \ReflectionClass(PHPUnitHelp::class);
|
$helpReflection = new PHPUnitHelp;
|
||||||
|
|
||||||
/** @var array<string, array<int, array{arg: string, desc: string}>> $content */
|
$content = (fn (): array => $this->elements())->call($helpReflection);
|
||||||
$content = $helpReflection->getConstant('HELP_TEXT');
|
|
||||||
|
|
||||||
$content['Configuration'] = [...[[
|
$content['Configuration'] = [...[[
|
||||||
'arg' => '--init',
|
'arg' => '--init',
|
||||||
@ -120,6 +123,21 @@ final class Help implements HandlesArguments
|
|||||||
], [
|
], [
|
||||||
'arg' => '--todos',
|
'arg' => '--todos',
|
||||||
'desc' => 'Output to standard output the list of todos',
|
'desc' => 'Output to standard output the list of todos',
|
||||||
|
], [
|
||||||
|
'arg' => '--notes',
|
||||||
|
'desc' => 'Output to standard output tests with notes',
|
||||||
|
], [
|
||||||
|
], [
|
||||||
|
'arg' => '--issue',
|
||||||
|
'desc' => 'Output to standard output tests with the given issue number',
|
||||||
|
], [
|
||||||
|
], [
|
||||||
|
'arg' => '--pr',
|
||||||
|
'desc' => 'Output to standard output tests with the given pull request number',
|
||||||
|
], [
|
||||||
|
], [
|
||||||
|
'arg' => '--pull-request',
|
||||||
|
'desc' => 'Output to standard output tests with the given pull request number (alias for --pr)',
|
||||||
], [
|
], [
|
||||||
'arg' => '--retry',
|
'arg' => '--retry',
|
||||||
'desc' => 'Run non-passing tests first and stop execution upon first error or failure',
|
'desc' => 'Run non-passing tests first and stop execution upon first error or failure',
|
||||||
@ -140,6 +158,59 @@ final class Help implements HandlesArguments
|
|||||||
'desc' => 'Set the minimum required coverage percentage, and fail if not met',
|
'desc' => 'Set the minimum required coverage percentage, and fail if not met',
|
||||||
], ...$content['Code Coverage']];
|
], ...$content['Code Coverage']];
|
||||||
|
|
||||||
|
$content['Mutation Testing'] = [[
|
||||||
|
'arg' => '--mutate ',
|
||||||
|
'desc' => 'Runs mutation testing, to understand the quality of your tests',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --parallel',
|
||||||
|
'desc' => 'Runs mutation testing in parallel',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --min',
|
||||||
|
'desc' => 'Set the minimum required mutation score, and fail if not met',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --id',
|
||||||
|
'desc' => 'Run only the mutation with the given ID. But E.g. --id=ecb35ab30ffd3491. Note, you need to provide the same options as the original run',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --covered-only',
|
||||||
|
'desc' => 'Only generate mutations for classes that are covered by tests',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --bail',
|
||||||
|
'desc' => 'Stop mutation testing execution upon first untested or uncovered mutation',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --class',
|
||||||
|
'desc' => 'Generate mutations for the given class(es). E.g. --class=App\\\\Models',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --ignore',
|
||||||
|
'desc' => 'Ignore the given class(es) when generating mutations. E.g. --ignore=App\\\\Http\\\\Requests',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --clear-cache',
|
||||||
|
'desc' => 'Clear the mutation cache',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --no-cache',
|
||||||
|
'desc' => 'Clear the mutation cache',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --ignore-min-score-on-zero-mutations',
|
||||||
|
'desc' => 'Ignore the minimum score requirement when there are no mutations',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --covered-only',
|
||||||
|
'desc' => 'Only generate mutations for classes that are covered by tests',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --everything',
|
||||||
|
'desc' => 'Generate mutations for all classes, even if they are not covered by tests',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --profile',
|
||||||
|
'desc' => 'Output to standard output the top ten slowest mutations',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --retry',
|
||||||
|
'desc' => 'Run untested or uncovered mutations first and stop execution upon first error or failure',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --stop-on-uncovered',
|
||||||
|
'desc' => 'Stop mutation testing execution upon first untested mutation',
|
||||||
|
], [
|
||||||
|
'arg' => '--mutate --stop-on-untested',
|
||||||
|
'desc' => 'Stop mutation testing execution upon first untested mutation',
|
||||||
|
]];
|
||||||
|
|
||||||
$content['Profiling'] = [
|
$content['Profiling'] = [
|
||||||
[
|
[
|
||||||
'arg' => '--profile ',
|
'arg' => '--profile ',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user