mirror of
https://github.com/pestphp/pest.git
synced 2026-09-05 22:33:35 +02:00
Compare commits
2215 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 375b92e93b | |||
| e0e00ca748 | |||
| d70ecb873e | |||
| 638932ddb5 | |||
| ac48185736 | |||
| 2e4f8ad56f | |||
| 8c554a81e2 | |||
| 42d9b777bf | |||
| eb88513baf | |||
| 7b6415ccd0 | |||
| a5d726b678 | |||
| 93223caa56 | |||
| d0572ce138 | |||
| b0d1fc589d | |||
| 5a4f124199 | |||
| c3ceb7d0c7 | |||
| 16d1208344 | |||
| 1cebc84c2d | |||
| 1f79660add | |||
| 37bdf60a1c | |||
| cc7acfe485 | |||
| 7bfb2a6185 | |||
| ca56ca5216 | |||
| b49ba062d5 | |||
| 09699847a2 | |||
| 545d0c2784 | |||
| b795af3b1b | |||
| a40cc6bc0e | |||
| 4d3d0105b7 | |||
| e90e4f70fc | |||
| d112582857 | |||
| 9f3c4e1e82 | |||
| 71f39366c2 | |||
| bfd5b75677 | |||
| 411b9954b5 | |||
| db70017cb2 | |||
| 86adaedbbd | |||
| 92c7677c6e | |||
| 872f0a50c2 | |||
| 668809bc75 | |||
| 8d8f45c843 | |||
| 086b3e9107 | |||
| 43fe26f324 | |||
| 865c1e5113 | |||
| 19eed8d581 | |||
| ad1850b110 | |||
| 585de259a2 | |||
| 953664dce0 | |||
| 1dd959848c | |||
| 047753c836 | |||
| b63626a94d | |||
| ae080becd6 | |||
| aa86304794 | |||
| 75963ea973 | |||
| 5d0ceb1a5d | |||
| 6f0c0536a4 | |||
| 6b2cd358e8 | |||
| e09e1be76d | |||
| 759ac63139 | |||
| 333f469efb | |||
| c4ec925aa5 | |||
| 5c8cfed483 | |||
| 962510b7e3 | |||
| e9c268a584 | |||
| fa91fcf744 | |||
| c6073511a2 | |||
| b5e1b83bb4 | |||
| 1c11595504 | |||
| cd0e921158 | |||
| f8c4c061f8 | |||
| 81eacd79fd | |||
| 1ef680c75d | |||
| 820fa08313 | |||
| 9e79e491e2 | |||
| 1b83b8fcda | |||
| ff4aba2ffa | |||
| feaac1ab7e | |||
| bbc6ada21e | |||
| 3927dbfcdf | |||
| b52bbb4cca | |||
| 52819501eb | |||
| 7b18287374 | |||
| 33e3bf2888 | |||
| 72ab506d7b | |||
| 5dc49a71d6 | |||
| 1adb484d3f | |||
| aadf7e92a6 | |||
| df42b9ed7b | |||
| 632ae79c8b | |||
| b5fed82883 | |||
| 145e20db75 | |||
| ee2e97e932 | |||
| b561236325 | |||
| e685179b7e | |||
| ac8feafdcc | |||
| b49cd150c9 | |||
| 5cfb4133bf | |||
| 3c8bae5f05 | |||
| b2998bc69e | |||
| 3876093cd2 | |||
| 932f8bcc07 | |||
| d393799d2a | |||
| 0d7814ca16 | |||
| 8467c64c22 | |||
| 97714a7088 | |||
| afb582616d | |||
| 15e9b6a507 | |||
| 520ce29376 | |||
| 87882a8561 | |||
| 77ef7e0df1 | |||
| 12100dd901 | |||
| 20c12d006e | |||
| 3aab02d2eb | |||
| 553aac65e6 | |||
| be49a3ce18 | |||
| 774a340400 | |||
| 04c9d41895 | |||
| dfb7b870af | |||
| 37821e1108 | |||
| cd711a25d5 | |||
| eee60c9e11 | |||
| c40c8dbc24 | |||
| 2d93c9c373 | |||
| 3d5bba93f8 | |||
| e5ab3af05b | |||
| 79bc7a8257 | |||
| 40b88b62ef | |||
| e3361bc321 | |||
| fc48c1bd1e | |||
| 92e76eb5ab | |||
| da726beffc | |||
| 4ef12b9aac | |||
| bd22f478b8 | |||
| eeaac34cf6 | |||
| b9b07d8983 | |||
| 6aa7d2f891 | |||
| 4d550cecfd | |||
| 1c21a7647a | |||
| d649de1988 | |||
| 783ca4bcd6 | |||
| ba07497219 | |||
| 34695843b3 | |||
| 1ca021dea6 | |||
| d17be9decd | |||
| b828ddcec7 | |||
| f859bb179d | |||
| 2fc75cfcf0 | |||
| 6cc48f63f8 | |||
| e0419d1328 | |||
| faa6988801 | |||
| c12247fafd | |||
| 29b4452443 | |||
| 1b168aba1c | |||
| 6aabd977cd | |||
| a882543c53 | |||
| c250b9da4f | |||
| 46bc3dc628 | |||
| d3ce498b8a | |||
| e1a4b98b71 | |||
| 9afbcd5c18 | |||
| 75593b6454 | |||
| 89590d6120 | |||
| fb0978c9bf | |||
| a3796daa42 | |||
| e3004db666 | |||
| 99cc4e0146 | |||
| a47e6f8fef | |||
| 536d79f765 | |||
| 65c0fbc528 | |||
| 9e4cf4b665 | |||
| 7bea819978 | |||
| 4280233b40 | |||
| d6db3a8a20 | |||
| 51c8ce4df6 | |||
| 5b8393b925 | |||
| e4d9b61fdf | |||
| e2d940cd53 | |||
| 380ccd30b4 | |||
| 31c200716d | |||
| 6add4da543 | |||
| 8ddcd3e853 | |||
| e3e178fd94 | |||
| 7b1ec9f003 | |||
| 1e48c5d473 | |||
| d00ec95dd9 | |||
| 89f3d6cb39 | |||
| a07a2e512a | |||
| 57eecb2b3d | |||
| 9f804dc954 | |||
| 7cbad4c589 | |||
| 5cae93b059 | |||
| df829ad19d | |||
| 635460653c | |||
| 1aa80dc398 | |||
| 8a14056111 | |||
| f247dd8e7b | |||
| 1c7c9754fd | |||
| 5f37939fda | |||
| 28305fcb7a | |||
| 5242803694 | |||
| 925935a7e8 | |||
| 460401c379 | |||
| 348b439172 | |||
| a4e77766c5 | |||
| 4a8c2d7d78 | |||
| 7d51601120 | |||
| 631bbe318b | |||
| 9b7c15d5b6 | |||
| 872796bd9b | |||
| c38d32ae86 | |||
| 6407c4f78f | |||
| 6e1bf63f6a | |||
| 1d3e8bb5dd | |||
| 3cc9b169e3 | |||
| c4911d046b | |||
| d0295f6168 | |||
| 21efbc3107 | |||
| e59b99cd73 | |||
| bf48e20880 | |||
| 53db68e005 | |||
| 34f1e9a7f2 | |||
| 57fd5ce042 | |||
| 3bcabfb63b | |||
| aa3a7c303a | |||
| 5c08a135f7 | |||
| 6e0e030d71 | |||
| b2c07561e7 | |||
| 97600b6f0b | |||
| 8a51f15d65 | |||
| a349f53964 | |||
| a725e774c0 | |||
| bed5e5b54a | |||
| 45b1d4ce20 | |||
| d106b70766 | |||
| 6ac6c1518e | |||
| fda515a17f | |||
| 0a97d3a288 | |||
| 3802fa80e6 | |||
| 5c3cbc14d2 | |||
| 6b9c768172 | |||
| 4a2fc179ae | |||
| b5bb2139dc | |||
| 07416a3c61 | |||
| 30b94e3034 | |||
| be34eecb2f | |||
| 5d9f95f8d4 | |||
| 48b70a03d5 | |||
| 4b8642b972 | |||
| 8711d51eac | |||
| 58dfb6da64 | |||
| d7735d1faa | |||
| 6b59166f3c | |||
| 3a26028d17 | |||
| 3c91bf4ad2 | |||
| 6a434be0f6 | |||
| f355b99bbf | |||
| 95a00341e9 | |||
| 466259646d | |||
| 00f8d56083 | |||
| ca2dca592d | |||
| 405d8d4406 | |||
| b944ee5841 | |||
| f4e22dcafe | |||
| 339c1e8cac | |||
| d4c7362132 | |||
| 81bfdbf8fe | |||
| f45cbf43c5 | |||
| b9088d23fb | |||
| 7250185423 | |||
| e457eb0e9c | |||
| 48357c6f30 | |||
| b46f051550 | |||
| 3d3c5d41ac | |||
| caabebf2a1 | |||
| 470a5833d4 | |||
| c1feefbb9e | |||
| e876dba8ba | |||
| d9c18f9c02 | |||
| 660b57b365 | |||
| 68527c996f | |||
| c6a42a2b28 | |||
| fcf5c27914 | |||
| 856a370032 | |||
| e24882c486 | |||
| 51fc380789 | |||
| f6609f4039 | |||
| 2941f9821f | |||
| ed399af43e | |||
| 0d66dc4322 | |||
| 7e4280bf83 | |||
| a5915b16ab | |||
| 1476b529a1 | |||
| 2892341c28 | |||
| 59e781e77b | |||
| 55a3394f8c | |||
| 0d99c33c4e | |||
| adc5aae6f8 | |||
| 980667e845 | |||
| 8c849c5f40 | |||
| 47f1fc2d94 | |||
| 9c8033d60c | |||
| 42d1092a9e | |||
| c7e32f5d33 | |||
| d379128cc4 | |||
| f09d6f2064 | |||
| 494cc6e2a4 | |||
| f52a455773 | |||
| 184f5d2742 | |||
| 1d81069a2a | |||
| 4b9bb77b54 | |||
| c440031e28 | |||
| 18bbca748f | |||
| bff44562a9 | |||
| 9ebb990f96 | |||
| f142aad8ad | |||
| cabff738f7 | |||
| 0746173a32 | |||
| 74a28d4f5e | |||
| 6053e15d00 | |||
| 87db0b4847 | |||
| 6ba373a772 | |||
| 945d476409 | |||
| a8cf0fe2cb | |||
| 2ae072bb95 | |||
| 59d066950c | |||
| 0dd1aa72ef | |||
| 4e03cd3edb | |||
| eeab24e2bb | |||
| 9b64d5425a | |||
| 0acab1cbb4 | |||
| e616eab9fb | |||
| 7cbb1fcdb2 | |||
| cb5f6e1bd2 | |||
| 985dadd934 | |||
| 10aee6045c | |||
| 4ac14b2528 | |||
| 2d649d765f | |||
| 4fb4908570 | |||
| 13c322bab3 | |||
| 3855249ce9 | |||
| f528bd8427 | |||
| acd8aafa63 | |||
| e8d630e774 | |||
| b6385dc865 | |||
| 02dc8d7bcc | |||
| 729f18a152 | |||
| bdf60cea91 | |||
| 3a8ee8291c | |||
| 654cb726c9 | |||
| bce26aeaad | |||
| 5948bcd71e | |||
| 89006d83a9 | |||
| a8e974d64a | |||
| 617b074049 | |||
| 2eea71a664 | |||
| 4b5374d507 | |||
| 9085561ece | |||
| b71bfc513a | |||
| 75938ac9eb | |||
| e766825f5b | |||
| 8a83a1a1a9 | |||
| e63a886f98 | |||
| 109bb22c5e | |||
| 89dd212d84 | |||
| cd07c6d966 | |||
| 8dddb47ad5 | |||
| 3a6c2fab37 | |||
| 281dbf6cf4 | |||
| 40c8429058 | |||
| 8dd650fd05 | |||
| d9d46c73f8 | |||
| fbca346d7c | |||
| 3f13bca0f7 | |||
| d3acb1c56a | |||
| e601e6df31 | |||
| 6fdbca1226 | |||
| 54359b895f | |||
| e44c554a0b | |||
| 44c04bfce1 | |||
| 271c680d3c | |||
| 9797a71dbc | |||
| c1a54df233 | |||
| 4a1d8d27b8 | |||
| 0f6924984c | |||
| ce05ee9aad | |||
| 3d2ebdb273 | |||
| 668ca9f5de | |||
| f47b74445b | |||
| 6c42e7f4ea | |||
| be3ff37517 | |||
| a087555383 | |||
| 4b50cb486d | |||
| f7175ecfd7 | |||
| 07737bc0b2 | |||
| f659a45311 | |||
| e6ab897594 | |||
| a753b41409 | |||
| 1a4c06bd6e | |||
| 12c1da29ee | |||
| 5d42e8fe3a | |||
| 9d17b872dd | |||
| 2a80101f42 | |||
| f7015fe59c | |||
| 7281e0ded7 | |||
| 1675dd1d41 | |||
| fa27c8daef | |||
| f0a08f0503 | |||
| 2c040c5b1f | |||
| a9ce1fd739 | |||
| 3533356262 | |||
| 4aa41d0b14 | |||
| e4ed60085c | |||
| e2b119655d | |||
| fcf5baf0a9 | |||
| df7b6c8454 | |||
| 5de8693e3b | |||
| 7d80f1d20e | |||
| b3119cc120 | |||
| 4e294edf76 | |||
| f96a1b2786 | |||
| a49cf7edc5 | |||
| b0f6a74cb6 | |||
| aaa226f6a6 | |||
| 69cb752d02 | |||
| cf00e58b7d | |||
| 1f39b28e2c | |||
| 9fcbca69d4 | |||
| b081584ab6 | |||
| 6966802afc | |||
| c61dcad42b | |||
| ec3e0b2d33 | |||
| c3620840b4 | |||
| 10a19f16ba | |||
| a956de5446 | |||
| 3a4329ddc7 | |||
| e6f511302b | |||
| dd01229d7b | |||
| c7e4efcea4 | |||
| df3205e814 | |||
| bc57a84e77 | |||
| bc39830d8a | |||
| 3a566b100e | |||
| 9fe61e0e56 | |||
| e86bec3e68 | |||
| 58b8f3cc5d | |||
| c157b661f2 | |||
| be90610f17 | |||
| 1701a306c3 | |||
| 064ab3fc2e | |||
| 44e315df98 | |||
| 62694c14b9 | |||
| 7c43c1c583 | |||
| 6a96aed654 | |||
| b1c997a869 | |||
| b4172e2c2e | |||
| ae419afd36 | |||
| 27aa305897 | |||
| 0e7c2abe8b | |||
| f5820bd670 | |||
| 41fd831153 | |||
| 51340439e8 | |||
| 1a39826935 | |||
| bd5fed9e12 | |||
| 26345fd9f4 | |||
| ae1da79ac1 | |||
| 00990efc97 | |||
| 477d20a54f | |||
| b7b16096db | |||
| 4105e33c39 | |||
| 08b09f2e98 | |||
| b0fab7e437 | |||
| 8e3444e1db | |||
| dc9a1e8ace | |||
| fc7a4182b5 | |||
| b7406938ac | |||
| 314caabd1d | |||
| 65cabf91b1 | |||
| f91c6c1e1e | |||
| 843dbbf18a | |||
| 47fb1d7763 | |||
| 639df4cb43 | |||
| e54e4a0178 | |||
| 7749775f50 | |||
| f11f3aa0a4 | |||
| 33817013fe | |||
| 7f11ace329 | |||
| 3d776f1f20 | |||
| d5ced0a5ca | |||
| af1e214be4 | |||
| 7f9b50974a | |||
| cd5272d8cc | |||
| a7b2039175 | |||
| 72cf695554 | |||
| 50960a96e9 | |||
| 507df757a1 | |||
| 8722b3fc3c | |||
| 19eca6e338 | |||
| 6b523d6963 | |||
| a350545803 | |||
| 71c2e97c9f | |||
| 98a12012bf | |||
| 027f4e4832 | |||
| 92523a6f39 | |||
| ed38fb644f | |||
| 39b66bf01d | |||
| 165c879fe6 | |||
| 4c8bf4b2fd | |||
| 1ee36f584d | |||
| 1b0a846a81 | |||
| e3e518747f | |||
| 0b96b8f630 | |||
| 711a60c2db | |||
| e7132fa012 | |||
| 3b72bbd7fe | |||
| 273edb864c | |||
| fcb60f3c4a | |||
| 91bb7589e2 | |||
| e524bf5f73 | |||
| 27414ce19f | |||
| fbc9e704e2 | |||
| ee6b3ed062 | |||
| 4c88590b89 | |||
| 66e59efec6 | |||
| f692be3637 | |||
| 127ad618d3 | |||
| da04ba62a8 | |||
| d187566e63 | |||
| 3e86e158b2 | |||
| d6c6489e93 | |||
| ee70a3cfea | |||
| 7a6f33f139 | |||
| 55218bcf78 | |||
| e29302300f | |||
| 2a47b514ec | |||
| 222ed174bc | |||
| 2aa32569f0 | |||
| 1d8d1a046f | |||
| 3d9ceb1cf2 | |||
| 520a5fe29d | |||
| de4409e368 | |||
| 6d6e4e040f | |||
| aac08629f7 | |||
| fe27012bbc | |||
| f9901245f1 | |||
| 21e22decf3 | |||
| e513f76ea9 | |||
| be9c95e3bc | |||
| 9172721ce8 | |||
| 924dc016cc | |||
| f49b91ec0d | |||
| 516ace85b4 | |||
| f9814793dd | |||
| 00572f5f8e | |||
| fb282b184e | |||
| e0695a13cb | |||
| 8f810bf2a2 | |||
| 84636cee96 | |||
| 0355119afc | |||
| 9d0410ee0b | |||
| 0d148c2a67 | |||
| 0f1e87c726 | |||
| 73bf579da3 | |||
| 5def62018b | |||
| d8e1b27491 | |||
| 2ff4713968 | |||
| 3f27352560 | |||
| af3fdceddb | |||
| 3faeede1ef | |||
| 0bc3219a2b | |||
| a22013a7d3 | |||
| 7fc69033f8 | |||
| ef76c04dbe | |||
| 7d77bbf1bb | |||
| 163479ae60 | |||
| c3bfdf130e | |||
| 8c403a57c2 | |||
| 97c136cd94 | |||
| d6cbd12d8b | |||
| 49bf00024f | |||
| dd44ac4195 | |||
| 5d2aafd2a3 | |||
| 0fc9d4dfe0 | |||
| 02b1ffb334 | |||
| c62cc3fef0 | |||
| 909d778da3 | |||
| 7711a52fe9 | |||
| 99c9f4e5d8 | |||
| a310796165 | |||
| db9243ca2e | |||
| 635e3b4c41 | |||
| 791734a29c | |||
| 8cfb0acf46 | |||
| bf67407ba5 | |||
| efdc84e115 | |||
| d1608bf33d | |||
| 4f6140fdb1 | |||
| 442a58d07f | |||
| 19e9267021 | |||
| c6244a8712 | |||
| eed68f2840 | |||
| 6080f51a0b | |||
| e0f07be017 | |||
| 42e1b9f17f | |||
| 0171617c1d | |||
| 2e11e9e65d | |||
| 4969526ef2 | |||
| d7b1c36fdd | |||
| 003fc96e8f | |||
| f68d11ccae | |||
| e46d499384 | |||
| 490f321a0d | |||
| 174645caa2 | |||
| ed70c9dc2b | |||
| 157a753d87 | |||
| 2c3a53f6cd | |||
| 0bdaef29e9 | |||
| 1ad30a97b3 | |||
| a5317c5640 | |||
| 66ceb64faa | |||
| fa4098db8d | |||
| 4a987d3d5c | |||
| 4079a08f5f | |||
| e4aab77a34 | |||
| 50ff347b59 | |||
| b5b8fab09b | |||
| c4c9e915f4 | |||
| e834527db2 | |||
| 23f130b0f9 | |||
| 0cb8c42497 | |||
| fe4b5e5e1f | |||
| 8ee9d66d80 | |||
| 7760d945bb | |||
| 709ecb1ba2 | |||
| 6afb36519d | |||
| 150bb9478d | |||
| bf3178473d | |||
| d2eb94d723 | |||
| 9688b83a3d | |||
| 675372c794 | |||
| c18636b3d5 | |||
| 1ac594bdf0 | |||
| 145294a4a3 | |||
| c2cabaeae6 | |||
| 918a8fc169 | |||
| 5d32dd0641 | |||
| 982353fb38 | |||
| 2eefa8b88d | |||
| 787d5492ac | |||
| 06a0bd9b0b | |||
| 5331b44a18 | |||
| 91afc81222 | |||
| 179d46ce97 | |||
| fa2bc1e536 | |||
| eaeb133c77 | |||
| cf57ea1f94 | |||
| 0b7f4f2384 | |||
| 2903a7e621 | |||
| b8964375c7 | |||
| bdcb883829 | |||
| 8a7e7f39ef | |||
| 53c94600cb | |||
| 67f217852c | |||
| 1bad148487 | |||
| e24f137b8e | |||
| 6d9189f3f5 | |||
| 6968094e2b | |||
| 9510d4a2f9 | |||
| cd2eb3504b | |||
| 7c639cdbbd | |||
| 1513ede73b | |||
| 8c65197881 | |||
| a6cd83665c | |||
| 0c57142c03 | |||
| 3f65af9fdf | |||
| 42d89814e3 | |||
| 1e3156a5b6 | |||
| 97713c0832 | |||
| 62b0e3c9df | |||
| 647de2f1cf | |||
| 0a7bff0d24 | |||
| 7618434580 | |||
| dd7d150caa | |||
| 1e0bb88b73 | |||
| 83b76d7c2e | |||
| 5a870b3940 | |||
| 1115c64186 | |||
| e38a271ca2 | |||
| 43703ab40a | |||
| 86452765a4 | |||
| b8a1b7e5cc | |||
| 5fe79d9c18 | |||
| 2744da4292 | |||
| 87f4e5e7b3 | |||
| bb3decf3cc | |||
| 4e2987d438 | |||
| a25158bce8 | |||
| 49e77b1d4c | |||
| 989e43d1a0 | |||
| 7cd42aafd8 | |||
| 48a1de273f | |||
| 970e16e949 | |||
| 432ff221c6 | |||
| a55da85dd2 | |||
| f291cd1603 | |||
| 5de0c2254a | |||
| b98ce0ced3 | |||
| 28772c2609 | |||
| 452ffaf8df | |||
| e8338405b5 | |||
| 1b014e4b18 | |||
| 034715e8b1 | |||
| 09eff785c4 | |||
| 22cc7805d7 | |||
| 669dc0da71 | |||
| 689da4ed4e | |||
| 92bc1decd9 | |||
| e3bfcbe5f1 | |||
| 2f15861b0d | |||
| ba7eb70a5d | |||
| 74ff3b8cd9 | |||
| ab0b4a1b4e | |||
| 169b76458e | |||
| 668685498f | |||
| bab193e7e1 | |||
| f720be862e | |||
| 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 | |||
| 55b9266648 | |||
| 4313a1ef20 | |||
| 005ef03845 | |||
| bbac28c9f4 | |||
| eb56483ba2 | |||
| 5d6b717c9a | |||
| e888f3613b | |||
| e69899559d | |||
| e6fe968d44 | |||
| 678898efe7 | |||
| 6c3d8829ce | |||
| 14859a4c89 | |||
| 8a44d3f136 | |||
| be71d6918d | |||
| afb3dd459a | |||
| b6e3ffafa7 | |||
| 6c95f3d8cf | |||
| 2192373bec | |||
| dfcdaa3f8e | |||
| 79bc9e677f | |||
| 60b615ea6a | |||
| 8787481e40 | |||
| c24406259f | |||
| cbd6a65057 | |||
| 175004baf3 | |||
| 6d9c0483a6 | |||
| 2dc413cba0 | |||
| 206548af2b | |||
| af6de422e9 | |||
| 1c7b254395 | |||
| de1c721cd9 | |||
| f8dd286213 | |||
| e11337df2d | |||
| 2f90d4ccd7 | |||
| 2db15af24a | |||
| 8ea7b2b802 | |||
| c9e3932637 | |||
| d218afaf77 | |||
| 19739ff814 | |||
| 478144fb35 | |||
| 5d81cf0d4c | |||
| 0b115230f9 | |||
| 0b246f7a76 | |||
| 7914224ff7 | |||
| 997b0e9368 | |||
| a76414aeee | |||
| d2096df82a | |||
| 4951b1b0f9 | |||
| f2e31452f2 | |||
| c2985ffb31 | |||
| 492f797dd5 | |||
| 0b261ef97b | |||
| f19692a72f | |||
| 0787b37f2c | |||
| f0223b50d0 | |||
| 0263fcb2ac | |||
| c0a234317b | |||
| 72100075d2 | |||
| a7aa923241 | |||
| e012517b16 | |||
| b1dd18af8a | |||
| 398e3ff3b5 | |||
| 03648f580c | |||
| df2212055b | |||
| b1a137c513 | |||
| 62267dfd3e | |||
| f996a48dfa | |||
| 54e00dd4dc | |||
| f1414a0beb | |||
| 47f2ae32c1 | |||
| 306b7eb2a6 | |||
| 02f72aabb2 | |||
| e3a21384e6 | |||
| 331381eed5 | |||
| 75a7d77a80 | |||
| cc242a50d1 | |||
| 704acbf6de | |||
| 7baa48e068 | |||
| 3742e74adf | |||
| cbcea04768 | |||
| f0581f87c6 | |||
| 60763f2223 | |||
| 8a589022d9 | |||
| 00109e9976 | |||
| 43107c1743 | |||
| 546253d591 | |||
| d94a6580f5 | |||
| fb75b712d3 | |||
| 6ead2a4e8b | |||
| 9afe2296a6 | |||
| 0221c2a643 | |||
| 0518971d2f | |||
| fe3747f850 | |||
| 844d175981 | |||
| 4e719214c6 | |||
| 2f6b99885e | |||
| 4b24da1a58 | |||
| 72d482de28 | |||
| 049da041b2 | |||
| 4d7aa2b98f | |||
| 2e352c0084 | |||
| 3f854713e6 | |||
| 011bd3ba82 | |||
| 4de70da0a0 | |||
| 6820d8b7aa | |||
| 6886558ed1 | |||
| b795a92840 | |||
| 2e622f6fd4 | |||
| 5f7a1663dd | |||
| f3f35a2ed1 | |||
| 86a6b32715 | |||
| 1efb9de043 | |||
| b60d21dfe2 | |||
| 39e0d61dec | |||
| be41181b43 | |||
| 632ffc2f8e | |||
| 705f19dd87 | |||
| 5637dfa75d | |||
| cf5275293f | |||
| 81efe5953b | |||
| a37a3b9f99 | |||
| 9100913184 | |||
| 8fdb0b3d32 | |||
| 8322ff0f5e | |||
| c8287567eb | |||
| b00bc4d5ea | |||
| 8abc0d1920 | |||
| ea967b439f | |||
| 23d7191990 | |||
| c7e6df7c95 | |||
| 805b81edc0 | |||
| c42541a3d9 | |||
| 3f352605ca | |||
| 21a04fefcf | |||
| aa4fb3bba2 | |||
| 4d0dffafd3 | |||
| 19e75d1070 | |||
| cee5b9feb9 | |||
| 355a2349af | |||
| 7e815cc985 | |||
| fb443e0fa0 | |||
| 7f1135eeac | |||
| 25e15e76e0 | |||
| 9426881cf6 | |||
| 1f6970a5b3 | |||
| e541ee86fc | |||
| b1c6f247e0 | |||
| 36b585835d | |||
| 17db4bd616 | |||
| c98d8ca26a | |||
| d5334f96a4 | |||
| 54f4ee57ad | |||
| 4f3796ed2e | |||
| ac13a288fb | |||
| e2ccc9deac | |||
| 80129f2e23 | |||
| 5802bbc1dd | |||
| ee2f4eedbd | |||
| 0de1ce053a | |||
| be9056f978 | |||
| 26a6e7d712 | |||
| a90b90ad29 | |||
| bc951787d3 | |||
| 0ae0887665 | |||
| 551fa01415 | |||
| 68ea2c7d7e | |||
| 3e8616ec64 | |||
| 465c65243d | |||
| 9c0e5ddfc6 | |||
| 8442b9a6e4 | |||
| a1208b5876 | |||
| b5f89d1ff8 | |||
| cd823193cc | |||
| eb7bb34825 | |||
| 19e3d929b1 | |||
| 25729f6262 | |||
| cbb6a58c8a | |||
| 167c96965e | |||
| 8db3238a1f | |||
| cbd8cae83e | |||
| 3e03a87e02 | |||
| 193dd107d7 | |||
| 0cea8fe922 | |||
| abbbc9fdbb | |||
| 10b210d2bb | |||
| 9385a3dcea | |||
| c0af671ca2 | |||
| eb9f31edeb | |||
| 0e5470b192 | |||
| 2122e57990 | |||
| e42d224db2 | |||
| 070139eda7 | |||
| a0d0182031 | |||
| 1885d4e110 | |||
| 5959890125 | |||
| 7460bd6c1f | |||
| 801346b894 | |||
| 40fd06c0d0 | |||
| aa9fe351a6 | |||
| e00efb1b6d | |||
| 86a765b06b | |||
| 3c20e8114e | |||
| 73a859ee56 | |||
| c9180e590e | |||
| 4196579a3d | |||
| 436b20857d | |||
| 82b0adf5ae | |||
| a0041f139c | |||
| b25bc7ee05 | |||
| faafedd55c | |||
| c99325275a | |||
| 37a7583755 | |||
| a851b5ed70 | |||
| 983659f8e8 | |||
| 3e1cef296f | |||
| cada5c5136 | |||
| 477492fdd2 | |||
| 963b7f43ab | |||
| a34767fa15 | |||
| fe3c7d72bd | |||
| 97898a0a8e | |||
| cddddc3ec1 | |||
| f8930d20ae | |||
| 2e25eb59b8 | |||
| a5471fd0f4 | |||
| 9842e1051d | |||
| d39e4f15fe | |||
| 1a05df14d0 | |||
| 5c0df87f52 | |||
| ec6a81735a | |||
| 2ea37f3424 | |||
| 2e4206cb97 | |||
| 72b3cdaab4 | |||
| 9744b9848e | |||
| e1143d2cfc | |||
| 5861b0498a | |||
| f6d9aa51bd | |||
| 9070b12377 | |||
| dab68d6d85 | |||
| 40de54ecd5 | |||
| 6d8b926df3 | |||
| d6c3f3522b | |||
| 41cdb5f01b | |||
| 0064f3fdff | |||
| 518035514e | |||
| 81cdb7a300 | |||
| 97dbda92e6 | |||
| 4520fe918a | |||
| f73e6aebcb | |||
| 3b9ca8853f | |||
| cbc26faeb9 | |||
| 12c75524a2 | |||
| 6edbd69dc4 | |||
| 0cfb1237e5 | |||
| cab6aa6e57 | |||
| 9ff2f3b46c | |||
| ead0237fb3 | |||
| 31726b51ad | |||
| 99d3ee448c | |||
| d21ae255f1 | |||
| 0823e5da88 | |||
| 1f913f6fc1 | |||
| a6dec31b9d | |||
| c1979f735f | |||
| 1c1cb1e591 | |||
| 49ddcbd66b | |||
| 23c8adf497 | |||
| 8e3d7d85e8 | |||
| 3cee6a499d | |||
| 3f38f19df8 | |||
| 4df6ab4a84 | |||
| 90b6771451 | |||
| 681723cbc3 | |||
| d9595ec122 | |||
| ec35010cf4 | |||
| d14f2728d8 | |||
| 152b7a9fc4 | |||
| 56ec3b9ee3 | |||
| ca69e4fd94 | |||
| 1965763cd0 | |||
| cd8d94780f | |||
| ba87e1fde8 | |||
| e48bd31ae4 | |||
| 5ae061d208 | |||
| 3d7e621b7d | |||
| bf14c4262a | |||
| b186d7a4ee | |||
| e109cd1da2 | |||
| 473f295b77 | |||
| a05684026e | |||
| 5df46d03c3 | |||
| 19424ae06d | |||
| 6c8970e0a3 | |||
| 2f2b51ce3d | |||
| 33f596bcce | |||
| 50a96dcb8f | |||
| d9a4fa33b9 | |||
| cc6bd59df9 | |||
| 3ce6408195 | |||
| 1c673fcff9 | |||
| ff82596158 | |||
| 0539d2ba62 | |||
| 221ac62f03 | |||
| 4b6c949032 | |||
| 1915ad368a | |||
| 1408cffc02 | |||
| 95b5379945 | |||
| a4833bbfe4 | |||
| cb1c777b9b | |||
| 7433cc5565 | |||
| 4c769fac66 | |||
| 176d3efbc6 | |||
| d635665c1b | |||
| 22467d05c8 | |||
| 7a699e16db | |||
| 341ba56bb9 | |||
| a320cc3e2b | |||
| 8b428357b2 | |||
| bb6d6b0951 | |||
| b94b8c6a4f | |||
| 43894afa18 | |||
| 28de31a8b9 | |||
| 974e70d7d1 | |||
| f914f1ad87 | |||
| 14dd5cb57b | |||
| 077ed287b7 | |||
| 9a41f2ff82 | |||
| 88f29e4180 | |||
| c34f649724 | |||
| e1e4f8d884 | |||
| 2d6d8b810b | |||
| bcd1503cad | |||
| e0f2919f62 | |||
| a8bd353ba6 | |||
| ed3bb2634d | |||
| 48ae4bfc18 | |||
| 26bb0b6eec | |||
| 236a9bd7ce | |||
| f4d19c90d3 | |||
| ecbaff503e | |||
| 9d0cd32e3f | |||
| 8782e9c34e | |||
| a4932e41de | |||
| 522ac55d5f | |||
| b3a8aef6ac | |||
| 8068bebebd | |||
| b887116e5c | |||
| 6071d86ac6 | |||
| 5723da1043 | |||
| 17e242a5f6 | |||
| c9a8007811 | |||
| c64c41a4d9 | |||
| da4bf7f5c3 | |||
| bb5dbc878e | |||
| e3ab27e2ec | |||
| 8f91f40e8e | |||
| 2973b600f5 | |||
| 37d4434000 | |||
| 627b673380 | |||
| bc1b11054c | |||
| 8350d74020 | |||
| 1431a2a897 | |||
| 2906a2de2d | |||
| 540c2a56bd | |||
| 2fb8690320 | |||
| 88e047bd27 | |||
| 8f630c064f | |||
| 54fd188299 | |||
| 4db2318a66 | |||
| 7e1c769d1c | |||
| 7a57f9f9b8 | |||
| 961e0aec66 | |||
| ccfcd336fe | |||
| 7287d65865 | |||
| d96ddaeaac | |||
| 085d3436c8 | |||
| 2da899a2b1 | |||
| 48ea48981b | |||
| 084f7c596f | |||
| aafdf6f39c | |||
| d4c66d73a0 | |||
| 7d89d3546e | |||
| 840364891c | |||
| 1f3e5115c7 | |||
| 9de85175db | |||
| 0bf051610d | |||
| 42fc80b76d | |||
| ff1249b5cf | |||
| 331585a0ba | |||
| 0c808736b8 | |||
| 9f0dc108fa | |||
| cf5c6f9ffd | |||
| f6d6a4db78 | |||
| ef3305ec23 | |||
| f6ce6cece7 | |||
| 32923f32a8 | |||
| 02f3202a61 | |||
| e6af0c8a77 | |||
| 52a8183fcb | |||
| c2f57811e1 | |||
| 639029349d | |||
| 595172aba6 | |||
| e7af5a4cf9 | |||
| ef7d1527da | |||
| 2a370a2a76 | |||
| b828843974 | |||
| 0b99c72937 | |||
| ec4f15132e | |||
| 0e4cc94471 | |||
| 9e89fb5e23 | |||
| 26cf278103 | |||
| 050fe38a90 | |||
| f07c3a2480 | |||
| ae365324a8 | |||
| b4b212a426 | |||
| f52d4392a6 | |||
| c662c59daf | |||
| ef4a487322 | |||
| 30b151f927 | |||
| b93bf82be6 | |||
| ea3827fe7f | |||
| 7cd6b4ff40 | |||
| 544af117bb | |||
| d71af91360 | |||
| fccb90c8ea | |||
| 4b1cad2493 | |||
| 57cd294f0f | |||
| 837552b11d | |||
| 966c382d1a | |||
| 43e17741eb | |||
| d6e3906da6 | |||
| dbef162fa6 | |||
| 1bd9c9e60d | |||
| d1adc65037 | |||
| ae15fa668d | |||
| 88e576c3a3 | |||
| 60950c624f | |||
| 3d1e0c3f9f | |||
| d6c8149a3a | |||
| aedf2e3727 | |||
| 6316828fbd | |||
| 24a9ab6761 | |||
| 0a76032b1b | |||
| 730bc7d3b3 | |||
| 99c7bb705e | |||
| fc747c306d | |||
| b6b8a72a6a | |||
| 36f005981b | |||
| eadc6f4307 | |||
| 2876ac590d | |||
| 99436f94f9 | |||
| 60f82a21db | |||
| f68e6cefd4 | |||
| b939d94cda | |||
| 37d71adb4e | |||
| 813f63360b | |||
| ad97b202c4 | |||
| 1e61034e86 | |||
| 3f6b2e856e | |||
| 3df68b288a | |||
| a75e899d98 | |||
| f25a9f5558 | |||
| 69a0c3ba99 | |||
| e1bb1d8c2d | |||
| 62238b2714 | |||
| e9f83dc020 | |||
| b15dc03d16 | |||
| 3af60b874e | |||
| 1da1eeb384 | |||
| 8abad572ec | |||
| 20012b65fb | |||
| 244087db27 | |||
| b54c24a589 | |||
| c664094f35 | |||
| f5d71b9282 | |||
| a478798cfe | |||
| a6e133a194 | |||
| 46b785f29f | |||
| 69b1c08558 | |||
| 2a1db41880 | |||
| 64dbcf0a26 | |||
| 683910bff4 | |||
| 7492b331a0 | |||
| ae8df3f51d | |||
| 658c428b71 | |||
| d7ecef80e9 | |||
| 09644640bf | |||
| ba1f2df40b | |||
| 0348037638 | |||
| 23b42730ba | |||
| 9ded563e4b | |||
| 8a48a3da7f | |||
| 61a35d65c4 | |||
| aafbd04753 | |||
| 56a2f1b733 | |||
| aa3369757c | |||
| f309e06292 | |||
| 05989c35a7 | |||
| 813a74759b | |||
| c9fb8e6f52 | |||
| d374a46c4d | |||
| 0ce70a0180 | |||
| 7fc12613a8 | |||
| d0e949bf19 | |||
| 957dabc3f0 | |||
| b4b4bf3685 | |||
| 49619ff2b5 | |||
| efca71f1e7 | |||
| 86fdfb75cf | |||
| f865e93050 | |||
| 4fccddebb0 | |||
| 15ef5f12b8 | |||
| c87aabf5d4 | |||
| 0d9c11c99a | |||
| 7fdc7d6997 | |||
| 7fe8399d48 | |||
| 9be89c4042 | |||
| a61db76c24 | |||
| ea6af719e0 | |||
| 9f5506364b | |||
| 2d70f18a93 | |||
| 427ee89ae6 | |||
| dd4247e150 | |||
| cac777bce1 | |||
| 3c0390b1ac | |||
| 2df49947df | |||
| 26369a19b4 | |||
| 2b3e146d5a | |||
| aff8e33eca | |||
| 104db3f6a5 | |||
| ec8fb202b3 | |||
| e22f6e1e4d | |||
| d57437ff02 | |||
| eb64113a3d | |||
| 3b5f9ec59d | |||
| 666b2f3fd0 | |||
| 2bc33c7cd5 | |||
| 8ee7a4deef | |||
| 69afb31bb9 | |||
| 5ca4c5bca9 | |||
| b6fb81e506 | |||
| 735f131222 | |||
| 8a58e984fe | |||
| 0e9c1bc0f7 | |||
| e749af6d91 | |||
| 052b9e051b | |||
| 6ddc5c8572 | |||
| 8eaf4859ff | |||
| e1406554fc | |||
| 504fd04705 | |||
| 7fe7a01d43 | |||
| b7ec3c59b8 | |||
| 757a98230e | |||
| c319a8e84c | |||
| 8d33c9dc89 | |||
| 2ae06a0e2d | |||
| f107fdfa08 | |||
| 2561d47bb5 | |||
| aff11486b2 | |||
| 17cda168e1 | |||
| d5495a7e3a | |||
| 87ee5ef36b | |||
| a34001faf0 | |||
| dd840f8861 | |||
| f94ea9ba0d | |||
| 6338d762fa | |||
| f48ae48677 | |||
| 1658176fe1 | |||
| 2f519261f5 | |||
| 7466667c08 | |||
| d03302db7b | |||
| 951b54e7cd | |||
| 2929af4715 | |||
| 48309931ef | |||
| d69f61c8d3 | |||
| a8f0b96338 | |||
| c1663191ee | |||
| ded14c5425 | |||
| 80bea0b4b4 | |||
| 2cdcf7f3e3 | |||
| 8d34d0743f | |||
| fe763c1dc7 | |||
| 8a2aeff9a0 | |||
| b1545f270f | |||
| 975f1bd9fa | |||
| fed9776f9b | |||
| 803ba535c3 | |||
| 60358461c4 | |||
| bdff30b511 | |||
| 6e3d940c8c | |||
| b702e0c084 | |||
| 48caaed58c | |||
| c45a303451 | |||
| 2bffd6a51e | |||
| 179e6a9db1 | |||
| 94cef989d3 | |||
| 6982b02d48 | |||
| 3a4a57a262 | |||
| 67d26388de | |||
| a26e3946bd | |||
| 026d4920cf | |||
| d305f4dca0 | |||
| 217fae0967 | |||
| 349e2f45df | |||
| 0675529320 | |||
| 0839c7e127 | |||
| 15931e2418 | |||
| f6676118ac | |||
| 78673ceeb1 | |||
| e228d565af | |||
| 51bcf6a2be | |||
| feedeab7e3 | |||
| 76d1a8ffed | |||
| 5436ff8c49 | |||
| e8b10fcc91 | |||
| 4083e6e26e | |||
| 3ecf351432 | |||
| 6595e6960b | |||
| 330b4f0171 | |||
| 43a7df2cc1 | |||
| 0e98c5c5c4 | |||
| 39ee5b9b08 | |||
| 227d32a1fd | |||
| b7e2cd758f | |||
| c9a02b964d | |||
| 0fd5b2efe1 | |||
| 476f56b617 | |||
| 174a9ca60b | |||
| 406fcf72ae | |||
| 37b1367d25 | |||
| a55953a8e0 | |||
| f120b32791 | |||
| ca113127cc | |||
| 138bdf599b | |||
| 522504916b | |||
| b04207d9ea | |||
| 9596274b14 | |||
| 8d018ea3f1 | |||
| 0f0af9eefb | |||
| 8aece3981d | |||
| 1c9c408cf3 | |||
| 3911cfec6d | |||
| 6d7fd66f82 | |||
| e1e926076a | |||
| 68bf8a2d26 | |||
| c0d9f739b3 | |||
| d33cc19778 | |||
| f940b89284 | |||
| 21990ccd8b | |||
| c01654efcc | |||
| 1ad631c528 | |||
| ecb5d9c83e | |||
| 0039dbde38 | |||
| 70f447a8bc | |||
| d29d68a2c2 | |||
| c5f6923e5a | |||
| 34878bf432 | |||
| fab08f0e20 | |||
| 850955d7dd | |||
| 606d627f1d | |||
| dfe8a3deeb | |||
| 3bc356ceec | |||
| 7ad045d6b7 | |||
| 3324455e0a | |||
| 7ffc5602b4 | |||
| 8944fdd96f | |||
| 3aaa93931a | |||
| d77715b0fe | |||
| 17cc194ad1 | |||
| f9397924fa | |||
| 9281060ab5 | |||
| 6a3cc48d6b | |||
| 7194a87d0a | |||
| 3e325e3364 | |||
| 348bd4b923 | |||
| f6cfd425c6 | |||
| 7683d791f4 | |||
| 649047f087 | |||
| 3aab10774e | |||
| e37f3d3d45 | |||
| e105afce83 | |||
| 3d16183a93 | |||
| 129325db8e | |||
| f214a78c75 | |||
| 42c10f9a62 | |||
| 8d4fa06ba3 | |||
| a73744c081 | |||
| e2ab53ed53 | |||
| 5c4e98cf0c | |||
| 6bad9d302b | |||
| db00fc8c09 | |||
| 98b04632ce | |||
| cbd4cefc1a | |||
| 84b8c389b2 | |||
| e34364d8b1 | |||
| 80854d5f87 | |||
| cbee6e76b0 | |||
| 12618ff8b3 | |||
| 115bb551df | |||
| 3799dc7a2a | |||
| a8b8adafdf | |||
| 961e44b5ad | |||
| 20e99af194 | |||
| 1c888ce055 | |||
| f3a748fee3 | |||
| 2a68234bd3 | |||
| 8275d7e08d | |||
| e9630ff9a6 | |||
| 97431f2622 | |||
| df8a64b017 | |||
| accfe86eaa | |||
| 667918905f | |||
| c1231c9bde | |||
| bac941715a | |||
| 8a3caa5e7f | |||
| 7bc513bc2b | |||
| eecc598471 | |||
| d8d1baf945 | |||
| c5cb1fc325 | |||
| 93a118a532 | |||
| d6ddd7326a | |||
| fa1145dced | |||
| 06ef57060f | |||
| 921d580d47 | |||
| 7c7c8358e9 | |||
| 9b54b61e2e | |||
| b93e3524f5 | |||
| b6598af59f | |||
| 4de7284657 | |||
| 0e0e2adfbe | |||
| 08b62f6633 | |||
| b0b83505af | |||
| 3d5271f512 | |||
| 462982bb28 | |||
| 245f636fa8 | |||
| 9fd8610390 | |||
| 7a41a540f2 | |||
| fb588711ef | |||
| 1e7e164d84 | |||
| 036f2de795 | |||
| 45011ebd14 | |||
| e9564febaf | |||
| 579b975318 | |||
| d8f3e9c313 | |||
| 01ccbfe254 | |||
| af82c1005a | |||
| 02e3b5aa77 | |||
| fb378bed56 | |||
| 1454bcf165 | |||
| f20a7cc9e4 | |||
| b21bfe3666 | |||
| 0669423138 | |||
| 3f111708e3 | |||
| a1f47f1a90 | |||
| 24a17cfb7c | |||
| bee4eda3ef | |||
| 797ebb2986 | |||
| ddb75441e7 | |||
| c61d70abf1 | |||
| 0953ae431e | |||
| b3a2e6026f | |||
| 46ffdf9c7a | |||
| 99e607dcfb | |||
| 1006b9cb7b | |||
| 6769231b00 | |||
| 3ff95faaaa | |||
| eab944023c | |||
| 00d3c735fe | |||
| 173a72e69d | |||
| 38a82cd142 | |||
| 42ceddf374 | |||
| 6252e288b9 | |||
| 8594980dae | |||
| c22fed89f3 | |||
| 429660fe57 | |||
| a4ec4b2841 | |||
| c0d66b7dc7 | |||
| 787e7f6ce5 | |||
| 2c338468bc | |||
| d8b456d89a | |||
| bff18ab381 | |||
| 4e95a65af1 | |||
| 1706e1d2e0 | |||
| 324823fef0 | |||
| afedf83f5d | |||
| 402995bf29 | |||
| c0fc52f719 | |||
| 41dbc947fa | |||
| fe09184c9c | |||
| 4821bd4423 | |||
| 1461c9ba88 | |||
| 452d4b26b9 | |||
| 3dd7b677bd | |||
| 1e86dcecd0 | |||
| 949ba1f298 | |||
| 77b7181b08 | |||
| 6e120d60f6 | |||
| 10ff36480a | |||
| ffb20c9956 | |||
| 751a532124 | |||
| 80c411be44 | |||
| db5c11d96e | |||
| f3ed9bdf8e | |||
| 53c20d9cd2 | |||
| fca0c8fc0c | |||
| c8aa204ee0 | |||
| a3889110f1 | |||
| cda4665979 | |||
| 09e2a26b7d | |||
| 3dc451cf44 | |||
| 3795870150 | |||
| 24204adc09 | |||
| a027e24e3c | |||
| 15e2e1711b | |||
| d363321db5 | |||
| 3ffed844a6 | |||
| 9cf1005183 | |||
| 3601d01bd5 | |||
| d0136b63d4 | |||
| 00029c15ef | |||
| a5cbdea868 | |||
| edd1d890ca | |||
| 985bbf4ea5 | |||
| 443f848386 | |||
| 32dbac87c8 | |||
| 1dc33070fe | |||
| 1079793ccf | |||
| 21364779f9 | |||
| 50d8688b79 | |||
| 27baad82d0 | |||
| a894386b49 | |||
| 2465b88462 | |||
| 7660517f7c | |||
| 74470ec96d | |||
| 03ccea8978 | |||
| ed89689425 | |||
| 62d7652376 | |||
| 0e85921964 | |||
| 51ec80f11f | |||
| e08d6f2803 | |||
| 22030bffd0 | |||
| bc105bc818 | |||
| 2c3a296040 | |||
| 10b204e19d | |||
| 36130eb7a0 | |||
| 6d5a8a9235 | |||
| 04663e0c8e | |||
| 6f9ebe04b0 | |||
| 8ca4caaffa | |||
| 8baaf80691 | |||
| e91c85496f | |||
| 6bf92d20ad | |||
| 35f607c46c | |||
| b99f65d936 | |||
| ead2dfd0a9 | |||
| d2ca6e630d | |||
| 12b48a6cf6 | |||
| 635a71ce66 | |||
| b99c4d611b | |||
| af8886c062 | |||
| 30b1f6cd0a | |||
| d24091d224 | |||
| b3d3b4485d | |||
| 108fe45164 | |||
| e1a30e3c92 | |||
| d969eaac2c | |||
| dd081c59b7 | |||
| b0264886c9 | |||
| 6dcdfdb82f | |||
| e7d75365fd | |||
| 266447bcc0 | |||
| b74a688677 | |||
| b1f9ce2283 | |||
| e64b6fe924 | |||
| c8697e0310 | |||
| 98db677646 | |||
| 6c3a8be049 | |||
| 129a733888 | |||
| 5aeda553a4 | |||
| 0146186ddb | |||
| 106b279ed0 | |||
| 05f44ed84a | |||
| 6e7890c206 | |||
| 2d2760e15c | |||
| b2eb69cbc1 | |||
| 0fc90ec181 | |||
| 337d55b9ab | |||
| 9f1e3dadf4 | |||
| 24943e5fbb | |||
| 33d1579660 | |||
| 8047ae570d | |||
| e236bf3821 | |||
| 32c2df0444 | |||
| 7a0e841a0d | |||
| ce2dcc3128 | |||
| 52314a4928 | |||
| d65cc9be84 | |||
| beb14ce5f4 | |||
| c49700dd47 | |||
| 3d0f267a5c | |||
| 729d7c4bef | |||
| d10b8f5d2c | |||
| 8494d4566a | |||
| 7fd9cfa2e9 | |||
| 3df78f2edf | |||
| 5f0752e874 | |||
| cdd67a6900 | |||
| fce24ef01f | |||
| 47264416b1 | |||
| 86dca12c09 | |||
| 0b5cea6df1 | |||
| 9258dcc988 | |||
| 24edab45b1 | |||
| f3371e51fe | |||
| 83b9f86972 | |||
| ca30677c53 | |||
| b205b8e748 | |||
| b9b9de1945 | |||
| 22895ce682 | |||
| 3829623984 | |||
| 3f3bc525bc | |||
| ece0930319 | |||
| 94585789dc | |||
| 7ea6d8a35d | |||
| 9dd40e4610 | |||
| 8cdca8d012 | |||
| 7bcd3ebaee | |||
| d4a8a3ec37 | |||
| f460cceeba | |||
| f3f58c7f52 | |||
| f2a9b73b83 | |||
| ffd4e6d577 | |||
| 5287eff507 | |||
| f6004e07c1 | |||
| b2cd60395f | |||
| 183f975166 | |||
| 8ace01b6f1 | |||
| 4b213d63bd | |||
| be58d5517a | |||
| 602403eb59 | |||
| 5f1776829b | |||
| 4a22c5d673 | |||
| 3943919709 | |||
| 8174f2d973 | |||
| 22d16d54c6 | |||
| cd34f0ba81 | |||
| 648c6c5a27 | |||
| 2b687a7269 | |||
| e8c2fe6e35 | |||
| 88dfabc633 | |||
| a35bf249a0 | |||
| cf47b45262 | |||
| eed3ed5513 | |||
| 0346450a51 | |||
| fc53f08e37 | |||
| bc4e5b9b4e | |||
| c3a445534b | |||
| 70877bfad4 | |||
| 55376d32e5 | |||
| 8835502074 | |||
| ba9b06adf3 | |||
| e92d9bfaae | |||
| d802e88148 |
+10
-6
@@ -1,14 +1,18 @@
|
|||||||
/art export-ignore
|
/docker export-ignore
|
||||||
/docs export-ignore
|
/docs export-ignore
|
||||||
/tests export-ignore
|
/tests export-ignore
|
||||||
/scripts export-ignore
|
/scripts export-ignore
|
||||||
/.github export-ignore
|
/.github export-ignore
|
||||||
/.php-cs-fixer.dist.php export-ignore
|
|
||||||
.editorconfig export-ignore
|
.editorconfig export-ignore
|
||||||
.gitattributes export-ignore
|
.gitattributes export-ignore
|
||||||
.gitignore export-ignore
|
.gitignore export-ignore
|
||||||
phpstan.neon export-ignore
|
/phpstan.neon export-ignore
|
||||||
/phpunit.xml export-ignore
|
/phpunit.xml export-ignore
|
||||||
CHANGELOG.md export-ignore
|
/CHANGELOG.md export-ignore
|
||||||
CONTRIBUTING.md export-ignore
|
/CONTRIBUTING.md export-ignore
|
||||||
README.md export-ignore
|
/docker-compose.yml export-ignore
|
||||||
|
/Makefile export-ignore
|
||||||
|
/rector.php export-ignore
|
||||||
|
/README.md export-ignore
|
||||||
|
/RELEASE.md export-ignore
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: [nunomaduro,owenvoke,olivernybroe,octoper,lukeraymonddowning]
|
github: [nunomaduro]
|
||||||
patreon: nunomaduro
|
custom: https://www.paypal.com/paypalme/enunomaduro
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report an Issue or Bug with the Pest
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
We're sorry to hear you have a problem. Can you help us solve it by providing the following details.
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What Happened
|
||||||
|
description: What did you expect to happen?
|
||||||
|
placeholder: When I use expect()->toBeTrue() in my tests, I get an error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: how-to-reproduce
|
||||||
|
attributes:
|
||||||
|
label: How to Reproduce
|
||||||
|
description: How did this occur, please add any config values used and provide a set of reliable steps if possible.
|
||||||
|
placeholder: Install a fresh Laravel app, add Pest, add a test that uses expect()->toBeTrue()
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: repository-sample
|
||||||
|
attributes:
|
||||||
|
label: Sample Repository
|
||||||
|
description: If possible, please provide a sample repository that reproduces the issue.
|
||||||
|
placeholder: https://github.com.br/your-username/your-repository
|
||||||
|
- type: input
|
||||||
|
id: pest-version
|
||||||
|
attributes:
|
||||||
|
label: Pest Version
|
||||||
|
description: What version of our Package are you running? Please be as specific as possible
|
||||||
|
placeholder: 2.14.1
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: php-version
|
||||||
|
attributes:
|
||||||
|
label: PHP Version
|
||||||
|
description: What version of PHP are you running? Please be as specific as possible
|
||||||
|
placeholder: 8.1.20
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: operating-systems
|
||||||
|
attributes:
|
||||||
|
label: Operation System
|
||||||
|
description: On which operating systems does the problem occur? You can select more than one.
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: notes
|
||||||
|
attributes:
|
||||||
|
label: Notes
|
||||||
|
description: Use this field to provide any other notes that you feel might be relevant to the issue.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
| Q | A
|
|
||||||
| ------------- | ---
|
|
||||||
| Bug fix? | yes/no
|
|
||||||
| New feature? | yes/no
|
|
||||||
| Fixed tickets | #... <!-- #-prefixed issue number(s), if any -->
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
- Replace this comment by a description of what your PR is solving.
|
- Fill in the form below correctly. This will help the Pest team to understand the PR and also work on it.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### What:
|
||||||
|
|
||||||
|
- [ ] Bug Fix
|
||||||
|
- [ ] New Feature
|
||||||
|
|
||||||
|
### Description:
|
||||||
|
|
||||||
|
<!-- describe what your PR is solving -->
|
||||||
|
|
||||||
|
### Related:
|
||||||
|
|
||||||
|
<!-- link to the issue(s) your PR is solving. If it doesn't exist, remove the "Related" section. -->
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security vulnerability in Pest, please report it privately using one of the following channels:
|
||||||
|
|
||||||
|
1. **GitHub Private Vulnerability Reporting** (preferred) — go to the repository's **Security** tab and click **"Report a vulnerability"**. This creates a private advisory visible only to maintainers and provides a structured workflow for triage, fix coordination, and CVE assignment.
|
||||||
|
|
||||||
|
2. **Email** — send the details to Nuno Maduro at **enunomaduro@gmail.com**.
|
||||||
|
|
||||||
|
All security vulnerabilities will be promptly addressed.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
cooldown:
|
||||||
|
default-days: 5
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
name: Changelog
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ 1.x ]
|
|
||||||
paths:
|
|
||||||
- CHANGELOG.md
|
|
||||||
- .github/workflows/changelog.yml
|
|
||||||
pull_request:
|
|
||||||
branches: [ 1.x ]
|
|
||||||
paths:
|
|
||||||
- CHANGELOG.md
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
if: github.repository == 'pestphp/pest'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Checkout website repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CHANGELOG_KEY }}
|
|
||||||
repository: pestphp/docs
|
|
||||||
path: pestphp-docs
|
|
||||||
ref: master
|
|
||||||
|
|
||||||
- name: Read CHANGELOG.md
|
|
||||||
id: package
|
|
||||||
uses: juliangruber/read-file-action@v1
|
|
||||||
with:
|
|
||||||
path: ./CHANGELOG.md
|
|
||||||
|
|
||||||
- name: Add file headers
|
|
||||||
uses: DamianReeves/write-file-action@v1.0
|
|
||||||
with:
|
|
||||||
path: ./CHANGELOG.md
|
|
||||||
contents: |
|
|
||||||
---
|
|
||||||
title: Changelog
|
|
||||||
description: Changelog
|
|
||||||
---
|
|
||||||
${{ steps.package.outputs.content }}
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
Next section: [Upgrade Guide →](/docs/upgrade-guide)
|
|
||||||
write-mode: overwrite
|
|
||||||
|
|
||||||
- name: Copy CHANGELOG to website repository
|
|
||||||
run: cp CHANGELOG.md pestphp-docs/changelog.md
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
uses: peter-evans/create-pull-request@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CHANGELOG_KEY }}
|
|
||||||
commit-message: Update changelog.md
|
|
||||||
committer: GitHub Action <noreply@github.com>
|
|
||||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
|
||||||
title: 'Update changelog.md'
|
|
||||||
path: ./pestphp-docs
|
|
||||||
@@ -1,51 +1,70 @@
|
|||||||
name: Static Analysis
|
name: Static Analysis
|
||||||
|
|
||||||
on: ['push', 'pull_request']
|
on:
|
||||||
|
push:
|
||||||
|
branches: [5.x]
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 9 * * *'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: static-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cs:
|
static:
|
||||||
runs-on: ubuntu-latest
|
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
|
||||||
|
name: Static Tests
|
||||||
name: Code Style
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: 8.0
|
|
||||||
tools: composer:v2
|
|
||||||
coverage: none
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: composer update --no-interaction --no-progress
|
|
||||||
|
|
||||||
- name: Run PHP-CS-Fixer
|
|
||||||
run: vendor/bin/php-cs-fixer fix -v --allow-risky=yes --dry-run
|
|
||||||
|
|
||||||
phpstan:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
dependency-version: [prefer-lowest, prefer-stable]
|
dependency-version: [prefer-stable]
|
||||||
|
|
||||||
name: PHPStan ${{ matrix.dependency-version }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
|
||||||
with:
|
with:
|
||||||
php-version: 8.0
|
php-version: 8.4
|
||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
coverage: none
|
coverage: none
|
||||||
|
extensions: sockets
|
||||||
|
|
||||||
|
- name: Get Composer cache directory
|
||||||
|
id: composer-cache
|
||||||
|
shell: bash
|
||||||
|
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: static-php-8.4-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
static-php-8.4-${{ matrix.dependency-version }}-composer-
|
||||||
|
static-php-8.4-composer-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: composer update --prefer-stable --no-interaction --no-progress
|
env:
|
||||||
|
COMPOSER_ROOT_VERSION: 5.x-dev
|
||||||
|
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
|
||||||
|
|
||||||
- name: Run PHPStan
|
- name: Profanity Check
|
||||||
run: vendor/bin/phpstan analyse --no-progress
|
run: composer test:profanity
|
||||||
|
|
||||||
|
- name: Type Check
|
||||||
|
run: composer test:type:check
|
||||||
|
|
||||||
|
- name: Type Coverage
|
||||||
|
run: composer test:type:coverage
|
||||||
|
|
||||||
|
- name: Style
|
||||||
|
run: composer test:lint
|
||||||
|
|||||||
+52
-24
@@ -1,38 +1,60 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on: ['push', 'pull_request']
|
on:
|
||||||
|
push:
|
||||||
|
branches: [5.x]
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 9 * * *'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
tests:
|
||||||
runs-on: ${{ matrix.os }}
|
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
||||||
php: ['7.3', '7.4', '8.0', '8.1', '8.2']
|
|
||||||
dependency-version: [prefer-lowest, prefer-stable]
|
|
||||||
parallel: ['', '--parallel']
|
|
||||||
exclude:
|
|
||||||
- php: 8.1
|
|
||||||
os: macos-latest
|
|
||||||
- php: 8.1
|
|
||||||
os: windows-latest
|
|
||||||
- php: 8.2
|
|
||||||
os: macos-latest
|
|
||||||
- php: 8.2
|
|
||||||
os: windows-latest
|
|
||||||
|
|
||||||
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} - ${{ matrix.parallel }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest] # windows-latest
|
||||||
|
symfony: ['8.0']
|
||||||
|
php: ['8.4', '8.5']
|
||||||
|
dependency_version: [prefer-stable]
|
||||||
|
|
||||||
|
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php }}
|
php-version: ${{ matrix.php }}
|
||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
coverage: none
|
coverage: none
|
||||||
|
extensions: sockets
|
||||||
|
|
||||||
|
- name: Get Composer cache directory
|
||||||
|
id: composer-cache
|
||||||
|
shell: bash
|
||||||
|
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-
|
||||||
|
${{ matrix.os }}-php-${{ matrix.php }}-composer-
|
||||||
|
|
||||||
- name: Setup Problem Matches
|
- name: Setup Problem Matches
|
||||||
run: |
|
run: |
|
||||||
@@ -40,10 +62,16 @@ 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
|
shell: bash
|
||||||
|
env:
|
||||||
|
COMPOSER_ROOT_VERSION: 5.x-dev
|
||||||
|
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
|
||||||
|
|
||||||
- name: Unit Tests
|
- name: Unit Tests
|
||||||
run: php bin/pest --colors=always --exclude-group=integration ${{ matrix.parallel }}
|
run: composer test:unit
|
||||||
|
|
||||||
|
- name: Parallel Tests
|
||||||
|
run: composer test:parallel
|
||||||
|
|
||||||
- name: Integration Tests
|
- name: Integration Tests
|
||||||
run: php bin/pest --colors=always --group=integration
|
run: composer test:integration
|
||||||
|
|||||||
+5
-1
@@ -1,12 +1,16 @@
|
|||||||
.idea/*
|
.idea/*
|
||||||
.idea/codeStyleSettings.xml
|
.idea/codeStyleSettings.xml
|
||||||
composer.lock
|
.temp/*
|
||||||
|
/composer.lock
|
||||||
/vendor/
|
/vendor/
|
||||||
coverage.xml
|
coverage.xml
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
|
.phpunit.cache
|
||||||
/.php-cs-fixer.php
|
/.php-cs-fixer.php
|
||||||
.php-cs-fixer.cache
|
.php-cs-fixer.cache
|
||||||
.temp/coverage.php
|
.temp/coverage.php
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.STREAM.md
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
$finder = PhpCsFixer\Finder::create()
|
|
||||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'tests')
|
|
||||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'bin')
|
|
||||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'stubs')
|
|
||||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'src')
|
|
||||||
->append(['.php-cs-fixer.dist.php']);
|
|
||||||
|
|
||||||
$rules = [
|
|
||||||
'@Symfony' => true,
|
|
||||||
'phpdoc_no_empty_return' => false,
|
|
||||||
'array_syntax' => ['syntax' => 'short'],
|
|
||||||
'yoda_style' => false,
|
|
||||||
'binary_operator_spaces' => [
|
|
||||||
'operators' => [
|
|
||||||
'=>' => 'align',
|
|
||||||
'=' => 'align',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'concat_space' => ['spacing' => 'one'],
|
|
||||||
'not_operator_with_space' => false,
|
|
||||||
'global_namespace_import' => [
|
|
||||||
'import_classes' => true,
|
|
||||||
'import_functions' => true,
|
|
||||||
'import_constants' => true,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$rules['increment_style'] = ['style' => 'post'];
|
|
||||||
|
|
||||||
return (new PhpCsFixer\Config())
|
|
||||||
->setUsingCache(true)
|
|
||||||
->setRules($rules)
|
|
||||||
->setFinder($finder);
|
|
||||||
-366
@@ -1,366 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
||||||
|
|
||||||
## [v1.22.2 (2022-11-09)](https://github.com/pestphp/pest/compare/v1.22.1...v1.22.2)
|
|
||||||
### Fixed
|
|
||||||
- Fixes storing lazy datasets into internal array ([#602](https://github.com/pestphp/pest/pull/602))
|
|
||||||
|
|
||||||
## [v1.22.1 (2022-08-29)](https://github.com/pestphp/pest/compare/v1.22.0...v1.22.1)
|
|
||||||
### Fixed
|
|
||||||
- Initial PHP 8.2 support ([d050ef8](https://github.com/pestphp/pest/commit/d050ef8d54ffe8dfe015adeb1188aae2cca33c33))
|
|
||||||
|
|
||||||
## [v1.22.0 (2022-08-28)](https://github.com/pestphp/pest/compare/v1.21.3...v1.22.0)
|
|
||||||
### Added
|
|
||||||
- Initial PHP 8.2 support ([#563](https://github.com/pestphp/pest/pull/563))
|
|
||||||
|
|
||||||
## [v1.21.3 (2022-05-12)](https://github.com/pestphp/pest/compare/v1.21.2...v1.21.3)
|
|
||||||
### Fixed
|
|
||||||
- Debug of high order tests ([c1b32b9](https://github.com/pestphp/pest/commit/c1b32b9ffb5134803c490592454b11b8c05ea27d))
|
|
||||||
|
|
||||||
## [v1.21.2 (2022-03-05)](https://github.com/pestphp/pest/compare/v1.21.1...v1.21.2)
|
|
||||||
### Fixed
|
|
||||||
- `toThrow` expectation when exception does not exist ([#487](https://github.com/pestphp/pest/pull/487))
|
|
||||||
|
|
||||||
## [v1.21.1 (2021-11-25)](https://github.com/pestphp/pest/compare/v1.21.0...v1.21.1)
|
|
||||||
### Fixed
|
|
||||||
- sequence callables causing problems ([#442](https://github.com/pestphp/pest/pull/442))
|
|
||||||
|
|
||||||
## [v1.21.0 (2021-11-17)](https://github.com/pestphp/pest/compare/v1.20.0...v1.21.0)
|
|
||||||
### Added
|
|
||||||
- warn about xdebug modes ([1e011c](https://github.com/pestphp/pest/commit/1e011c7b4074d08f5dabab1f927d45383c85d210))
|
|
||||||
|
|
||||||
## [v1.20.0 (2021-09-25)](https://github.com/pestphp/pest/compare/v1.19.0...v1.20.0)
|
|
||||||
### Added
|
|
||||||
- `throwsIf` test call ([#371](https://github.com/pestphp/pest/pull/371))
|
|
||||||
- `--ci` CLI option to ignore development options like `->local()` ([#405](https://github.com/pestphp/pest/pull/405))
|
|
||||||
- `when` conditional expectation ([#406](https://github.com/pestphp/pest/pull/406))
|
|
||||||
- `unless` conditional expectation ([b43a598](https://github.com/pestphp/pest/commit/b43a59868d5b790a28cbb29c6110c9f068b0b812))
|
|
||||||
- `match` conditional expectation ([#407](https://github.com/pestphp/pest/pull/407))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- `sequence` with more expectations than iterable elements ([#399](https://github.com/pestphp/pest/pull/399))
|
|
||||||
|
|
||||||
## [v1.19.0 (2021-09-20)](https://github.com/pestphp/pest/compare/v1.18.0...v1.19.0)
|
|
||||||
### Added
|
|
||||||
- PHP 8.1 support ([e6c7d68](https://github.com/pestphp/pest/commit/e6c7d68defaec8efe01e71e15dd8d8c45b0cf60f))
|
|
||||||
- `toHaveProperties` expectation ([#391](https://github.com/pestphp/pest/pull/391))
|
|
||||||
|
|
||||||
## [v1.18.0 (2021-08-30)](https://github.com/pestphp/pest/compare/v1.17.0...v1.18.0)
|
|
||||||
### Added
|
|
||||||
- `toHaveLength` expectation ([#386](https://github.com/pestphp/pest/pull/386))
|
|
||||||
- `nunomaduro/collision:^6.0` support ([4ae482c](https://github.com/pestphp/pest/commit/4ae482c7073fb77782b8a4b5738ef1fcea0f82ab))
|
|
||||||
|
|
||||||
## [v1.17.0 (2021-08-26)](https://github.com/pestphp/pest/compare/v1.16.0...v1.17.0)
|
|
||||||
### Added
|
|
||||||
- `toThrow` expectation ([#361](https://github.com/pestphp/pest/pull/361))
|
|
||||||
|
|
||||||
## [v1.16.0 (2021-08-19)](https://github.com/pestphp/pest/compare/v1.15.0...v1.16.0)
|
|
||||||
### Added
|
|
||||||
- Support for new parallel options ([#369](https://github.com/pestphp/pest/pull/369))
|
|
||||||
|
|
||||||
## [v1.15.0 (2021-08-04)](https://github.com/pestphp/pest/compare/v1.14.0...v1.15.0)
|
|
||||||
### Added
|
|
||||||
- `toBeTruthy` and `toBeFalsy` ([#367](https://github.com/pestphp/pest/pull/367))
|
|
||||||
|
|
||||||
## [v1.14.0 (2021-08-03)](https://github.com/pestphp/pest/compare/v1.13.0...v1.14.0)
|
|
||||||
### Added
|
|
||||||
- A new bound closure that allows you to access the test case in Datasets ([#364](https://github.com/pestphp/pest/pull/364))
|
|
||||||
|
|
||||||
## [v1.13.0 (2021-08-02)](https://github.com/pestphp/pest/compare/v1.12.0...v1.13.0)
|
|
||||||
### Added
|
|
||||||
- A cleaner output when running the Pest runner in PhpStorm ([#350](https://github.com/pestphp/pest/pull/350))
|
|
||||||
- `toBeIn` expectation ([#363](https://github.com/pestphp/pest/pull/363))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- `skip` with false condition marking test as skipped ([22b822c](https://github.com/pestphp/pest/commit/22b822ce87a3d19d84960fa5c93eb286820b525d))
|
|
||||||
|
|
||||||
## [v1.12.0 (2021-07-26)](https://github.com/pestphp/pest/compare/v1.11.0...v1.12.0)
|
|
||||||
### Added
|
|
||||||
- `--force` option to override tests in `pest:test` artisan command ([#353](https://github.com/pestphp/pest/pull/353))
|
|
||||||
- Support for PHPUnit `^9.3.7` ([ca9d783](https://github.com/pestphp/pest/commit/ca9d783cf942a2caabc85ff7a728c7f28350c67a))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- `beforeAll` and `afterAll` behind called multiple times per test ([#357](https://github.com/pestphp/pest/pull/357))
|
|
||||||
|
|
||||||
## [v1.11.0 (2021-07-21)](https://github.com/pestphp/pest/compare/v1.10.0...v1.11.0)
|
|
||||||
### Added
|
|
||||||
- Support for interacting with datasets in higher order tests ([#352](https://github.com/pestphp/pest/pull/352))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- The unit test stub now uses the expectation API ([#348](https://github.com/pestphp/pest/pull/348))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- PhpStorm will no longer show 0 assertions in the output ([#349](https://github.com/pestphp/pest/pull/349))
|
|
||||||
|
|
||||||
## [v1.10.0 (2021-07-12)](https://github.com/pestphp/pest/compare/v1.9.1...v1.10.0)
|
|
||||||
### Added
|
|
||||||
- The ability to use higher order expectations inside higher order tests ([#341](https://github.com/pestphp/pest/pull/341))
|
|
||||||
|
|
||||||
## [v1.9.1 (2021-07-11)](https://github.com/pestphp/pest/compare/v1.9.0...v1.9.1)
|
|
||||||
### Fixed
|
|
||||||
- Callable `expect` values in higher order tests failing if the value was an existing method name ([#334](https://github.com/pestphp/pest/pull/344))
|
|
||||||
|
|
||||||
## [v1.9.0 (2021-07-09)](https://github.com/pestphp/pest/compare/v1.8.0...v1.9.0)
|
|
||||||
### Changed
|
|
||||||
- You may now pass just an exception message when using the `throws` method ([#339](https://github.com/pestphp/pest/pull/339))
|
|
||||||
|
|
||||||
## [v1.8.0 (2021-07-08)](https://github.com/pestphp/pest/compare/v1.7.1...v1.8.0)
|
|
||||||
### Added
|
|
||||||
- A new `tap` and test case aware `expect` methods for higher order tests ([#331](https://github.com/pestphp/pest/pull/331))
|
|
||||||
- Access to test case methods and properties when using `skip` ([#338](https://github.com/pestphp/pest/pull/338))
|
|
||||||
|
|
||||||
## [v1.7.1 (2021-06-24)](https://github.com/pestphp/pest/compare/v1.7.0...v1.7.1)
|
|
||||||
### Fixed
|
|
||||||
- The `and` method not being usable in Higher Order expectations ([#330](https://github.com/pestphp/pest/pull/330))
|
|
||||||
|
|
||||||
## [v1.7.0 (2021-06-19)](https://github.com/pestphp/pest/compare/v1.6.0...v1.7.0)
|
|
||||||
### Added
|
|
||||||
- Support for non-callable values in the sequence method, which will be passed as `toEqual` ([#323](https://github.com/pestphp/pest/pull/323))
|
|
||||||
- Support for nested Higher Order Expectations ([#324](https://github.com/pestphp/pest/pull/324))
|
|
||||||
|
|
||||||
## [v1.6.0 (2021-06-18)](https://github.com/pestphp/pest/compare/v1.5.0...v1.6.0)
|
|
||||||
### Added
|
|
||||||
- Adds a new `json` expectation method to improve testing with JSON strings ([#325](https://github.com/pestphp/pest/pull/325))
|
|
||||||
- Adds dot notation support to the `toHaveKey` and `toHaveKeys` expectations ([#322](https://github.com/pestphp/pest/pull/322))
|
|
||||||
|
|
||||||
## [v1.5.0 (2021-06-15)](https://github.com/pestphp/pest/compare/v1.4.0...v1.5.0)
|
|
||||||
### Changed
|
|
||||||
- Moves plugins from the `require` section to the core itself ([#317](https://github.com/pestphp/pest/pull/317)), ([#318](https://github.com/pestphp/pest/pull/318)), ([#320](https://github.com/pestphp/pest/pull/320))
|
|
||||||
|
|
||||||
## [v1.4.0 (2021-06-10)](https://github.com/pestphp/pest/compare/v1.3.2...v1.4.0)
|
|
||||||
### Added
|
|
||||||
- Support for multiple datasets (Matrix) on the `with` method ([#303](https://github.com/pestphp/pest/pull/303))
|
|
||||||
- Support for incompleted tests ([49de462](https://github.com/pestphp/pest/commit/49de462250cf9f65f09e13eaf6dcc0e06865b930))
|
|
||||||
|
|
||||||
## [v1.3.2 (2021-06-07)](https://github.com/pestphp/pest/compare/v1.3.1...v1.3.2)
|
|
||||||
### Fixed
|
|
||||||
- Test cases with the @ symbol in the directory fail ([#308](https://github.com/pestphp/pest/pull/308))
|
|
||||||
|
|
||||||
## [v1.3.1 (2021-06-06)](https://github.com/pestphp/pest/compare/v1.3.0...v1.3.1)
|
|
||||||
### Added
|
|
||||||
- Added for PHPUnit 9.5.5 ([#310](https://github.com/pestphp/pest/pull/310))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Lock minimum Pest plugin versions ([#306](https://github.com/pestphp/pest/pull/306))
|
|
||||||
|
|
||||||
## [v1.3.0 (2021-05-23)](https://github.com/pestphp/pest/compare/v1.2.1...v1.3.0)
|
|
||||||
### Added
|
|
||||||
- Named datasets no longer show the arguments ([#302](https://github.com/pestphp/pest/pull/302))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Wraps global functions within `function_exists` ([#300](https://github.com/pestphp/pest/pull/300))
|
|
||||||
|
|
||||||
## [v1.2.1 (2021-05-14)](https://github.com/pestphp/pest/compare/v1.2.0...v1.2.1)
|
|
||||||
### Fixed
|
|
||||||
- Laravel commands failing with new `--test-directory` option ([#297](https://github.com/pestphp/pest/pull/297))
|
|
||||||
|
|
||||||
## [v1.2.0 (2021-05-13)](https://github.com/pestphp/pest/compare/v1.1.0...v1.2.0)
|
|
||||||
### Added
|
|
||||||
- Adds JUnit / Infection support ([#291](https://github.com/pestphp/pest/pull/291))
|
|
||||||
- `--test-directory` command line option ([#283](https://github.com/pestphp/pest/pull/283))
|
|
||||||
|
|
||||||
## [v1.1.0 (2021-05-02)](https://github.com/pestphp/pest/compare/v1.0.5...v1.1.0)
|
|
||||||
### Added
|
|
||||||
- Possibility of "hooks" being added using the "uses" function ([#282](https://github.com/pestphp/pest/pull/282))
|
|
||||||
|
|
||||||
## [v1.0.5 (2021-03-31)](https://github.com/pestphp/pest/compare/v1.0.4...v1.0.5)
|
|
||||||
### Added
|
|
||||||
- Add `--browse` option to `pest:dusk` command ([#280](https://github.com/pestphp/pest/pull/280))
|
|
||||||
- Support for PHPUnit 9.5.4 ([#284](https://github.com/pestphp/pest/pull/284))
|
|
||||||
|
|
||||||
## [v1.0.4 (2021-03-17)](https://github.com/pestphp/pest/compare/v1.0.3...v1.0.4)
|
|
||||||
### Added
|
|
||||||
- Support for PHPUnit 9.5.3 ([#278](https://github.com/pestphp/pest/pull/278))
|
|
||||||
|
|
||||||
## [v1.0.3 (2021-03-13)](https://github.com/pestphp/pest/compare/v1.0.2...v1.0.3)
|
|
||||||
### Added
|
|
||||||
- Support for test extensions ([#269](https://github.com/pestphp/pest/pull/269))
|
|
||||||
|
|
||||||
## [v1.0.2 (2021-02-04)](https://github.com/pestphp/pest/compare/v1.0.1...v1.0.2)
|
|
||||||
### Added
|
|
||||||
- Support for PHPUnit 9.5.2 ([#267](https://github.com/pestphp/pest/pull/267))
|
|
||||||
|
|
||||||
## [v1.0.1 (2021-01-18)](https://github.com/pestphp/pest/compare/v1.0.0...v1.0.1)
|
|
||||||
### Added
|
|
||||||
- Support for PHPUnit 9.5.1 ([#261](https://github.com/pestphp/pest/pull/261))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Fix `TestCase@expect` PHPDoc tag ([#251](https://github.com/pestphp/pest/pull/251))
|
|
||||||
|
|
||||||
## [v1.0.0 (2021-01-03)](https://github.com/pestphp/pest/compare/v0.3.19...v1.0.0)
|
|
||||||
### Added
|
|
||||||
- `pest:test --dusk` option ([#245](https://github.com/pestphp/pest/pull/245))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Stable version
|
|
||||||
- Updates init structure ([#240](https://github.com/pestphp/pest/pull/240))
|
|
||||||
|
|
||||||
## [v0.3.19 (2020-12-27)](https://github.com/pestphp/pest/compare/v0.3.18...v0.3.19)
|
|
||||||
### Fixed
|
|
||||||
- Fix binary path in `pest:dusk` command ([#239](https://github.com/pestphp/pest/pull/239))
|
|
||||||
|
|
||||||
## [v0.3.18 (2020-12-26)](https://github.com/pestphp/pest/compare/v0.3.17...v0.3.18)
|
|
||||||
### Added
|
|
||||||
- `toBeJson()` expectation ([plugin-expectations#2](https://github.com/pestphp/pest-plugin-expectations/pull/2))
|
|
||||||
|
|
||||||
## [v0.3.17 (2020-12-20)](https://github.com/pestphp/pest/compare/v0.3.16...v0.3.17)
|
|
||||||
### Fixed
|
|
||||||
- Class inheritance with `depends()` ([#236](https://github.com/pestphp/pest/pull/236))
|
|
||||||
|
|
||||||
## [v0.3.16 (2020-12-13)](https://github.com/pestphp/pest/compare/v0.3.15...v0.3.16)
|
|
||||||
### Changed
|
|
||||||
- Moves expectation API for external plugin ([5d7f262](https://github.com/pestphp/pest/commit/5d7f262f4ab280a660a85900f402eebb23abfda8))
|
|
||||||
|
|
||||||
## [v0.3.15 (2020-12-04)](https://github.com/pestphp/pest/compare/v0.3.14...v0.3.15)
|
|
||||||
### Added
|
|
||||||
- Support for PHPUnit 9.5.0 ([#234](https://github.com/pestphp/pest/pull/234))
|
|
||||||
- Support for extending expectation API ([#232](https://github.com/pestphp/pest/pull/232))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Static analysis while using string as key for datasets ([#233](https://github.com/pestphp/pest/pull/233))
|
|
||||||
|
|
||||||
## [v0.3.14 (2020-11-28)](https://github.com/pestphp/pest/compare/v0.3.13...v0.3.14)
|
|
||||||
### Added
|
|
||||||
- `pest:dusk` command ([#223](https://github.com/pestphp/pest/pull/223))
|
|
||||||
- Better feedback on errors in `toMatchArray` and `toMatchObject` ([#231](https://github.com/pestphp/pest/pull/231))
|
|
||||||
|
|
||||||
## [v0.3.13 (2020-11-23)](https://github.com/pestphp/pest/compare/v0.3.12...v0.3.13)
|
|
||||||
### Added
|
|
||||||
- `toMatchArray` expectation ([7bea51f](https://github.com/pestphp/pest/commit/7bea51fe09dd2eca7093e4c34cf2dab2e8d39fa5), [3fd24d9](https://github.com/pestphp/pest/commit/3fd24d96d3145dcebdb0aab40aa8b76faa8b6979))
|
|
||||||
- Add Pest options to `--help` output ([#217](https://github.com/pestphp/pest/pull/217))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Resolve issue with name resolution in `depends()` ([#216](https://github.com/pestphp/pest/pull/216))
|
|
||||||
|
|
||||||
## [v0.3.12 (2020-11-11)](https://github.com/pestphp/pest/compare/v0.3.11...v0.3.12)
|
|
||||||
### Added
|
|
||||||
- Add support for PHPUnit 9.4.3 ([#219](https://github.com/pestphp/pest/pull/219))
|
|
||||||
|
|
||||||
## [v0.3.11 (2020-11-09)](https://github.com/pestphp/pest/compare/v0.3.10...v0.3.11)
|
|
||||||
### Changed
|
|
||||||
- Improved the exception output for the TeamCity printer (usage with phpstorm plugin) ([#215](https://github.com/pestphp/pest/pull/215))
|
|
||||||
|
|
||||||
## [v0.3.10 (2020-11-01)](https://github.com/pestphp/pest/compare/v0.3.9...v0.3.10)
|
|
||||||
### Added
|
|
||||||
- Add support for PHPUnit 9.4.2 ([d177ab5](https://github.com/pestphp/pest/commit/d177ab5ec2030c5bb8e418d10834c370c94c433d))
|
|
||||||
|
|
||||||
## [v0.3.9 (2020-10-13)](https://github.com/pestphp/pest/compare/v0.3.8...v0.3.9)
|
|
||||||
### Added
|
|
||||||
- Add support for named datasets in description output ([#134](https://github.com/pestphp/pest/pull/134))
|
|
||||||
- Add Pest version to `--help` output ([#203](https://github.com/pestphp/pest/pull/203))
|
|
||||||
- Add support for PHPUnit 9.4.1 ([#207](https://github.com/pestphp/pest/pull/207))
|
|
||||||
|
|
||||||
## [v0.3.8 (2020-10-03)](https://github.com/pestphp/pest/compare/v0.3.7...v0.3.8)
|
|
||||||
### Added
|
|
||||||
- Add support for PHPUnit 9.4.0 ([#199](https://github.com/pestphp/pest/pull/199))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Fix chained higher order assertions returning void ([#196](https://github.com/pestphp/pest/pull/196))
|
|
||||||
|
|
||||||
## [v0.3.7 (2020-09-30)](https://github.com/pestphp/pest/compare/v0.3.6...v0.3.7)
|
|
||||||
### Added
|
|
||||||
- Add support for PHPUnit 9.3.11 ([#193](https://github.com/pestphp/pest/pull/193))
|
|
||||||
|
|
||||||
## [v0.3.6 (2020-09-21)](https://github.com/pestphp/pest/compare/v0.3.5...v0.3.6)
|
|
||||||
### Added
|
|
||||||
- `toMatch` expectation ([#191](https://github.com/pestphp/pest/pull/191))
|
|
||||||
- `toMatchConstraint` expectation ([#190](https://github.com/pestphp/pest/pull/190))
|
|
||||||
|
|
||||||
## [v0.3.5 (2020-09-16)](https://github.com/pestphp/pest/compare/v0.3.4...v0.3.5)
|
|
||||||
### Added
|
|
||||||
- `toStartWith` and `toEndWith` expectations ([#187](https://github.com/pestphp/pest/pull/187))
|
|
||||||
|
|
||||||
## [v0.3.4 (2020-09-15)](https://github.com/pestphp/pest/compare/v0.3.3...v0.3.4)
|
|
||||||
### Added
|
|
||||||
- `toMatchObject` expectation ([4e184b2](https://github.com/pestphp/pest/commit/4e184b2f906c318a5e9cd38fe693cdab5c48d8a2))
|
|
||||||
|
|
||||||
## [v0.3.3 (2020-09-13)](https://github.com/pestphp/pest/compare/v0.3.2...v0.3.3)
|
|
||||||
### Added
|
|
||||||
- `toHaveKeys` expectation ([204f343](https://github.com/pestphp/pest/commit/204f343831adc17bb3734553c24fac92d02f27c7))
|
|
||||||
|
|
||||||
## [v0.3.2 (2020-09-12)](https://github.com/pestphp/pest/compare/v0.3.1...v0.3.2)
|
|
||||||
### Added
|
|
||||||
- Support to PHPUnit 9.3.9, and 9.3.10 ([1318bf9](https://github.com/pestphp/pest/commit/97f98569bc86e8b87f8cde963fe7b4bf5399623b))
|
|
||||||
|
|
||||||
## [v0.3.1 (2020-08-29)](https://github.com/pestphp/pest/compare/v0.3.0...v0.3.1)
|
|
||||||
### Added
|
|
||||||
- Support to PHPUnit 9.3.8 ([#174](https://github.com/pestphp/pest/pull/174))
|
|
||||||
|
|
||||||
## [v0.3.0 (2020-08-27)](https://github.com/pestphp/pest/compare/v0.2.3...v0.3.0)
|
|
||||||
### Added
|
|
||||||
- Expectation API (TODO)
|
|
||||||
- PHPUnit 9.3 and PHP 8 support ([#128](https://github.com/pestphp/pest/pull/128))
|
|
||||||
- Forwards `$this` calls to globals ([#169](https://github.com/pestphp/pest/pull/169))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- don't decorate output if --colors=never is set ([36b879f](https://github.com/pestphp/pest/commit/36b879f97d7b187c87a94eb60af5b7d3b7253d56))
|
|
||||||
|
|
||||||
## [v0.2.3 (2020-07-01)](https://github.com/pestphp/pest/compare/v0.2.2...v0.2.3)
|
|
||||||
### Added
|
|
||||||
- `--init` and `pest:install` artisan command output changes ([#118](https://github.com/pestphp/pest/pull/118), [db7c4b1](https://github.com/pestphp/pest/commit/db7c4b174f0974969450dda71dcd649ef0c073a3))
|
|
||||||
- `--version` option to view the current version of Pest ([9ea51ca](https://github.com/pestphp/pest/commit/9ea51caf3f74569debb1e465992e9ea916cb80fe))
|
|
||||||
|
|
||||||
## [v0.2.2 (2020-06-21)](https://github.com/pestphp/pest/compare/v0.2.1...v0.2.2)
|
|
||||||
### Added
|
|
||||||
- `depends` phpunit feature ([#103](https://github.com/pestphp/pest/pull/103))
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
- datasets name conflict ([#101](https://github.com/pestphp/pest/pull/101))
|
|
||||||
|
|
||||||
## [v0.2.1 (2020-06-17)](https://github.com/pestphp/pest/compare/v0.2.0...v0.2.1)
|
|
||||||
### Fixes
|
|
||||||
- Multiple `uses` in the same path override previous `uses` ([#97](https://github.com/pestphp/pest/pull/97))
|
|
||||||
|
|
||||||
## [v0.2.0 (2020-06-14)](https://github.com/pestphp/pest/compare/v0.1.5...v0.2.0)
|
|
||||||
### Adds
|
|
||||||
- `--init` option to install Pest on a new blank project ([70b3c7e](https://github.com/pestphp/pest/commit/70b3c7ea1ddb031f3bbfaabdc28d56270608ebbd))
|
|
||||||
- pending higher orders tests aka tests without description ([aa1917c](https://github.com/pestphp/pest/commit/aa1917c28d9b69c2bd1d51f986c4f61318ee7e16))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- `--verbose` and `--colors` options not being used by printers ([#51](https://github.com/pestphp/pest/pull/51))
|
|
||||||
- missing support on windows ([#61](https://github.com/pestphp/pest/pull/61))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- `helpers.php` stub provides now namespaced functions
|
|
||||||
- functions provided by plugins are now namespaced functions:
|
|
||||||
|
|
||||||
```php
|
|
||||||
use function Pest\Faker\faker;
|
|
||||||
|
|
||||||
it('foo', function () {
|
|
||||||
$name = faker()->name;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## [v0.1.5 (2020-05-24)](https://github.com/pestphp/pest/compare/v0.1.4...v0.1.5)
|
|
||||||
### Fixed
|
|
||||||
- Missing default decorated output on coverage ([88d2391](https://github.com/pestphp/pest/commit/88d2391d2e6fe9c9416462734b9b523cb418f469))
|
|
||||||
|
|
||||||
## [v0.1.4 (2020-05-24)](https://github.com/pestphp/pest/compare/v0.1.3...v0.1.4)
|
|
||||||
### Added
|
|
||||||
- Support to Lumen on artisan commands ([#18](https://github.com/pestphp/pest/pull/18))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Mockery tests without assertions being considered risky ([415f571](https://github.com/pestphp/pest/commit/415f5719101b30c11d87f74810a71686ef2786c6))
|
|
||||||
|
|
||||||
## [v0.1.3 (2020-05-21)](https://github.com/pestphp/pest/compare/v0.1.2...v0.1.3)
|
|
||||||
### Added
|
|
||||||
- `Plugin::uses()` method for making traits globally available ([6c4be01](https://github.com/pestphp/pest/commit/6c4be0190e9493702a976b996bbbf5150cc6bb53))
|
|
||||||
|
|
||||||
## [v0.1.2 (2020-05-15)](https://github.com/pestphp/pest/compare/v0.1.1...v0.1.2)
|
|
||||||
### Added
|
|
||||||
- Support to custom helpers ([#7](https://github.com/pestphp/pest/pull/7))
|
|
||||||
|
|
||||||
## [v0.1.1 (2020-05-14)](https://github.com/pestphp/pest/compare/v0.1.0...v0.1.1)
|
|
||||||
### Added
|
|
||||||
- `test` function without any arguments returns the current test case ([6fc55be](https://github.com/pestphp/pest/commit/6fc55becc8aecff685a958617015be1a4c118b01))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- "No coverage driver error" now returns proper error on Laravel ([28d8822](https://github.com/pestphp/pest/commit/28d8822de01f4fa92c62d8b8e019313f382b97e9))
|
|
||||||
|
|
||||||
## [v0.1.0 (2020-05-09)](https://github.com/pestphp/pest/commit/de2929077b344a099ef9c2ddc2f48abce14e248f)
|
|
||||||
### Added
|
|
||||||
- First version
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
**Do not edit this file.** Agents must never add, remove, or reword anything here. If a change seems needed, say so and let a human do it.
|
||||||
|
|
||||||
|
## Ask before testing
|
||||||
|
|
||||||
|
When asked to review code or build a feature, do not run the test suite and do not write new tests. Make the change, report it, then ask the user whether tests should be added — describing the tests you have in mind — and wait for the user to confirm.
|
||||||
|
|
||||||
|
Two reasons this matters here: the suite takes minutes, and `tests/.snapshots/success.txt` plus the tally in `tests/Visual/Parallel.php` encode the whole suite's result, so a single added test breaks both.
|
||||||
|
|
||||||
|
Once the user confirms:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer test:unit # fast, excludes the visual group
|
||||||
|
composer test:integration # visual and snapshot tests
|
||||||
|
composer test # everything CI runs, in CI's order
|
||||||
|
composer update:snapshots # only when a test was added or removed
|
||||||
|
```
|
||||||
|
|
||||||
|
## TIA scenario tests
|
||||||
|
|
||||||
|
`tests/Features/Tia/*` scaffold a throwaway git project, run a real `pest` subprocess against it, and diff the TIA graph it wrote. They exist because TIA's contract is about what a run *writes* — replay, branch keys, and the COMPLETE / RESULTS-ONLY / HARD-SUPPRESSED tiers are invisible to ordinary assertions, and every case used to be measured by hand against a playground app.
|
||||||
|
|
||||||
|
Add one whenever a change touches branch resolution, replay, filtered mode, or the write tiers. How:
|
||||||
|
|
||||||
|
- `Project::make('master')` scaffolds; `seed('master')` writes a graph and sentinels every cached result (`time=9.999`, `assertions=42`) so any rewrite shows up.
|
||||||
|
- `$project->pest('--tia', …)` runs it; `$project->delta()` compares against that snapshot. `writtenCount()` is the discriminator — `0` means "replayed", not "wrote the same values". `mutateGraph()` bends one entry; overlays in `tests/Fixtures/Tia/overlays/<name>/` supply a different `tests/Pest.php`.
|
||||||
|
- Keep expectations driver-independent: a cold recording run needs pcov/xdebug and behaves differently without one. Seed a graph instead of recording one.
|
||||||
|
|
||||||
|
Run them by file (a directory argument finds nothing) or by `--filter`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/pest tests/Features/Tia/PartialRunWriteTier.php
|
||||||
|
```
|
||||||
+20
-1
@@ -42,7 +42,7 @@ composer test
|
|||||||
|
|
||||||
Check types:
|
Check types:
|
||||||
```bash
|
```bash
|
||||||
composer test:types
|
composer test:type:check
|
||||||
```
|
```
|
||||||
|
|
||||||
Unit tests:
|
Unit tests:
|
||||||
@@ -54,3 +54,22 @@ Integration tests:
|
|||||||
```bash
|
```bash
|
||||||
composer test:integration
|
composer test:integration
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Simplified setup using Docker
|
||||||
|
|
||||||
|
If you have Docker installed, you can quickly get all dependencies for Pest in place using
|
||||||
|
our Docker files. Assuming you have the repository cloned, you may run the following
|
||||||
|
commands:
|
||||||
|
|
||||||
|
1. `make build` to build the Docker image
|
||||||
|
2. `make install` to install Composer dependencies
|
||||||
|
3. `make test` to run the project tests and analysis tools
|
||||||
|
|
||||||
|
If you want to check things work against a specific version of PHP, you may include
|
||||||
|
the `PHP` build argument when building the image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build ARGS="--build-arg PHP=8.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
The default PHP version will always be the lowest version of PHP supported by Pest.
|
||||||
|
|||||||
@@ -1,36 +1,46 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://raw.githubusercontent.com/pestphp/art/master/readme.png" width="600" alt="PEST">
|
<img src="https://raw.githubusercontent.com/pestphp/art/master/v5/social.png" width="600" alt="PEST">
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (master)" src="https://img.shields.io/github/workflow/status/pestphp/pest/Tests/master"></a>
|
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (5.x)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=5.x&label=Tests%205.x"></a>
|
||||||
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
|
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
|
||||||
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a>
|
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a>
|
||||||
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
|
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
|
||||||
|
<a href="https://whyphp.dev"><img src="https://img.shields.io/badge/Why_PHP-in_2026-7A86E8?style=flat-square&labelColor=18181b" alt="Why PHP in 2026"></a>
|
||||||
|
<a href="https://youtube.com/@nunomaduro?sub_confirmation=1"><img alt="YouTube Channel Subscribers" src="https://img.shields.io/youtube/channel/subscribers/UCO_hYZF2gb_CyG5sA7ArlGg?style=flat&label=youtube&color=brightgreen"></a>
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
------
|
------
|
||||||
**Pest** is an elegant PHP Testing Framework with a focus on simplicity. It was carefully crafted to bring the joy of testing to PHP.
|
|
||||||
|
|
||||||
- Explore the docs: **[pestphp.com »](https://pestphp.com)**
|
> Pest v5 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest5-now-available)**.
|
||||||
- Follow us on Twitter: **[@pestphp »](https://twitter.com/pestphp)**
|
|
||||||
- Join us on the Discord Server: **[discord.gg/bMAJv82 »](https://discord.gg/bMAJv82)**
|
|
||||||
|
|
||||||
## Pest Sponsors
|
**Pest** is an elegant testing framework for PHP developers and AI agents.
|
||||||
|
|
||||||
We would like to extend our thanks to the following sponsors for funding Pest development. If you are interested in becoming a sponsor, please visit the Nuno Maduro's [Sponsors page](https://github.com/sponsors/nunomaduro).
|
- Explore our docs at **[pestphp.com »](https://pestphp.com)**
|
||||||
|
- Follow the creator Nuno Maduro:
|
||||||
|
- YouTube: **[youtube.com/@nunomaduro](https://youtube.com/@nunomaduro)** — Videos every week
|
||||||
|
- Twitch: **[twitch.tv/nunomaduro](https://twitch.tv/nunomaduro)** — Live coding on Mondays, Wednesdays, and Fridays at 9PM UTC
|
||||||
|
- Twitter / X: **[x.com/enunomaduro](https://x.com/enunomaduro)**
|
||||||
|
- LinkedIn: **[linkedin.com/in/nunomaduro](https://www.linkedin.com/in/nunomaduro)**
|
||||||
|
- Instagram: **[instagram.com/enunomaduro](https://www.instagram.com/enunomaduro)**
|
||||||
|
- Tiktok: **[tiktok.com/@enunomaduro](https://www.tiktok.com/@enunomaduro)**
|
||||||
|
|
||||||
### Platinum Sponsors
|
## Sponsors
|
||||||
|
|
||||||
- **[Spatie](https://spatie.be)**
|
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)**.
|
||||||
- **[Worksome](https://www.worksome.com/)**
|
|
||||||
|
|
||||||
### Premium Sponsors
|
- **[PhpStorm](https://jb.gg/nuno)**
|
||||||
|
- **[CodeRabbit](https://coderabbit.link/nunomaduro)**
|
||||||
- [Akaunting](https://akaunting.com)
|
- **[SerpApi](https://serpapi.com/?ref=nunomaduro)**
|
||||||
- [Auth0](https://auth0.com)
|
- **[Bento](https://bentonow.com/?ref=nunomaduro)**
|
||||||
- [Codecourse](https://codecourse.com/)
|
- **[CMS Max](https://cmsmax.com/?ref=nunomaduro)**
|
||||||
- [Fathom Analytics](https://usefathom.com/)
|
- **[Redberry](https://redberry.international/laravel-development/?utm_source=pest&utm_medium=banner&utm_campaign=pest_sponsorship)**
|
||||||
- [Meema](https://meema.io)
|
- **[Typesense](https://typesense.org/?ref=nunomaduro)**
|
||||||
- [Scout APM](https://scoutapm.com)
|
- **[Pixel](https://wearepixel.com.au/?ref=nunomaduro)**
|
||||||
|
- [TestMu AI](https://www.testmuai.com/?utm_medium=sponsor&utm_source=pest)
|
||||||
|
- [Zapiet](https://zapiet.com/?ref=nunomaduro)
|
||||||
|
- [LoadForge](https://loadforge.com/?ref=nunomaduro)
|
||||||
|
- [Route4Me](https://route4me.com/?ref=nunomaduro)
|
||||||
|
- [Nerdify](https://getnerdify.com/?ref=nunomaduro)
|
||||||
|
|
||||||
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)**.
|
||||||
|
|||||||
+5
-3
@@ -2,11 +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:
|
||||||
|
|
||||||
- Clear your local repository with: `git add . && git reset --hard && git checkout 1.x`
|
> **For Pest v4 you should use the `4.x` branch instead.**
|
||||||
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...1.x](https://github.com/pestphp/pest/compare/{latest_version}...1.x) and update the [changelog](CHANGELOG.md) file with the main changes for this release
|
|
||||||
|
- Clear your local repository with: `git add . && git reset --hard && git checkout 5.x`
|
||||||
|
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...5.x](https://github.com/pestphp/pest/compare/{latest_version}...5.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`
|
||||||
|
|||||||
@@ -1,21 +1,138 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php declare(strict_types=1);
|
<?php
|
||||||
|
|
||||||
use NunoMaduro\Collision\Provider;
|
declare(strict_types=1);
|
||||||
use Pest\Actions\ValidatesEnvironment;
|
|
||||||
|
use Pest\Contracts\Restarter;
|
||||||
|
use Pest\Kernel;
|
||||||
|
use Pest\Panic;
|
||||||
use Pest\Support\Container;
|
use Pest\Support\Container;
|
||||||
|
use Pest\TestCaseFilters\GitDirtyTestCaseFilter;
|
||||||
|
use Pest\TestCaseMethodFilters\AssigneeTestCaseFilter;
|
||||||
|
use Pest\TestCaseMethodFilters\IssueTestCaseFilter;
|
||||||
|
use Pest\TestCaseMethodFilters\NotesTestCaseFilter;
|
||||||
|
use Pest\TestCaseMethodFilters\PrTestCaseFilter;
|
||||||
|
use Pest\TestCaseMethodFilters\FlakyTestCaseFilter;
|
||||||
|
use Pest\TestCaseMethodFilters\TodoTestCaseFilter;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Input\ArgvInput;
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
|
|
||||||
(static function () {
|
(static function () {
|
||||||
|
// Ensures Collision's Printer is registered.
|
||||||
|
$_SERVER['COLLISION_PRINTER'] = 'DefaultPrinter';
|
||||||
|
|
||||||
|
$arguments = $originalArguments = $_SERVER['argv'];
|
||||||
|
|
||||||
|
$dirty = false;
|
||||||
|
$todo = false;
|
||||||
|
$flaky = false;
|
||||||
|
$notes = false;
|
||||||
|
|
||||||
|
foreach ($arguments as $key => $value) {
|
||||||
|
|
||||||
|
if ($value === '--compact') {
|
||||||
|
$_SERVER['COLLISION_PRINTER_COMPACT'] = 'true';
|
||||||
|
unset($arguments[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === '--profile') {
|
||||||
|
$_SERVER['COLLISION_PRINTER_PROFILE'] = 'true';
|
||||||
|
unset($arguments[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--test-directory=')) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
} elseif ($value === '--test-directory') {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
|
||||||
|
if (isset($arguments[$key + 1])) {
|
||||||
|
unset($arguments[$key + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === '--dirty') {
|
||||||
|
$dirty = true;
|
||||||
|
unset($arguments[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($value, ['--todo', '--todos'], true)) {
|
||||||
|
$todo = true;
|
||||||
|
unset($arguments[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === '--flaky') {
|
||||||
|
$flaky = true;
|
||||||
|
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')) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
$arguments[] = '--no-output';
|
||||||
|
unset($_SERVER['COLLISION_PRINTER']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Used when Pest is required using composer.
|
// Used when Pest is required using composer.
|
||||||
$vendorPath = dirname(__DIR__, 4) . '/vendor/autoload.php';
|
$vendorPath = dirname(__DIR__, 4).'/vendor/autoload.php';
|
||||||
|
|
||||||
// Used when Pest maintainers are running Pest tests.
|
// Used when Pest maintainers are running Pest tests.
|
||||||
$localPath = dirname(__DIR__) . '/vendor/autoload.php';
|
$localPath = dirname(__DIR__).'/vendor/autoload.php';
|
||||||
|
|
||||||
if (file_exists($vendorPath)) {
|
if (file_exists($vendorPath)) {
|
||||||
include_once $vendorPath;
|
include_once $vendorPath;
|
||||||
@@ -25,39 +142,74 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
$autoloadPath = $localPath;
|
$autoloadPath = $localPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
(new Provider())->register();
|
|
||||||
|
|
||||||
// Get $rootPath based on $autoloadPath
|
// Get $rootPath based on $autoloadPath
|
||||||
$rootPath = dirname($autoloadPath, 2);
|
$rootPath = dirname($autoloadPath, 2);
|
||||||
$argv = new ArgvInput();
|
|
||||||
|
|
||||||
$testSuite = TestSuite::getInstance($rootPath, $argv->getParameterOption('--test-directory', 'tests'));
|
$input = new ArgvInput;
|
||||||
|
|
||||||
|
$testSuite = TestSuite::getInstance(
|
||||||
|
$rootPath,
|
||||||
|
$input->getParameterOption('--test-directory', 'tests'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($dirty) {
|
||||||
|
$testSuite->tests->addTestCaseFilter(new GitDirtyTestCaseFilter($rootPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($todo) {
|
||||||
|
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($flaky) {
|
||||||
|
$testSuite->tests->addTestCaseMethodFilter(new FlakyTestCaseFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = $argv->getParameterOption('--colors', 'always') !== 'never';
|
|
||||||
$output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated);
|
$output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated);
|
||||||
|
|
||||||
$container = Container::getInstance();
|
try {
|
||||||
$container->add(TestSuite::class, $testSuite);
|
$kernel = Kernel::boot($testSuite, $input, $output);
|
||||||
$container->add(OutputInterface::class, $output);
|
|
||||||
|
|
||||||
ValidatesEnvironment::in($testSuite);
|
$container = Container::getInstance();
|
||||||
|
|
||||||
$args = $_SERVER['argv'];
|
foreach (Kernel::RESTARTERS as $restarterClass) {
|
||||||
|
$restarter = $container->get($restarterClass);
|
||||||
|
assert($restarter instanceof Restarter);
|
||||||
|
|
||||||
// Let's remove any arguments that PHPUnit does not understand
|
$restarter->maybeRestart($rootPath, $originalArguments);
|
||||||
if ($argv->hasParameterOption('--test-directory')) {
|
|
||||||
foreach ($args as $key => $value) {
|
|
||||||
if (strpos($value, '--test-directory') !== false) {
|
|
||||||
unset($args[$key]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$result = $kernel->handle($originalArguments, $arguments);
|
||||||
|
|
||||||
|
$kernel->terminate();
|
||||||
|
} catch (Throwable|Error $e) {
|
||||||
|
Panic::with($e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($runInParallel = $argv->hasParameterOption(['--parallel', '-p'])) && !class_exists(\Pest\Parallel\Command::class)) {
|
exit($result);
|
||||||
$output->writeln("Parallel support requires the Pest Parallel plugin. Run <fg=yellow;options=bold>`composer require --dev pestphp/pest-plugin-parallel`</> first.");
|
|
||||||
exit(Command::FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
$command = $runInParallel ? \Pest\Parallel\Command::class : \Pest\Console\Command::class;
|
|
||||||
exit($container->get($command)->run($args));
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { readdir, readFile } from 'node:fs/promises'
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
|
import { createRequire } from 'node:module'
|
||||||
|
import { resolve, relative, extname, sep, join } from 'node:path'
|
||||||
|
import { pathToFileURL } from 'node:url'
|
||||||
|
|
||||||
|
const PAGE_EXTENSIONS = new Set([
|
||||||
|
'.vue', '.svelte',
|
||||||
|
'.tsx', '.jsx',
|
||||||
|
'.ts', '.js',
|
||||||
|
'.mts', '.cts', '.mjs', '.cjs',
|
||||||
|
])
|
||||||
|
const ASSET_EXT_RE = /\.(css|scss|sass|less|styl|stylus|svg|png|jpe?g|gif|webp|avif|ico|bmp|woff2?|ttf|eot|otf|md|mdx|txt|html|mp4|webm|mp3|wav|ogg|m4a|pdf|wasm|glsl|frag|vert)$/i
|
||||||
|
const PROJECT_ROOT = resolve(process.argv[2] ?? process.cwd())
|
||||||
|
const PAGE_DIR_CANDIDATES = [
|
||||||
|
'resources/js/Pages',
|
||||||
|
'resources/js/pages',
|
||||||
|
'assets/js/Pages',
|
||||||
|
'assets/js/pages',
|
||||||
|
'assets/Pages',
|
||||||
|
'assets/pages',
|
||||||
|
]
|
||||||
|
|
||||||
|
async function loadRolldown() {
|
||||||
|
const projectRequire = createRequire(join(PROJECT_ROOT, 'package.json'))
|
||||||
|
let path = null
|
||||||
|
|
||||||
|
try { path = projectRequire.resolve('rolldown') } catch {}
|
||||||
|
|
||||||
|
if (path === null) {
|
||||||
|
// rolldown-vite installs (vite@npm:rolldown-vite) ship rolldown as a
|
||||||
|
// dependency of the vite package rather than a top-level install.
|
||||||
|
try {
|
||||||
|
const viteRequire = createRequire(projectRequire.resolve('vite/package.json'))
|
||||||
|
path = viteRequire.resolve('rolldown')
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === null) return null
|
||||||
|
|
||||||
|
return await import(pathToFileURL(path).href)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripJsonComments(raw) {
|
||||||
|
let out = ''
|
||||||
|
let inString = false
|
||||||
|
let quote = ''
|
||||||
|
let inLine = false
|
||||||
|
let inBlock = false
|
||||||
|
|
||||||
|
for (let i = 0; i < raw.length; i++) {
|
||||||
|
const c = raw[i]
|
||||||
|
const n = raw[i + 1]
|
||||||
|
|
||||||
|
if (inLine) {
|
||||||
|
if (c === '\n') { inLine = false; out += c }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (inBlock) {
|
||||||
|
if (c === '*' && n === '/') { inBlock = false; i++ }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (inString) {
|
||||||
|
out += c
|
||||||
|
if (c === '\\') { out += n ?? ''; i++; continue }
|
||||||
|
if (c === quote) inString = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (c === '"' || c === "'") { inString = true; quote = c; out += c; continue }
|
||||||
|
if (c === '/' && n === '/') { inLine = true; i++; continue }
|
||||||
|
if (c === '/' && n === '*') { inBlock = true; i++; continue }
|
||||||
|
if (c === '}' || c === ']') out = out.replace(/,\s*$/, '')
|
||||||
|
out += c
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJsonWithComments(path) {
|
||||||
|
const raw = await readFile(path, 'utf8')
|
||||||
|
return JSON.parse(stripJsonComments(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAliasFromTsconfig(projectRoot = PROJECT_ROOT) {
|
||||||
|
const alias = {}
|
||||||
|
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
||||||
|
const p = join(projectRoot, name)
|
||||||
|
if (!existsSync(p)) continue
|
||||||
|
let cfg
|
||||||
|
try { cfg = await readJsonWithComments(p) } catch { continue }
|
||||||
|
const baseUrl = resolve(projectRoot, cfg?.compilerOptions?.baseUrl ?? '.')
|
||||||
|
const paths = cfg?.compilerOptions?.paths ?? {}
|
||||||
|
for (const [key, targets] of Object.entries(paths)) {
|
||||||
|
if (!key.endsWith('/*')) continue
|
||||||
|
const t0 = Array.isArray(targets) ? targets[0] : null
|
||||||
|
if (typeof t0 !== 'string' || !t0.endsWith('/*')) continue
|
||||||
|
const aliasKey = key.slice(0, -2)
|
||||||
|
if (alias[aliasKey] !== undefined) continue
|
||||||
|
alias[aliasKey] = resolve(baseUrl, t0.slice(0, -2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return alias
|
||||||
|
}
|
||||||
|
|
||||||
|
const VITE_CONFIG_FILES = [
|
||||||
|
'vite.config.ts',
|
||||||
|
'vite.config.js',
|
||||||
|
'vite.config.mjs',
|
||||||
|
'vite.config.cjs',
|
||||||
|
'vite.config.mts',
|
||||||
|
'vite.config.cts',
|
||||||
|
]
|
||||||
|
|
||||||
|
function resolveAliasTarget(projectRoot, target) {
|
||||||
|
if (target.startsWith('/')) {
|
||||||
|
// Vite resolves a leading slash against the project root, unless the
|
||||||
|
// config author used a genuinely absolute path.
|
||||||
|
return existsSync(target) ? target : resolve(projectRoot, '.' + target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(projectRoot, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function usesLaravelVitePlugin(projectRoot) {
|
||||||
|
const p = join(projectRoot, 'package.json')
|
||||||
|
if (!existsSync(p)) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pkg = JSON.parse(await readFile(p, 'utf8'))
|
||||||
|
return Boolean(pkg.dependencies?.['laravel-vite-plugin'] ?? pkg.devDependencies?.['laravel-vite-plugin'])
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAliasFromViteConfig(projectRoot = PROJECT_ROOT) {
|
||||||
|
const alias = {}
|
||||||
|
let source = null
|
||||||
|
|
||||||
|
for (const name of VITE_CONFIG_FILES) {
|
||||||
|
const p = join(projectRoot, name)
|
||||||
|
if (!existsSync(p)) continue
|
||||||
|
try { source = await readFile(p, 'utf8') } catch { continue }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source !== null) {
|
||||||
|
// The config is executable code we cannot import safely, so extract alias
|
||||||
|
// entries textually: a quoted `@…`/`~…` key, then the last quoted path in
|
||||||
|
// the value expression — covers `'@': '/resources/js'` as well as
|
||||||
|
// `'@': path.resolve(__dirname, 'resources/js')`. The value stops at a
|
||||||
|
// top-level comma; call arguments are kept via the paren group.
|
||||||
|
for (const m of source.matchAll(/['"`]([@~][\w./-]*)['"`]\s*:\s*((?:\([^)\n]*\)|[^,\n])*)/g)) {
|
||||||
|
const key = m[1]
|
||||||
|
if (alias[key] !== undefined) continue
|
||||||
|
|
||||||
|
const paths = [...m[2].matchAll(/['"`]([^'"`]+)['"`]/g)].map((p) => p[1])
|
||||||
|
const target = paths.length > 0 ? paths[paths.length - 1] : null
|
||||||
|
if (!target || target.includes('*')) continue
|
||||||
|
|
||||||
|
alias[key] = resolveAliasTarget(projectRoot, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// laravel-vite-plugin registers '@' → resources/js by default.
|
||||||
|
if (alias['@'] === undefined && (await usesLaravelVitePlugin(projectRoot))) {
|
||||||
|
alias['@'] = resolve(projectRoot, 'resources/js')
|
||||||
|
}
|
||||||
|
|
||||||
|
return alias
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listPageFiles(pagesDir) {
|
||||||
|
if (!existsSync(pagesDir)) return []
|
||||||
|
|
||||||
|
const out = []
|
||||||
|
const walk = async (dir) => {
|
||||||
|
let entries
|
||||||
|
try { entries = await readdir(dir, { withFileTypes: true }) } catch { return }
|
||||||
|
for (const entry of entries) {
|
||||||
|
const full = resolve(dir, entry.name)
|
||||||
|
if (entry.isDirectory()) { await walk(full); continue }
|
||||||
|
if (PAGE_EXTENSIONS.has(extname(entry.name))) out.push(full)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await walk(pagesDir)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async function discoverPagesDir() {
|
||||||
|
const override = process.env.TIA_VITE_PAGES_DIR
|
||||||
|
if (override && override.length > 0) {
|
||||||
|
return resolve(PROJECT_ROOT, override.replace(/\\/g, '/'))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rel of PAGE_DIR_CANDIDATES) {
|
||||||
|
const abs = resolve(PROJECT_ROOT, rel)
|
||||||
|
if (!existsSync(abs)) continue
|
||||||
|
const files = await listPageFiles(abs)
|
||||||
|
if (files.length > 0) return abs
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentNameFor(pageAbs, pagesDir) {
|
||||||
|
const rel = relative(pagesDir, pageAbs).split(sep).join('/')
|
||||||
|
const ext = extname(rel)
|
||||||
|
return rel.slice(0, rel.length - ext.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLocalSpecifier(source, aliasKeys) {
|
||||||
|
if (source.startsWith('.') || source.startsWith('/')) return true
|
||||||
|
for (const key of aliasKeys) {
|
||||||
|
if (source === key || source.startsWith(key + '/')) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pagesDir = await discoverPagesDir()
|
||||||
|
|
||||||
|
if (pagesDir === null) {
|
||||||
|
process.stdout.write('{}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = await listPageFiles(pagesDir)
|
||||||
|
|
||||||
|
if (pages.length === 0) {
|
||||||
|
process.stdout.write('{}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const loaded = await loadRolldown()
|
||||||
|
|
||||||
|
if (loaded === null) {
|
||||||
|
process.stdout.write('{}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rolldown } = loaded
|
||||||
|
// The vite config is what the dev server actually resolves with — let it win.
|
||||||
|
const alias = { ...(await loadAliasFromTsconfig()), ...(await loadAliasFromViteConfig()) }
|
||||||
|
const aliasKeys = Object.keys(alias)
|
||||||
|
|
||||||
|
const graph = new Map()
|
||||||
|
|
||||||
|
const collector = {
|
||||||
|
name: 'pest-tia-collector',
|
||||||
|
moduleParsed(info) {
|
||||||
|
const id = info.id
|
||||||
|
if (!id || id.startsWith('\0')) return
|
||||||
|
const deps = new Set()
|
||||||
|
for (const i of info.importedIds) if (i && !i.startsWith('\0')) deps.add(i)
|
||||||
|
for (const i of info.dynamicallyImportedIds) if (i && !i.startsWith('\0')) deps.add(i)
|
||||||
|
graph.set(id, deps)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const externalBare = {
|
||||||
|
name: 'pest-tia-external-bare',
|
||||||
|
resolveId(source) {
|
||||||
|
if (!source) return null
|
||||||
|
if (isLocalSpecifier(source, aliasKeys)) return null
|
||||||
|
return { id: source, external: true }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetStub = {
|
||||||
|
name: 'pest-tia-asset-stub',
|
||||||
|
load(id) {
|
||||||
|
if (!id) return null
|
||||||
|
if (ASSET_EXT_RE.test(id)) {
|
||||||
|
return { code: 'export default null', moduleSideEffects: false }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = Object.create(null)
|
||||||
|
for (let i = 0; i < pages.length; i++) input[`p${i}`] = pages[i]
|
||||||
|
|
||||||
|
const bundle = await rolldown({
|
||||||
|
input,
|
||||||
|
cwd: PROJECT_ROOT,
|
||||||
|
resolve: {
|
||||||
|
alias,
|
||||||
|
extensions: ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.mjs', '.cjs', '.json', '.vue', '.svelte'],
|
||||||
|
},
|
||||||
|
transform: { jsx: 'preserve' },
|
||||||
|
treeshake: false,
|
||||||
|
plugins: [externalBare, assetStub, collector],
|
||||||
|
logLevel: 'silent',
|
||||||
|
onLog: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bundle.generate({ format: 'esm' })
|
||||||
|
} finally {
|
||||||
|
await bundle.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const reverse = new Map()
|
||||||
|
const transitiveCache = new Map()
|
||||||
|
|
||||||
|
const computeTransitive = (id, stack) => {
|
||||||
|
const cached = transitiveCache.get(id)
|
||||||
|
if (cached) return cached
|
||||||
|
if (stack.has(id)) return null
|
||||||
|
|
||||||
|
stack.add(id)
|
||||||
|
const acc = new Set()
|
||||||
|
// A set computed while skipping an in-stack (cyclic) dependency is missing
|
||||||
|
// that dependency's subtree and must not be memoized — the ancestor call
|
||||||
|
// completes it for the current traversal, but a cached copy would leak the
|
||||||
|
// incomplete set into other pages' traversals.
|
||||||
|
let complete = true
|
||||||
|
const deps = graph.get(id)
|
||||||
|
if (deps) {
|
||||||
|
for (const dep of deps) {
|
||||||
|
if (!dep || dep.startsWith('\0')) continue
|
||||||
|
if (dep.startsWith(PROJECT_ROOT)) {
|
||||||
|
const rel = relative(PROJECT_ROOT, dep).split(sep).join('/')
|
||||||
|
acc.add(rel)
|
||||||
|
}
|
||||||
|
if (stack.has(dep)) {
|
||||||
|
complete = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const child = computeTransitive(dep, stack)
|
||||||
|
if (child) for (const r of child) acc.add(r)
|
||||||
|
if (!transitiveCache.has(dep)) complete = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stack.delete(id)
|
||||||
|
if (complete) transitiveCache.set(id, acc)
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const page of pages) {
|
||||||
|
const pageComponent = componentNameFor(page, pagesDir)
|
||||||
|
const reachable = computeTransitive(page, new Set())
|
||||||
|
if (!reachable) continue
|
||||||
|
for (const rel of reachable) {
|
||||||
|
const bucket = reverse.get(rel) ?? new Set()
|
||||||
|
bucket.add(pageComponent)
|
||||||
|
reverse.set(rel, bucket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = Object.create(null)
|
||||||
|
const keys = [...reverse.keys()].sort()
|
||||||
|
for (const key of keys) {
|
||||||
|
payload[key] = [...reverse.get(key)].sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(JSON.stringify(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
const invokedDirectly = process.argv[1] !== undefined
|
||||||
|
&& import.meta.url === pathToFileURL(process.argv[1]).href
|
||||||
|
|
||||||
|
if (invokedDirectly) {
|
||||||
|
try {
|
||||||
|
await main()
|
||||||
|
} catch (err) {
|
||||||
|
process.stderr.write(String(err?.stack ?? err ?? 'unknown error'))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use ParaTest\WrapperRunner\ApplicationForWrapperWorker;
|
||||||
|
use ParaTest\WrapperRunner\WrapperWorker;
|
||||||
|
use Pest\Kernel;
|
||||||
|
use Pest\Plugins\Actions\CallsHandleArguments;
|
||||||
|
use Pest\Support\Container;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
$bootPest = (static function (): void {
|
||||||
|
$workerArgv = new ArgvInput;
|
||||||
|
|
||||||
|
$rootPath = dirname(PHPUNIT_COMPOSER_INSTALL, 2);
|
||||||
|
$testSuite = TestSuite::getInstance($rootPath, $workerArgv->getParameterOption(
|
||||||
|
'--test-directory',
|
||||||
|
'tests'
|
||||||
|
));
|
||||||
|
|
||||||
|
$input = new ArgvInput;
|
||||||
|
|
||||||
|
$output = new ConsoleOutput(OutputInterface::VERBOSITY_NORMAL, true);
|
||||||
|
|
||||||
|
Kernel::boot($testSuite, $input, $output);
|
||||||
|
});
|
||||||
|
|
||||||
|
(static function () use ($bootPest): void {
|
||||||
|
$getopt = getopt('', [
|
||||||
|
'status-file:',
|
||||||
|
'progress-file:',
|
||||||
|
'unexpected-output-file:',
|
||||||
|
'test-result-file:',
|
||||||
|
'result-cache-file:',
|
||||||
|
'teamcity-file:',
|
||||||
|
'testdox-file:',
|
||||||
|
'testdox-color',
|
||||||
|
'testdox-columns:',
|
||||||
|
'testdox-summary',
|
||||||
|
'phpunit-argv:',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$composerAutoloadFiles = [
|
||||||
|
dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'autoload.php',
|
||||||
|
dirname(__DIR__, 2).DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php',
|
||||||
|
dirname(__DIR__).DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($composerAutoloadFiles as $file) {
|
||||||
|
|
||||||
|
if (file_exists($file)) {
|
||||||
|
require_once $file;
|
||||||
|
define('PHPUNIT_COMPOSER_INSTALL', $file);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$container = Container::getInstance();
|
||||||
|
$rootPath = dirname(PHPUNIT_COMPOSER_INSTALL, 2);
|
||||||
|
|
||||||
|
foreach (Kernel::RESTARTERS as $restarterClass) {
|
||||||
|
$restarter = $container->get($restarterClass);
|
||||||
|
|
||||||
|
$restarter->maybeRestart($rootPath, $_SERVER['argv']);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(isset($getopt['status-file']) && is_string($getopt['status-file']));
|
||||||
|
$statusFile = fopen($getopt['status-file'], 'wb');
|
||||||
|
assert(is_resource($statusFile));
|
||||||
|
|
||||||
|
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['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['testdox-file']) || is_string($getopt['testdox-file']));
|
||||||
|
|
||||||
|
assert(isset($getopt['phpunit-argv']) && is_string($getopt['phpunit-argv']));
|
||||||
|
$phpunitArgv = unserialize($getopt['phpunit-argv'], ['allowed_classes' => false]);
|
||||||
|
assert(is_array($phpunitArgv));
|
||||||
|
|
||||||
|
$bootPest();
|
||||||
|
|
||||||
|
$phpunitArgv = CallsHandleArguments::execute($phpunitArgv);
|
||||||
|
|
||||||
|
$application = new ApplicationForWrapperWorker(
|
||||||
|
$phpunitArgv,
|
||||||
|
$getopt['progress-file'],
|
||||||
|
$getopt['unexpected-output-file'],
|
||||||
|
$getopt['test-result-file'],
|
||||||
|
$getopt['result-cache-file'] ?? null,
|
||||||
|
$getopt['teamcity-file'] ?? null,
|
||||||
|
$getopt['testdox-file'] ?? null,
|
||||||
|
isset($getopt['testdox-color']),
|
||||||
|
(int) ($getopt['testdox-columns'] ?? null),
|
||||||
|
);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (feof(STDIN)) {
|
||||||
|
$application->end();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$testPath = fgets(STDIN);
|
||||||
|
if ($testPath === false || $testPath === WrapperWorker::COMMAND_EXIT) {
|
||||||
|
$application->end();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exitCode = $application->runTest(realpath(trim($testPath)));
|
||||||
|
|
||||||
|
fwrite($statusFile, (string) $exitCode);
|
||||||
|
fflush($statusFile);
|
||||||
|
}
|
||||||
|
})();
|
||||||
+70
-25
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pestphp/pest",
|
"name": "pestphp/pest",
|
||||||
"description": "An elegant PHP Testing Framework.",
|
"description": "The elegant PHP Testing Framework.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"php",
|
"php",
|
||||||
"framework",
|
"framework",
|
||||||
@@ -17,10 +17,22 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.3 || ^8.0",
|
"php": "^8.4",
|
||||||
"nunomaduro/collision": "^5.11.0|^6.3.0",
|
"brianium/paratest": "^7.24.0",
|
||||||
"pestphp/pest-plugin": "^1.1.0",
|
"nunomaduro/collision": "^8.9.5",
|
||||||
"phpunit/phpunit": "^9.5.26"
|
"nunomaduro/termwind": "^2.4.0",
|
||||||
|
"pestphp/pest-plugin": "^5.0.0",
|
||||||
|
"pestphp/pest-plugin-arch": "^5.0.0",
|
||||||
|
"pestphp/pest-plugin-mutate": "^5.0.1",
|
||||||
|
"pestphp/pest-plugin-profanity": "^5.0.0",
|
||||||
|
"phpunit/phpunit": "^13.3.0",
|
||||||
|
"symfony/process": "^8.1.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"filp/whoops": "<2.18.3",
|
||||||
|
"phpunit/phpunit": ">13.3.0",
|
||||||
|
"sebastian/exporter": "<7.0.0",
|
||||||
|
"webmozart/assert": "<1.11.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -33,24 +45,33 @@
|
|||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
"Tests\\Fixtures\\Covers\\": "tests/Fixtures/Covers",
|
||||||
|
"Tests\\Fixtures\\Inheritance\\": "tests/Fixtures/Inheritance",
|
||||||
|
"Tests\\Fixtures\\Arch\\": "tests/Fixtures/Arch",
|
||||||
|
"Tests\\Fixtures\\Tia\\": "tests/Fixtures/Tia",
|
||||||
"Tests\\": "tests/PHPUnit/"
|
"Tests\\": "tests/PHPUnit/"
|
||||||
},
|
},
|
||||||
|
"classmap": [
|
||||||
|
"tests/Fixtures/Arch/ToBeCasedCorrectly/IncorrectCasing/incorrectCasing.php"
|
||||||
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"tests/Autoload.php"
|
"tests/Autoload.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"illuminate/console": "^8.83.26",
|
"pestphp/pest-dev-tools": "^5.0.0",
|
||||||
"illuminate/support": "^8.83.26",
|
"pestphp/pest-plugin-browser": "^5.0.0",
|
||||||
"laravel/dusk": "^6.25.2",
|
"pestphp/pest-plugin-phpstan": "^5.0.2",
|
||||||
"pestphp/pest-dev-tools": "^1.0.0",
|
"pestphp/pest-plugin-rector": "^5.0.3",
|
||||||
"pestphp/pest-plugin-parallel": "^1.2"
|
"pestphp/pest-plugin-type-coverage": "^5.0.2",
|
||||||
|
"psy/psysh": "^0.12.24"
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true,
|
"sort-packages": true,
|
||||||
"preferred-install": "dist",
|
"preferred-install": "dist",
|
||||||
|
"process-timeout": 0,
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
"pestphp/pest-plugin": true
|
"pestphp/pest-plugin": true
|
||||||
}
|
}
|
||||||
@@ -59,36 +80,60 @@
|
|||||||
"bin/pest"
|
"bin/pest"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix -v",
|
"lint": [
|
||||||
"test:lint": "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix -v --dry-run",
|
"rector",
|
||||||
"test:types": "phpstan analyse --ansi --memory-limit=-1",
|
"pint --parallel"
|
||||||
"test:unit": "php bin/pest --colors=always --exclude-group=integration",
|
],
|
||||||
"test:parallel": "php bin/pest -p --colors=always --exclude-group=integration",
|
"test:lint": [
|
||||||
"test:integration": "php bin/pest --colors=always --group=integration",
|
"rector --dry-run",
|
||||||
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always",
|
"pint --parallel --test"
|
||||||
|
],
|
||||||
|
"test:profanity": "php bin/pest --profanity --compact",
|
||||||
|
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
|
||||||
|
"test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100",
|
||||||
|
"test:unit": "php bin/pest --exclude-group=integration --compact",
|
||||||
|
"test:inline": "php bin/pest --configuration=phpunit.inline.xml",
|
||||||
|
"test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3",
|
||||||
|
"test:integration": "php bin/pest --group=integration -v",
|
||||||
|
"test:tia": "php bin/pest --group=tia -v",
|
||||||
|
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots --exclude-group=tia",
|
||||||
"test": [
|
"test": [
|
||||||
"@test:lint",
|
"@test:lint",
|
||||||
"@test:types",
|
"@test:type:check",
|
||||||
|
"@test:type:coverage",
|
||||||
"@test:unit",
|
"@test:unit",
|
||||||
"@test:parallel",
|
"@test:parallel",
|
||||||
"@test:integration"
|
"@test:integration"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
|
||||||
"dev-1.x": "1.x-dev"
|
|
||||||
},
|
|
||||||
"pest": {
|
"pest": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
"Pest\\Mutate\\Plugins\\Mutate",
|
||||||
|
"Pest\\Plugins\\Configuration",
|
||||||
|
"Pest\\Plugins\\Bail",
|
||||||
|
"Pest\\Plugins\\Cache",
|
||||||
"Pest\\Plugins\\Coverage",
|
"Pest\\Plugins\\Coverage",
|
||||||
"Pest\\Plugins\\Init",
|
"Pest\\Plugins\\Init",
|
||||||
|
"Pest\\Plugins\\Environment",
|
||||||
|
"Pest\\Plugins\\Help",
|
||||||
|
"Pest\\Plugins\\Memory",
|
||||||
|
"Pest\\Plugins\\Only",
|
||||||
|
"Pest\\Plugins\\Printer",
|
||||||
|
"Pest\\Plugins\\ProcessIsolation",
|
||||||
|
"Pest\\Plugins\\Profile",
|
||||||
|
"Pest\\Plugins\\Retry",
|
||||||
|
"Pest\\Plugins\\Snapshot",
|
||||||
|
"Pest\\Plugins\\Verbose",
|
||||||
"Pest\\Plugins\\Version",
|
"Pest\\Plugins\\Version",
|
||||||
"Pest\\Plugins\\Environment"
|
"Pest\\Plugins\\Shard",
|
||||||
|
"Pest\\Plugins\\Tia",
|
||||||
|
"Pest\\Plugins\\Parallel"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"laravel": {
|
"phpstan": {
|
||||||
"providers": [
|
"includes": [
|
||||||
"Pest\\Laravel\\PestServiceProvider"
|
"extension.neon"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
ARG PHP=8.1
|
||||||
|
FROM php:${PHP}-cli-alpine
|
||||||
|
|
||||||
|
RUN apk update && apk add \
|
||||||
|
zip libzip-dev icu-dev git
|
||||||
|
|
||||||
|
RUN docker-php-ext-install zip intl
|
||||||
|
|
||||||
|
RUN apk add --no-cache linux-headers autoconf build-base
|
||||||
|
RUN pecl install xdebug
|
||||||
|
RUN docker-php-ext-enable xdebug
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENTRYPOINT ["php"]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
parameters:
|
||||||
|
universalObjectCratesClasses:
|
||||||
|
- Pest\Support\HigherOrderTapProxy
|
||||||
|
- Pest\Expectation
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?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\Event\Code;
|
||||||
|
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderableOnCollisionEditor;
|
||||||
|
use PHPUnit\Event\NoPreviousThrowableException;
|
||||||
|
use PHPUnit\Framework\Exception;
|
||||||
|
use PHPUnit\Util\Filter;
|
||||||
|
use PHPUnit\Util\ThrowableToStringMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*
|
||||||
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*/
|
||||||
|
final readonly class ThrowableBuilder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
* @throws NoPreviousThrowableException
|
||||||
|
*/
|
||||||
|
public static function from(\Throwable $t): Throwable
|
||||||
|
{
|
||||||
|
$previous = $t->getPrevious();
|
||||||
|
|
||||||
|
if ($previous !== null) {
|
||||||
|
$previous = self::from($previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
$trace = Filter::stackTraceFromThrowableAsString($t);
|
||||||
|
|
||||||
|
if ($t instanceof RenderableOnCollisionEditor && $frame = $t->toCollisionEditor()) {
|
||||||
|
$file = $frame->getFile();
|
||||||
|
$line = $frame->getLine();
|
||||||
|
|
||||||
|
$trace = "$file:$line\n$trace";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Throwable(
|
||||||
|
$t::class,
|
||||||
|
$t->getMessage(),
|
||||||
|
ThrowableToStringMapper::map($t),
|
||||||
|
$trace,
|
||||||
|
$previous,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
<?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 Pest\Logging\Converter;
|
||||||
|
use Pest\Support\Container;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
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\PrintedUnexpectedOutput;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*
|
||||||
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*/
|
||||||
|
final class JunitXmlLogger
|
||||||
|
{
|
||||||
|
private readonly Printer $printer;
|
||||||
|
|
||||||
|
private readonly Converter $converter;
|
||||||
|
|
||||||
|
private DOMDocument $document;
|
||||||
|
|
||||||
|
private DOMElement $root;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DOMElement[]
|
||||||
|
*/
|
||||||
|
private array $testSuites = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int,int>
|
||||||
|
*/
|
||||||
|
private array $testSuiteTests = [0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int,int>
|
||||||
|
*/
|
||||||
|
private array $testSuiteAssertions = [0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int,int>
|
||||||
|
*/
|
||||||
|
private array $testSuiteErrors = [0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int,int>
|
||||||
|
*/
|
||||||
|
private array $testSuiteFailures = [0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int,int>
|
||||||
|
*/
|
||||||
|
private array $testSuiteSkipped = [0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 Converter(Container::getInstance()->get(TestSuite::class)->rootPath);
|
||||||
|
$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()));
|
||||||
|
if ($event->testSuite()->isForTestClass()) {
|
||||||
|
$testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->testSuiteLevel > 0) {
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
|
||||||
|
} else {
|
||||||
|
$this->root->appendChild($testSuite);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->testSuiteLevel++;
|
||||||
|
$this->testSuites[$this->testSuiteLevel] = $testSuite;
|
||||||
|
$this->testSuiteTests[$this->testSuiteLevel] = 0;
|
||||||
|
$this->testSuiteAssertions[$this->testSuiteLevel] = 0;
|
||||||
|
$this->testSuiteErrors[$this->testSuiteLevel] = 0;
|
||||||
|
$this->testSuiteFailures[$this->testSuiteLevel] = 0;
|
||||||
|
$this->testSuiteSkipped[$this->testSuiteLevel] = 0;
|
||||||
|
$this->testSuiteTimes[$this->testSuiteLevel] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSuiteFinished(): void
|
||||||
|
{
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'tests',
|
||||||
|
(string) $this->testSuiteTests[$this->testSuiteLevel],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'assertions',
|
||||||
|
(string) $this->testSuiteAssertions[$this->testSuiteLevel],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'errors',
|
||||||
|
(string) $this->testSuiteErrors[$this->testSuiteLevel],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'failures',
|
||||||
|
(string) $this->testSuiteFailures[$this->testSuiteLevel],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'skipped',
|
||||||
|
(string) $this->testSuiteSkipped[$this->testSuiteLevel],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||||
|
'time',
|
||||||
|
sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->testSuiteLevel > 1) {
|
||||||
|
$this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel];
|
||||||
|
$this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel];
|
||||||
|
$this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel];
|
||||||
|
$this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel];
|
||||||
|
$this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel];
|
||||||
|
$this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->testSuiteLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testPreparationStarted(PreparationStarted $event): void
|
||||||
|
{
|
||||||
|
$this->createTestCase($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPreparationFailed(): void
|
||||||
|
{
|
||||||
|
$this->preparationFailed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPrepared(): void
|
||||||
|
{
|
||||||
|
$this->prepared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPrintedUnexpectedOutput(PrintedUnexpectedOutput $event): void
|
||||||
|
{
|
||||||
|
assert($this->currentTestCase !== null);
|
||||||
|
|
||||||
|
$systemOut = $this->document->createElement(
|
||||||
|
'system-out',
|
||||||
|
Xml::prepareString($event->output()),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->currentTestCase->appendChild($systemOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testFinished(Finished $event): void
|
||||||
|
{
|
||||||
|
if (! $this->prepared || $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 TestPrintedUnexpectedOutputSubscriber($this),
|
||||||
|
new TestFinishedSubscriber($this),
|
||||||
|
new TestErroredSubscriber($this),
|
||||||
|
new TestFailedSubscriber($this),
|
||||||
|
new TestMarkedIncompleteSubscriber($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());
|
||||||
|
$throwable = $event->throwable();
|
||||||
|
$buffer .= trim(
|
||||||
|
$this->converter->getExceptionMessage($throwable).PHP_EOL.
|
||||||
|
$this->converter->getExceptionDetails($throwable),
|
||||||
|
);
|
||||||
|
|
||||||
|
$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
|
||||||
|
*
|
||||||
|
* @phpstan-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);
|
||||||
|
$testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test));
|
||||||
|
$testCase->setAttribute('file', $file);
|
||||||
|
if ($test->isTestMethod()) {
|
||||||
|
assert($test instanceof TestMethod);
|
||||||
|
|
||||||
|
$className = $this->converter->getTrimmedTestClassName($test);
|
||||||
|
$testCase->setAttribute('class', $className);
|
||||||
|
$testCase->setAttribute('classname', str_replace('\\', '.', $className));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->currentTestCase = $testCase;
|
||||||
|
$this->time = $event->telemetryInfo()->time();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ParaTest\WrapperRunner;
|
||||||
|
|
||||||
|
use PHPUnit\TextUI\Output\Printer;
|
||||||
|
|
||||||
|
use function preg_match;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class ProgressPrinterOutput implements Printer
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Printer $progressPrinter,
|
||||||
|
private Printer $outputPrinter,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function print(string $buffer): void
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
$buffer === "\n"
|
||||||
|
|| preg_match('/^ +$/', $buffer) === 1
|
||||||
|
|| preg_match('/^ \d+ \/ \d+ \(...%\)$/', $buffer) === 1
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match ($buffer) {
|
||||||
|
'E', 'F', 'I', 'N', 'D', 'R', 'W', 'S', 'T', '.' => $this->progressPrinter->print($buffer),
|
||||||
|
default => $this->outputPrinter->print($buffer),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
$this->progressPrinter->flush();
|
||||||
|
$this->outputPrinter->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?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\Runner\Filter;
|
||||||
|
|
||||||
|
use Pest\Contracts\HasPrintableTestCaseName;
|
||||||
|
use PHPUnit\Framework\Test;
|
||||||
|
use PHPUnit\Framework\TestSuite;
|
||||||
|
use PHPUnit\Runner\Phpt\TestCase as PhptTestCase;
|
||||||
|
use RecursiveFilterIterator;
|
||||||
|
use RecursiveIterator;
|
||||||
|
|
||||||
|
use function end;
|
||||||
|
use function preg_match;
|
||||||
|
use function trim;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends RecursiveFilterIterator<int, Test, RecursiveIterator<int, Test>>
|
||||||
|
*
|
||||||
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*/
|
||||||
|
abstract class NameFilterIterator extends RecursiveFilterIterator
|
||||||
|
{
|
||||||
|
private readonly CompiledNameFilter $filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param RecursiveIterator<int, Test> $iterator
|
||||||
|
* @param non-empty-string $filter
|
||||||
|
*/
|
||||||
|
public function __construct(RecursiveIterator $iterator, string $filter)
|
||||||
|
{
|
||||||
|
parent::__construct($iterator);
|
||||||
|
|
||||||
|
$this->filter = CompiledNameFilter::from($filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function accept(): bool
|
||||||
|
{
|
||||||
|
$test = $this->getInnerIterator()->current();
|
||||||
|
|
||||||
|
if ($test instanceof TestSuite) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($test instanceof PhptTestCase) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($test instanceof HasPrintableTestCaseName) {
|
||||||
|
$name = trim(
|
||||||
|
$test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName().$test->dataSetAsString()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$name = $test::class.'::'.$test->nameWithDataSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
$accepted = @preg_match($this->filter->regularExpression(), $name, $matches) === 1;
|
||||||
|
|
||||||
|
if ($accepted && $this->filter->hasDataSetRange()) {
|
||||||
|
$set = end($matches);
|
||||||
|
$accepted = $set >= $this->filter->dataSetMinimum() && $set <= $this->filter->dataSetMaximum();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->doAccept($accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function doAccept(bool $result): bool;
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
<?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\Runner\TestRunHistory;
|
||||||
|
|
||||||
|
use const DIRECTORY_SEPARATOR;
|
||||||
|
use const LOCK_EX;
|
||||||
|
use const LOCK_UN;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestStatus\TestStatus;
|
||||||
|
use PHPUnit\Runner\DirectoryDoesNotExistException;
|
||||||
|
use PHPUnit\Runner\Exception;
|
||||||
|
use PHPUnit\Util\Filesystem;
|
||||||
|
|
||||||
|
use function array_keys;
|
||||||
|
use function dirname;
|
||||||
|
use function fclose;
|
||||||
|
use function file_get_contents;
|
||||||
|
use function flock;
|
||||||
|
use function fopen;
|
||||||
|
use function ftruncate;
|
||||||
|
use function fwrite;
|
||||||
|
use function is_array;
|
||||||
|
use function is_dir;
|
||||||
|
use function is_file;
|
||||||
|
use function is_float;
|
||||||
|
use function is_int;
|
||||||
|
use function is_string;
|
||||||
|
use function json_decode;
|
||||||
|
use function json_encode;
|
||||||
|
use function Pest\version;
|
||||||
|
use function rewind;
|
||||||
|
use function stream_get_contents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*/
|
||||||
|
final class DefaultTestRunHistory implements TestRunHistory
|
||||||
|
{
|
||||||
|
private const string DEFAULT_FILENAME = '.phpunit.result.cache';
|
||||||
|
|
||||||
|
private readonly string $filename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, TestStatus>
|
||||||
|
*/
|
||||||
|
private array $defects = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, float>
|
||||||
|
*/
|
||||||
|
private array $times = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, true>
|
||||||
|
*/
|
||||||
|
private array $changedDefects = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, true>
|
||||||
|
*/
|
||||||
|
private array $changedTimes = [];
|
||||||
|
|
||||||
|
public function __construct(?string $filepath = null)
|
||||||
|
{
|
||||||
|
if ($filepath !== null && is_dir($filepath)) {
|
||||||
|
$filepath .= DIRECTORY_SEPARATOR.self::DEFAULT_FILENAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->filename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_FILENAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(TestRunHistoryId $id, TestStatus $status): void
|
||||||
|
{
|
||||||
|
if ($status->isSuccess()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->defects[$id->asString()] = $status;
|
||||||
|
$this->changedDefects[$id->asString()] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(TestRunHistoryId $id): void
|
||||||
|
{
|
||||||
|
unset($this->defects[$id->asString()]);
|
||||||
|
|
||||||
|
$this->changedDefects[$id->asString()] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status(TestRunHistoryId $id): TestStatus
|
||||||
|
{
|
||||||
|
return $this->defects[$id->asString()] ?? TestStatus::unknown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTime(TestRunHistoryId $id, float $time): void
|
||||||
|
{
|
||||||
|
$this->times[$id->asString()] = $time;
|
||||||
|
$this->changedTimes[$id->asString()] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function time(TestRunHistoryId $id): float
|
||||||
|
{
|
||||||
|
return $this->times[$id->asString()] ?? 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mergeWith(self $other): void
|
||||||
|
{
|
||||||
|
foreach ($other->defects as $id => $defect) {
|
||||||
|
$this->defects[$id] = $defect;
|
||||||
|
$this->changedDefects[$id] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($other->times as $id => $time) {
|
||||||
|
$this->times[$id] = $time;
|
||||||
|
$this->changedTimes[$id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function load(): void
|
||||||
|
{
|
||||||
|
if (! is_file($this->filename)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = file_get_contents($this->filename);
|
||||||
|
|
||||||
|
if ($contents === false) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
return;
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsed = $this->parse($contents);
|
||||||
|
|
||||||
|
if ($parsed === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$this->defects, $this->times] = $parsed;
|
||||||
|
|
||||||
|
$this->changedDefects = [];
|
||||||
|
$this->changedTimes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function persist(): void
|
||||||
|
{
|
||||||
|
$this->writeToFile(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function persistAndPrune(): void
|
||||||
|
{
|
||||||
|
$this->writeToFile(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function writeToFile(bool $prune): void
|
||||||
|
{
|
||||||
|
if (! Filesystem::createDirectory(dirname($this->filename))) {
|
||||||
|
throw new DirectoryDoesNotExistException(dirname($this->filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen($this->filename, 'c+');
|
||||||
|
|
||||||
|
if ($handle === false) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
return;
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
flock($handle, LOCK_EX);
|
||||||
|
|
||||||
|
if ($prune) {
|
||||||
|
$defects = [];
|
||||||
|
|
||||||
|
foreach ($this->defects as $id => $status) {
|
||||||
|
if (isset($this->changedDefects[$id])) {
|
||||||
|
$defects[$id] = $status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$times = [];
|
||||||
|
|
||||||
|
foreach ($this->times as $id => $time) {
|
||||||
|
if (isset($this->changedTimes[$id]) || isset($this->changedDefects[$id])) {
|
||||||
|
$times[$id] = $time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$parsed = $this->parse((string) stream_get_contents($handle));
|
||||||
|
|
||||||
|
if ($parsed !== null) {
|
||||||
|
[$defects, $times] = $parsed;
|
||||||
|
|
||||||
|
foreach (array_keys($this->changedDefects) as $id) {
|
||||||
|
if (isset($this->defects[$id])) {
|
||||||
|
$defects[$id] = $this->defects[$id];
|
||||||
|
} else {
|
||||||
|
unset($defects[$id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->times as $id => $time) {
|
||||||
|
if (isset($this->changedTimes[$id])) {
|
||||||
|
$times[$id] = $time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$defects = $this->defects;
|
||||||
|
$times = $this->times;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'version' => $this->version(),
|
||||||
|
'defects' => [],
|
||||||
|
'times' => $times,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($defects as $test => $status) {
|
||||||
|
$data['defects'][$test] = $status->asInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_encode($data);
|
||||||
|
|
||||||
|
if ($json !== false) {
|
||||||
|
ftruncate($handle, 0);
|
||||||
|
rewind($handle);
|
||||||
|
fwrite($handle, $json);
|
||||||
|
}
|
||||||
|
|
||||||
|
flock($handle, LOCK_UN);
|
||||||
|
fclose($handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ?array{0: array<string, TestStatus>, 1: array<string, float>}
|
||||||
|
*/
|
||||||
|
private function parse(string $contents): ?array
|
||||||
|
{
|
||||||
|
$data = json_decode(
|
||||||
|
$contents,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (! is_array($data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! isset($data['version']) || $data['version'] !== $this->version()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! isset($data['defects'], $data['times']) || ! is_array($data['defects']) || ! is_array($data['times'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$defects = [];
|
||||||
|
|
||||||
|
foreach ($data['defects'] as $test => $status) {
|
||||||
|
if (! is_string($test) || ! is_int($status)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$defects[$test] = TestStatus::from($status);
|
||||||
|
}
|
||||||
|
|
||||||
|
$times = [];
|
||||||
|
|
||||||
|
foreach ($data['times'] as $test => $time) {
|
||||||
|
if (! is_string($test) || (! is_float($time) && ! is_int($time))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$times[$test] = (float) $time;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$defects, $times];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function version(): string
|
||||||
|
{
|
||||||
|
return 'pest_'.version();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPUnit\Runner;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Pest\Contracts\HasPrintableTestCaseName;
|
||||||
|
use Pest\Panic;
|
||||||
|
use Pest\TestCases\IgnorableTestCase;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function array_diff;
|
||||||
|
use function array_values;
|
||||||
|
use function basename;
|
||||||
|
use function class_exists;
|
||||||
|
use function get_declared_classes;
|
||||||
|
use function substr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*/
|
||||||
|
final class TestSuiteLoader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @psalm-var list<class-string>
|
||||||
|
*/
|
||||||
|
private static array $loadedClasses = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-var array<string, array<class-string>>
|
||||||
|
*/
|
||||||
|
private static array $loadedClassesByFilename = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-var list<class-string>
|
||||||
|
*/
|
||||||
|
private static array $declaredClasses = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
if (empty(self::$declaredClasses)) {
|
||||||
|
self::$declaredClasses = get_declared_classes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function load(string $suiteClassFile): ReflectionClass
|
||||||
|
{
|
||||||
|
$suiteClassName = $this->classNameFromFileName($suiteClassFile);
|
||||||
|
|
||||||
|
(static function () use ($suiteClassFile) {
|
||||||
|
try {
|
||||||
|
include_once $suiteClassFile;
|
||||||
|
|
||||||
|
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Panic::with($e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
$loadedClasses = array_values(
|
||||||
|
array_diff(
|
||||||
|
get_declared_classes(),
|
||||||
|
array_merge(
|
||||||
|
self::$declaredClasses,
|
||||||
|
self::$loadedClasses
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
self::$loadedClasses = array_merge($loadedClasses, self::$loadedClasses);
|
||||||
|
|
||||||
|
foreach ($loadedClasses as $loadedClass) {
|
||||||
|
$reflection = new ReflectionClass($loadedClass);
|
||||||
|
$filename = $reflection->getFileName();
|
||||||
|
self::$loadedClassesByFilename[$filename] = [
|
||||||
|
$loadedClass,
|
||||||
|
...self::$loadedClassesByFilename[$filename] ?? [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$loadedClasses = array_merge(self::$loadedClassesByFilename[$suiteClassFile] ?? [], $loadedClasses);
|
||||||
|
|
||||||
|
if (empty($loadedClasses)) {
|
||||||
|
return $this->exceptionFor($suiteClassName, $suiteClassFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$testCaseFound = false;
|
||||||
|
$class = false;
|
||||||
|
|
||||||
|
foreach (array_reverse($loadedClasses) as $loadedClass) {
|
||||||
|
if (
|
||||||
|
is_subclass_of($loadedClass, HasPrintableTestCaseName::class)
|
||||||
|
|| is_subclass_of($loadedClass, TestCase::class)) {
|
||||||
|
try {
|
||||||
|
$class = new ReflectionClass($loadedClass);
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($class->isAbstract() || ($suiteClassFile !== $class->getFileName())) {
|
||||||
|
if (! str_contains($class->getFileName(), 'TestCaseFactory.php')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$suiteClassName = $loadedClass;
|
||||||
|
$testCaseFound = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $testCaseFound) {
|
||||||
|
foreach (array_reverse($loadedClasses) as $loadedClass) {
|
||||||
|
$offset = 0 - strlen($suiteClassName);
|
||||||
|
|
||||||
|
if (stripos(substr($loadedClass, $offset - 1), '\\'.$suiteClassName) === 0 ||
|
||||||
|
stripos(substr($loadedClass, $offset - 1), '_'.$suiteClassName) === 0) {
|
||||||
|
try {
|
||||||
|
$class = new ReflectionClass($loadedClass);
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$suiteClassName = $loadedClass;
|
||||||
|
$testCaseFound = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $testCaseFound) {
|
||||||
|
return $this->exceptionFor($suiteClassName, $suiteClassFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! class_exists($suiteClassName, false)) {
|
||||||
|
return $this->exceptionFor($suiteClassName, $suiteClassFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
|
||||||
|
if ($class->isSubclassOf(TestCase::class) && ! $class->isAbstract()) {
|
||||||
|
return $class;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($class->hasMethod('suite')) {
|
||||||
|
try {
|
||||||
|
$method = $class->getMethod('suite');
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
} catch (ReflectionException $e) {
|
||||||
|
throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
|
||||||
|
}
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
|
||||||
|
if (! $method->isAbstract() && $method->isPublic() && $method->isStatic()) {
|
||||||
|
return $class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->exceptionFor($suiteClassName, $suiteClassFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reload(ReflectionClass $aClass): ReflectionClass
|
||||||
|
{
|
||||||
|
return $aClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function classNameFromFileName(string $suiteClassFile): string
|
||||||
|
{
|
||||||
|
$className = basename($suiteClassFile, '.php');
|
||||||
|
$dotPos = strpos($className, '.');
|
||||||
|
|
||||||
|
if ($dotPos !== false) {
|
||||||
|
$className = substr($className, 0, $dotPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $className;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function exceptionFor(string $className, string $filename): ReflectionClass
|
||||||
|
{
|
||||||
|
return new ReflectionClass(IgnorableTestCase::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
<?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\Runner;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\DataProviderTestSuite;
|
||||||
|
use PHPUnit\Framework\IterativeTestSuite;
|
||||||
|
use PHPUnit\Framework\Reorderable;
|
||||||
|
use PHPUnit\Framework\Test;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use PHPUnit\Framework\TestSuite;
|
||||||
|
use PHPUnit\Runner\TestRunHistory\NullTestRunHistory;
|
||||||
|
use PHPUnit\Runner\TestRunHistory\TestRunHistory;
|
||||||
|
use PHPUnit\Runner\TestRunHistory\TestRunHistoryId;
|
||||||
|
|
||||||
|
use function array_diff;
|
||||||
|
use function array_merge;
|
||||||
|
use function array_reverse;
|
||||||
|
use function array_splice;
|
||||||
|
use function assert;
|
||||||
|
use function count;
|
||||||
|
use function in_array;
|
||||||
|
use function max;
|
||||||
|
use function shuffle;
|
||||||
|
use function usort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*/
|
||||||
|
final class TestSuiteSorter
|
||||||
|
{
|
||||||
|
public const int ORDER_DEFAULT = 0;
|
||||||
|
|
||||||
|
public const int ORDER_RANDOMIZED = 1;
|
||||||
|
|
||||||
|
public const int ORDER_REVERSED = 2;
|
||||||
|
|
||||||
|
public const int ORDER_DEFECTS_FIRST = 3;
|
||||||
|
|
||||||
|
public const int ORDER_DURATION_ASCENDING = 4;
|
||||||
|
|
||||||
|
public const int ORDER_SIZE_ASCENDING = 5;
|
||||||
|
|
||||||
|
public const int ORDER_DURATION_DESCENDING = 6;
|
||||||
|
|
||||||
|
public const int ORDER_SIZE_DESCENDING = 7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var non-empty-array<non-empty-string, positive-int>
|
||||||
|
*/
|
||||||
|
private const array SIZE_SORT_WEIGHT = [
|
||||||
|
'small' => 1,
|
||||||
|
'medium' => 2,
|
||||||
|
'large' => 3,
|
||||||
|
'unknown' => 4,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, int> Associative array of (string => DEFECT_SORT_WEIGHT) elements
|
||||||
|
*/
|
||||||
|
private array $defectSortOrder = [];
|
||||||
|
|
||||||
|
private readonly TestRunHistory $testRunHistory;
|
||||||
|
|
||||||
|
public function __construct(?TestRunHistory $testRunHistory = null)
|
||||||
|
{
|
||||||
|
$this->testRunHistory = $testRunHistory ?? new NullTestRunHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function reorderTestsInSuite(Test $suite, int $order, bool $resolveDependencies, int $orderDefects): void
|
||||||
|
{
|
||||||
|
$allowedOrders = [
|
||||||
|
self::ORDER_DEFAULT,
|
||||||
|
self::ORDER_REVERSED,
|
||||||
|
self::ORDER_RANDOMIZED,
|
||||||
|
self::ORDER_DURATION_ASCENDING,
|
||||||
|
self::ORDER_SIZE_ASCENDING,
|
||||||
|
self::ORDER_DURATION_DESCENDING,
|
||||||
|
self::ORDER_SIZE_DESCENDING,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! in_array($order, $allowedOrders, true)) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
throw new InvalidOrderException;
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedOrderDefects = [
|
||||||
|
self::ORDER_DEFAULT,
|
||||||
|
self::ORDER_DEFECTS_FIRST,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! in_array($orderDefects, $allowedOrderDefects, true)) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
throw new InvalidOrderException;
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($suite instanceof IterativeTestSuite) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($suite instanceof TestSuite) {
|
||||||
|
foreach ($suite as $_suite) {
|
||||||
|
$this->reorderTestsInSuite($_suite, $order, $resolveDependencies, $orderDefects);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($orderDefects === self::ORDER_DEFECTS_FIRST) {
|
||||||
|
$this->addSuiteToDefectSortOrder($suite);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sort($suite, $order, $resolveDependencies, $orderDefects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sort(TestSuite $suite, int $order, bool $resolveDependencies, int $orderDefects): void
|
||||||
|
{
|
||||||
|
$tests = $suite->tests();
|
||||||
|
|
||||||
|
if ($tests === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($order === self::ORDER_REVERSED) {
|
||||||
|
$tests = $this->reverse($tests);
|
||||||
|
} elseif ($order === self::ORDER_RANDOMIZED) {
|
||||||
|
$tests = $this->randomize($tests);
|
||||||
|
} elseif ($order === self::ORDER_DURATION_ASCENDING) {
|
||||||
|
$tests = $this->sortByDuration($tests);
|
||||||
|
} elseif ($order === self::ORDER_DURATION_DESCENDING) {
|
||||||
|
$tests = $this->sortByDurationDescending($tests);
|
||||||
|
} elseif ($order === self::ORDER_SIZE_ASCENDING) {
|
||||||
|
$tests = $this->sortBySize($tests);
|
||||||
|
} elseif ($order === self::ORDER_SIZE_DESCENDING) {
|
||||||
|
$tests = $this->sortBySizeDescending($tests);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($orderDefects === self::ORDER_DEFECTS_FIRST) {
|
||||||
|
$tests = $this->sortDefectsFirst($tests);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($resolveDependencies && ! ($suite instanceof DataProviderTestSuite)) {
|
||||||
|
/** @noinspection PhpParamsInspection */
|
||||||
|
/** @phpstan-ignore argument.type */
|
||||||
|
$tests = $this->resolveDependencies($tests);
|
||||||
|
}
|
||||||
|
|
||||||
|
$suite->setTests($tests);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addSuiteToDefectSortOrder(TestSuite $suite): void
|
||||||
|
{
|
||||||
|
$max = 0;
|
||||||
|
|
||||||
|
foreach ($suite->tests() as $test) {
|
||||||
|
assert($test instanceof Reorderable);
|
||||||
|
|
||||||
|
$sortId = $test->sortId();
|
||||||
|
|
||||||
|
if (! isset($this->defectSortOrder[$sortId])) {
|
||||||
|
$this->defectSortOrder[$sortId] = $this->testRunHistory->status(TestRunHistoryId::fromReorderable($test))->sortWeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
$max = max($max, $this->defectSortOrder[$sortId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->defectSortOrder[$suite->sortId()] = $max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Test> $tests
|
||||||
|
* @return list<Test>
|
||||||
|
*/
|
||||||
|
private function reverse(array $tests): array
|
||||||
|
{
|
||||||
|
return array_reverse($tests);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Test> $tests
|
||||||
|
* @return list<Test>
|
||||||
|
*/
|
||||||
|
private function randomize(array $tests): array
|
||||||
|
{
|
||||||
|
shuffle($tests);
|
||||||
|
|
||||||
|
return $tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Test> $tests
|
||||||
|
* @return list<Test>
|
||||||
|
*/
|
||||||
|
private function sortDefectsFirst(array $tests): array
|
||||||
|
{
|
||||||
|
usort(
|
||||||
|
$tests,
|
||||||
|
fn (Test $left, Test $right) => $this->cmpDefectPriorityAndTime($left, $right),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Test> $tests
|
||||||
|
* @return list<Test>
|
||||||
|
*/
|
||||||
|
private function sortByDuration(array $tests): array
|
||||||
|
{
|
||||||
|
usort(
|
||||||
|
$tests,
|
||||||
|
fn (Test $left, Test $right) => $this->cmpDuration($left, $right),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Test> $tests
|
||||||
|
* @return list<Test>
|
||||||
|
*/
|
||||||
|
private function sortByDurationDescending(array $tests): array
|
||||||
|
{
|
||||||
|
usort(
|
||||||
|
$tests,
|
||||||
|
fn (Test $left, Test $right) => $this->cmpDuration($right, $left),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Test> $tests
|
||||||
|
* @return list<Test>
|
||||||
|
*/
|
||||||
|
private function sortBySize(array $tests): array
|
||||||
|
{
|
||||||
|
usort(
|
||||||
|
$tests,
|
||||||
|
fn (Test $left, Test $right) => $this->cmpSize($left, $right),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Test> $tests
|
||||||
|
* @return list<Test>
|
||||||
|
*/
|
||||||
|
private function sortBySizeDescending(array $tests): array
|
||||||
|
{
|
||||||
|
usort(
|
||||||
|
$tests,
|
||||||
|
fn (Test $left, Test $right) => $this->cmpSize($right, $left),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cmpDefectPriorityAndTime(Test $a, Test $b): int
|
||||||
|
{
|
||||||
|
assert($a instanceof Reorderable);
|
||||||
|
assert($b instanceof Reorderable);
|
||||||
|
|
||||||
|
$priorityA = $this->defectSortOrder[$a->sortId()] ?? 0;
|
||||||
|
$priorityB = $this->defectSortOrder[$b->sortId()] ?? 0;
|
||||||
|
|
||||||
|
return $priorityB <=> $priorityA;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cmpDuration(Test $a, Test $b): int
|
||||||
|
{
|
||||||
|
return $this->durationWeight($a) <=> $this->durationWeight($b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function durationWeight(Test $test): float
|
||||||
|
{
|
||||||
|
if ($test instanceof TestSuite) {
|
||||||
|
$sum = 0.0;
|
||||||
|
|
||||||
|
foreach ($test->tests() as $inner) {
|
||||||
|
$sum += $this->durationWeight($inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($test instanceof Reorderable) {
|
||||||
|
return $this->testRunHistory->time(TestRunHistoryId::fromReorderable($test));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cmpSize(Test $a, Test $b): int
|
||||||
|
{
|
||||||
|
return $this->sizeWeight($a) <=> $this->sizeWeight($b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return positive-int
|
||||||
|
*/
|
||||||
|
private function sizeWeight(Test $test): int
|
||||||
|
{
|
||||||
|
if ($test instanceof TestCase || $test instanceof DataProviderTestSuite) {
|
||||||
|
return self::SIZE_SORT_WEIGHT[$test->size()->asString()];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($test instanceof TestSuite) {
|
||||||
|
$max = 0;
|
||||||
|
|
||||||
|
foreach ($test->tests() as $inner) {
|
||||||
|
$weight = $this->sizeWeight($inner);
|
||||||
|
|
||||||
|
if ($weight > $max) {
|
||||||
|
$max = $weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($max > 0) {
|
||||||
|
return $max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SIZE_SORT_WEIGHT['unknown'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<TestCase> $tests
|
||||||
|
* @return list<TestCase>
|
||||||
|
*/
|
||||||
|
private function resolveDependencies(array $tests): array
|
||||||
|
{
|
||||||
|
if (! $this->anyTestHasDependencies($tests)) {
|
||||||
|
return $tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newTestOrder = [];
|
||||||
|
$i = 0;
|
||||||
|
$provided = [];
|
||||||
|
|
||||||
|
while ($tests !== [] && $i < count($tests)) {
|
||||||
|
if (array_diff($tests[$i]->requires(), $provided) === []) {
|
||||||
|
$provided = array_merge($provided, $tests[$i]->provides());
|
||||||
|
$newTestOrder = array_merge($newTestOrder, array_splice($tests, $i, 1));
|
||||||
|
$i = 0;
|
||||||
|
} else {
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_merge($newTestOrder, $tests);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param iterable<Test> $tests
|
||||||
|
*/
|
||||||
|
private function anyTestHasDependencies(iterable $tests): bool
|
||||||
|
{
|
||||||
|
foreach ($tests as $test) {
|
||||||
|
if ($test instanceof TestSuite) {
|
||||||
|
if ($this->anyTestHasDependencies($test->tests())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($test instanceof TestCase && $test->requires() !== []) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?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\TextUI\Command;
|
||||||
|
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
|
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
|
||||||
|
use PHPUnit\TextUI\Configuration\Configuration;
|
||||||
|
use PHPUnit\TextUI\Configuration\NoCoverageCacheDirectoryException;
|
||||||
|
use SebastianBergmann\CodeCoverage\StaticAnalysis\CacheWarmer;
|
||||||
|
use SebastianBergmann\Timer\NoActiveTimerException;
|
||||||
|
use SebastianBergmann\Timer\Timer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*/
|
||||||
|
final readonly class WarmCodeCoverageCacheCommand implements Command
|
||||||
|
{
|
||||||
|
private Configuration $configuration;
|
||||||
|
|
||||||
|
private CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
|
||||||
|
|
||||||
|
public function __construct(Configuration $configuration, CodeCoverageFilterRegistry $codeCoverageFilterRegistry)
|
||||||
|
{
|
||||||
|
$this->configuration = $configuration;
|
||||||
|
$this->codeCoverageFilterRegistry = $codeCoverageFilterRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NoActiveTimerException
|
||||||
|
* @throws NoCoverageCacheDirectoryException
|
||||||
|
*/
|
||||||
|
public function execute(): Result
|
||||||
|
{
|
||||||
|
if (! $this->configuration->hasCoverageCacheDirectory()) {
|
||||||
|
return Result::from(
|
||||||
|
'Cache for static analysis has not been configured'.PHP_EOL,
|
||||||
|
Result::FAILURE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->codeCoverageFilterRegistry->init($this->configuration, true);
|
||||||
|
|
||||||
|
if (! $this->codeCoverageFilterRegistry->configured()) {
|
||||||
|
return Result::from(
|
||||||
|
'Filter for code coverage has not been configured'.PHP_EOL,
|
||||||
|
Result::FAILURE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$timer = new Timer;
|
||||||
|
$timer->start();
|
||||||
|
|
||||||
|
(new CacheWarmer)->warmCache(
|
||||||
|
$this->configuration->coverageCacheDirectory(),
|
||||||
|
! $this->configuration->disableCodeCoverageIgnore(),
|
||||||
|
$this->configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage(),
|
||||||
|
$this->codeCoverageFilterRegistry->get(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Result::from();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?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\TextUI\Output\Default\ProgressPrinter;
|
||||||
|
|
||||||
|
use PHPUnit\Event\Test\Skipped;
|
||||||
|
use PHPUnit\Event\Test\SkippedSubscriber;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*/
|
||||||
|
final readonly class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
|
||||||
|
{
|
||||||
|
public function notify(Skipped $event): void
|
||||||
|
{
|
||||||
|
if ($event->message() === '__TODO__') {
|
||||||
|
$this->printTodoItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->printer()->testSkipped();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function printTodoItem(): void
|
||||||
|
{
|
||||||
|
$mirror = new ReflectionClass($this->printer());
|
||||||
|
$printProgress = $mirror->getMethod('printProgress');
|
||||||
|
$printProgress->invoke($this->printer(), 'T');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?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\TextUI;
|
||||||
|
|
||||||
|
use Pest\Plugins\Only;
|
||||||
|
use Pest\Runner\Filter\EnsureTestCaseIsInitiatedFilter;
|
||||||
|
use PHPUnit\Event;
|
||||||
|
use PHPUnit\Framework\TestSuite;
|
||||||
|
use PHPUnit\Runner\Filter\Factory;
|
||||||
|
use PHPUnit\TextUI\Configuration\Configuration;
|
||||||
|
use PHPUnit\TextUI\Configuration\FilterNotConfiguredException;
|
||||||
|
|
||||||
|
use function array_map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*/
|
||||||
|
final readonly class TestSuiteFilterProcessor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws Event\RuntimeException
|
||||||
|
* @throws FilterNotConfiguredException
|
||||||
|
*/
|
||||||
|
public function process(Configuration $configuration, TestSuite $suite): void
|
||||||
|
{
|
||||||
|
$factory = new Factory;
|
||||||
|
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
(fn () => $this->filters[] = [
|
||||||
|
'className' => EnsureTestCaseIsInitiatedFilter::class,
|
||||||
|
'argument' => '',
|
||||||
|
])->call($factory);
|
||||||
|
|
||||||
|
if (! $configuration->hasFilter() &&
|
||||||
|
! $configuration->hasGroups() &&
|
||||||
|
! $configuration->hasExcludeGroups() &&
|
||||||
|
! $configuration->hasExcludeFilter() &&
|
||||||
|
! $configuration->hasTestsCovering() &&
|
||||||
|
! $configuration->hasTestsUsing() &&
|
||||||
|
! Only::isEnabled()) {
|
||||||
|
$suite->injectFilter($factory);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($configuration->hasExcludeGroups()) {
|
||||||
|
$factory->addExcludeGroupFilter(
|
||||||
|
$configuration->excludeGroups(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Only::isEnabled()) {
|
||||||
|
$factory->addIncludeGroupFilter([Only::group()]);
|
||||||
|
} elseif ($configuration->hasGroups()) {
|
||||||
|
$factory->addIncludeGroupFilter(
|
||||||
|
$configuration->groups(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($configuration->hasTestsCovering()) {
|
||||||
|
$factory->addIncludeGroupFilter(
|
||||||
|
array_map(
|
||||||
|
static fn (string $name): string => '__phpunit_covers_'.$name,
|
||||||
|
$configuration->testsCovering(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($configuration->hasTestsUsing()) {
|
||||||
|
$factory->addIncludeGroupFilter(
|
||||||
|
array_map(
|
||||||
|
static fn (string $name): string => '__phpunit_uses_'.$name,
|
||||||
|
$configuration->testsUsing(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($configuration->hasExcludeFilter()) {
|
||||||
|
$factory->addExcludeNameFilter(
|
||||||
|
$configuration->excludeFilter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($configuration->hasFilter()) {
|
||||||
|
$factory->addIncludeNameFilter(
|
||||||
|
$configuration->filter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$suite->injectFilter($factory);
|
||||||
|
|
||||||
|
Event\Facade::emitter()->testSuiteFiltered(
|
||||||
|
Event\TestSuite\TestSuiteBuilder::from($suite),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
-211
@@ -1,272 +1,121 @@
|
|||||||
parameters:
|
parameters:
|
||||||
ignoreErrors:
|
ignoreErrors:
|
||||||
-
|
-
|
||||||
message: "#^Cannot access an offset on mixed\\.$#"
|
message: '#^Trait Pest\\Concerns\\Expectable is used zero times and is not analysed\.$#'
|
||||||
|
identifier: trait.unused
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Actions/AddsDefaults.php
|
path: src/Concerns/Expectable.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$out of class Pest\\\\Logging\\\\JUnit constructor expects string, mixed given\\.$#"
|
message: '#^Trait Pest\\Concerns\\Logging\\WritesToConsole is used zero times and is not analysed\.$#'
|
||||||
|
identifier: trait.unused
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Actions/AddsDefaults.php
|
path: src/Concerns/Logging/WritesToConsole.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#2 \\$verbose of class NunoMaduro\\\\Collision\\\\Adapters\\\\Phpunit\\\\Printer constructor expects bool, mixed given\\.$#"
|
message: '#^Trait Pest\\Concerns\\Testable is used zero times and is not analysed\.$#'
|
||||||
|
identifier: trait.unused
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Actions/AddsDefaults.php
|
path: src/Concerns/Testable.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#2 \\$verbose of class Pest\\\\Logging\\\\TeamCity constructor expects bool, mixed given\\.$#"
|
message: '#^Method Pest\\Expectation\:\:and\(\) should return Pest\\Expectation\<TAndValue\> but returns \(Pest\\Expectation&TAndValue\)\|Pest\\Expectation\<TAndValue of mixed\>\.$#'
|
||||||
count: 1
|
identifier: return.type
|
||||||
path: src/Actions/AddsDefaults.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#3 \\$colors of class NunoMaduro\\\\Collision\\\\Adapters\\\\Phpunit\\\\Printer constructor expects string, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Actions/AddsDefaults.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#3 \\$colors of class Pest\\\\Logging\\\\TeamCity constructor expects string, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Actions/AddsDefaults.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$filename of function file_exists expects string, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Actions/ValidatesConfiguration.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$filename of method PHPUnit\\\\TextUI\\\\XmlConfiguration\\\\Loader\\:\\:load\\(\\) expects string, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Actions/ValidatesConfiguration.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$loader of class PHPUnit\\\\TextUI\\\\TestRunner constructor expects PHPUnit\\\\Runner\\\\TestSuiteLoader\\|null, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Console/Command.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$testSuite of static method Pest\\\\Actions\\\\AddsTests\\:\\:to\\(\\) expects iterable\\<PHPUnit\\\\Framework\\\\TestCase\\>&PHPUnit\\\\Framework\\\\TestSuite, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Console/Command.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$suffixes of method SebastianBergmann\\\\FileIterator\\\\Facade\\:\\:getFilesAsArray\\(\\) expects array\\|string, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Console/Command.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Datasets.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method Pest\\\\Datasets\\:\\:getDataSetsCombinations\\(\\) has parameter \\$combinations with no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Datasets.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method Pest\\\\Datasets\\:\\:getDataSetsCombinations\\(\\) return type has no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Datasets.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method Pest\\\\Datasets\\:\\:processDatasets\\(\\) return type has no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Datasets.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$key of static method Pest\\\\Datasets\\:\\:getDataSetDescription\\(\\) expects int\\|string, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Datasets.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Each.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$ of callable callable\\(Pest\\\\Expectation\\<TValue\\>\\)\\: mixed expects Pest\\\\Expectation\\<TValue\\>, Pest\\\\Expectation\\<mixed\\> given\\.$#"
|
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Expectation.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$directory of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertDirectoryExists\\(\\) expects string, mixed given\\.$#"
|
message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$each contains generic class Pest\\Expectations\\EachExpectation but does not specify its types\: TValue$#'
|
||||||
|
identifier: missingType.generics
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Expectation.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$directory of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertDirectoryIsReadable\\(\\) expects string, mixed given\\.$#"
|
message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$not contains generic class Pest\\Expectations\\OppositeExpectation but does not specify its types\: TValue$#'
|
||||||
|
identifier: missingType.generics
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Expectation.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$directory of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertDirectoryIsWritable\\(\\) expects string, mixed given\\.$#"
|
message: '#^Parameter \#2 \$newScope of method Closure\:\:bindTo\(\) expects ''static''\|class\-string\|object\|null, string given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Expectation.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$file of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertFileIsReadable\\(\\) expects string, mixed given\\.$#"
|
message: '#^Function expect\(\) should return Pest\\Expectation\<TValue\|null\> but returns Pest\\Expectation\<TValue\|null\>\.$#'
|
||||||
|
identifier: return.type
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Functions.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$file of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertFileIsWritable\\(\\) expects string, mixed given\\.$#"
|
message: '#^Parameter \#1 \$argv of method PHPUnit\\TextUI\\Application\:\:run\(\) expects list\<string\>, array\<int, string\> given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Kernel.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$filename of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertFileExists\\(\\) expects string, mixed given\\.$#"
|
message: '#^Call to an undefined method object&TValue of mixed\:\:__toString\(\)\.$#'
|
||||||
|
identifier: method.notFound
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Mixins/Expectation.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$json of function json_decode expects string, mixed given\\.$#"
|
message: '#^Call to an undefined method object&TValue of mixed\:\:toArray\(\)\.$#'
|
||||||
count: 1
|
identifier: method.notFound
|
||||||
path: src/Expectation.php
|
count: 4
|
||||||
|
path: src/Mixins/Expectation.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$needle of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringContainsString\\(\\) expects string, mixed given\\.$#"
|
message: '#^Call to an undefined method object&TValue of mixed\:\:toSnapshot\(\)\.$#'
|
||||||
|
identifier: method.notFound
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Mixins/Expectation.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$object_or_class of function property_exists expects object\\|string, mixed given\\.$#"
|
message: '#^Call to an undefined method object&TValue of mixed\:\:toString\(\)\.$#'
|
||||||
|
identifier: method.notFound
|
||||||
|
count: 1
|
||||||
|
path: src/Mixins/Expectation.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#'
|
||||||
|
identifier: staticMethod.alreadyNarrowedType
|
||||||
count: 2
|
count: 2
|
||||||
path: src/Expectation.php
|
path: src/Mixins/Expectation.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#2 \\$haystack of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertContains\\(\\) expects iterable, mixed given\\.$#"
|
message: '#^PHPDoc tag @var with type callable\(\)\: bool is not subtype of native type Closure\|null\.$#'
|
||||||
|
identifier: varTag.nativeType
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/PendingCalls/TestCall.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#2 \\$haystack of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertCount\\(\\) expects Countable\\|iterable, mixed given\\.$#"
|
message: '#^Parameter \#4 \$testErroredEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\AfterLastTestMethodErrored\|PHPUnit\\Event\\Test\\BeforeFirstTestMethodErrored\|PHPUnit\\Event\\Test\\Errored\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#2 \\$string of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertMatchesRegularExpression\\(\\) expects string, mixed given\\.$#"
|
message: '#^Parameter \#7 \$testSuiteSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestSuite\\Skipped\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#2 \\$string of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringEndsWith\\(\\) expects string, mixed given\\.$#"
|
message: '#^Parameter \#8 \$testSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\Skipped\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#2 \\$string of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringStartsWith\\(\\) expects string, mixed given\\.$#"
|
message: '#^Parameter \#9 \$testMarkedIncompleteEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\MarkedIncomplete\>, array given\.$#'
|
||||||
|
identifier: argument.type
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#2 \\$value of method Pest\\\\Expectation\\<TValue\\>\\:\\:retrieve\\(\\) expects array\\<string, null\\>\\|object, mixed given\\.$#"
|
message: '#^Property Pest\\Plugins\\Parallel\\Paratest\\WrapperRunner\:\:\$pending \(list\<non\-empty\-string\>\) does not accept array\<int, non\-empty\-string\>\.$#'
|
||||||
|
identifier: assign.propertyType
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Expectation.php
|
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Trying to invoke mixed but it's not a callable\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Expectation.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method Pest\\\\Factories\\\\TestCaseFactory\\:\\:build\\(\\) should return array\\<int, PHPUnit\\\\Framework\\\\TestCase\\> but returns array\\<int, object\\>\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Factories/TestCaseFactory.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Function it\\(\\) should return Pest\\\\PendingObjects\\\\TestCall but returns mixed\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Functions.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$classAndTraits of class Pest\\\\PendingObjects\\\\UsesCall constructor expects array\\<int, string\\>, array\\<int\\|string, string\\> given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Functions.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$value of method Pest\\\\HigherOrderExpectation\\:\\:retrieve\\(\\) expects array\\<string, null\\>\\|object, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/HigherOrderExpectation.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Dead catch \\- ReflectionException is never thrown in the try block\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Logging/JUnit.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$value of method DOMElement\\:\\:setAttribute\\(\\) expects string, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Logging/JUnit.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\$test of method Pest\\\\Logging\\\\JUnit\\:\\:startTest\\(\\) has invalid type Pest\\\\Concerns\\\\Testable\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Logging/JUnit.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\$test of method Pest\\\\Logging\\\\TeamCity\\:\\:endTest\\(\\) has invalid type Pest\\\\Concerns\\\\Testable\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Logging/TeamCity.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\$test of method Pest\\\\Logging\\\\TeamCity\\:\\:startTest\\(\\) has invalid type Pest\\\\Concerns\\\\Testable\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Logging/TeamCity.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Dead catch \\- PHPUnit\\\\Framework\\\\ExpectationFailedException is never thrown in the try block\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/OppositeExpectation.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Property Pest\\\\PendingObjects\\\\UsesCall\\:\\:\\$groups \\(array\\<int, string\\>\\) does not accept array\\<int\\|string, string\\>\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PendingObjects/UsesCall.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Cannot cast mixed to float\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Plugins/Coverage.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Static property Pest\\\\Plugins\\\\Environment\\:\\:\\$instance is unused\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Plugins/Environment.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method Pest\\\\Support\\\\Container\\:\\:get\\(\\) should return object but returns mixed\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Support/Container.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Support/ExceptionTrace.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$array of function key_exists expects array, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Support/ExceptionTrace.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method Pest\\\\Support\\\\HigherOrderCallables\\:\\:expect\\(\\) should return Pest\\\\Expectation\\<TValue\\> but returns Pest\\\\Expectation\\<mixed\\>\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Support/HigherOrderCallables.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$target of method Pest\\\\Support\\\\HigherOrderMessage\\:\\:call\\(\\) expects object, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Support/HigherOrderMessageCollection.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Constant Pest\\\\Support\\\\HigherOrderTapProxy\\:\\:UNDEFINED_PROPERTY is unused\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Support/HigherOrderTapProxy.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Dead catch \\- Throwable is never thrown in the try block\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Support/HigherOrderTapProxy.php
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
services:
|
||||||
|
-
|
||||||
|
class: Pest\PHPStan\HigherOrderExpectationTypeExtension
|
||||||
|
tags:
|
||||||
|
- phpstan.broker.expressionTypeResolverExtension
|
||||||
+4
-25
@@ -1,32 +1,11 @@
|
|||||||
includes:
|
includes:
|
||||||
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
|
||||||
- vendor/ergebnis/phpstan-rules/rules.neon
|
|
||||||
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
|
|
||||||
- phpstan-baseline.neon
|
- phpstan-baseline.neon
|
||||||
|
- phpstan-pest-extension.neon
|
||||||
|
- vendor/pestphp/pest-plugin-phpstan/extension.neon
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
level: max
|
level: 7
|
||||||
paths:
|
paths:
|
||||||
- src
|
- src
|
||||||
|
|
||||||
checkMissingIterableValueType: true
|
reportUnmatchedIgnoredErrors: false
|
||||||
checkGenericClassInNonGenericObjectType: false
|
|
||||||
reportUnmatchedIgnoredErrors: true
|
|
||||||
|
|
||||||
ignoreErrors:
|
|
||||||
- "#type mixed is not subtype of native#"
|
|
||||||
- "#is not allowed to extend#"
|
|
||||||
- "#Language construct eval#"
|
|
||||||
- "# with null as default value#"
|
|
||||||
- "#has parameter \\$closure with default value.#"
|
|
||||||
- "#has parameter \\$description with default value.#"
|
|
||||||
- "#Method Pest\\\\Support\\\\Reflection::getParameterClassName\\(\\) has a nullable return type declaration.#"
|
|
||||||
-
|
|
||||||
message: '#Call to an undefined method PHPUnit\\Framework\\Test::getName\(\)#'
|
|
||||||
path: src/Logging
|
|
||||||
-
|
|
||||||
message: '#is not subtype of native type PHPUnit\\Framework\\Test#'
|
|
||||||
path: src/Logging
|
|
||||||
-
|
|
||||||
message: '#Call to an undefined method PHPUnit\\Framework\\Test::getPrintableTestCaseName\(\)#'
|
|
||||||
path: src/Logging
|
|
||||||
|
|||||||
+26
-11
@@ -1,16 +1,31 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||||
|
beStrictAboutOutputDuringTests="true"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
colors="true"
|
colors="true"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="false"
|
||||||
|
processIsolation="false"
|
||||||
|
stopOnError="false"
|
||||||
|
stopOnFailure="false"
|
||||||
|
backupStaticProperties="false"
|
||||||
>
|
>
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="default">
|
<testsuite name="default">
|
||||||
<directory suffix=".php">./tests</directory>
|
<directory suffix=".php">./tests</directory>
|
||||||
</testsuite>
|
<directory suffix=".php">./tests-external</directory>
|
||||||
</testsuites>
|
<exclude>./tests/.snapshots</exclude>
|
||||||
<coverage processUncoveredFiles="true">
|
<exclude>./tests/Fixtures/Inheritance</exclude>
|
||||||
<include>
|
<exclude>./tests/Fixtures/Suites</exclude>
|
||||||
<directory suffix=".php">./src</directory>
|
<exclude>./tests/Fixtures/Tia</exclude>
|
||||||
</include>
|
</testsuite>
|
||||||
</coverage>
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">./src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"exclude": [
|
||||||
|
"tests/Fixtures/Suites"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"Pint/phpdoc_type_annotations_only": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Pest\Rector\Rules\UseToMatchArrayRector;
|
||||||
|
use Pest\Rector\Set\PestSetList;
|
||||||
|
use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector;
|
||||||
|
use Rector\Config\RectorConfig;
|
||||||
|
use Rector\DeadCode\Rector\ClassMethod\RemoveDuplicatedReturnSelfDocblockRector;
|
||||||
|
use Rector\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector;
|
||||||
|
use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector;
|
||||||
|
use Rector\DeadCode\Rector\ClassMethod\RemoveReturnTagIncompatibleWithNativeTypeRector;
|
||||||
|
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector;
|
||||||
|
use Rector\DeadCode\Rector\ClassMethod\RemoveUselessUnionReturnDocblockRector;
|
||||||
|
use Rector\DeadCode\Rector\Property\RemoveUnusedPrivatePropertyRector;
|
||||||
|
use Rector\TypeDeclaration\Rector\ArrowFunction\AddArrowFunctionReturnTypeRector;
|
||||||
|
use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector;
|
||||||
|
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
|
||||||
|
|
||||||
|
return RectorConfig::configure()
|
||||||
|
->withPaths([
|
||||||
|
__DIR__.'/src',
|
||||||
|
__DIR__.'/tests',
|
||||||
|
])
|
||||||
|
->withSets([
|
||||||
|
PestSetList::CODING_STYLE,
|
||||||
|
])
|
||||||
|
->withSkip([
|
||||||
|
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
|
||||||
|
__DIR__.'/tests/Fixtures/Arch',
|
||||||
|
__DIR__.'/tests/Fixtures/Suites',
|
||||||
|
ReturnNeverTypeRector::class,
|
||||||
|
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
|
||||||
|
NarrowObjectReturnTypeRector::class,
|
||||||
|
RemoveParentDelegatingConstructorRector::class,
|
||||||
|
RemoveDuplicatedReturnSelfDocblockRector::class,
|
||||||
|
RemoveUselessUnionReturnDocblockRector::class,
|
||||||
|
RemoveReturnTagIncompatibleWithNativeTypeRector::class => [
|
||||||
|
__DIR__.'/src/Expectations/HigherOrderExpectation.php',
|
||||||
|
],
|
||||||
|
UseToMatchArrayRector::class,
|
||||||
|
RemoveEmptyClassMethodRector::class => [
|
||||||
|
__DIR__.'/tests',
|
||||||
|
],
|
||||||
|
RemoveUnusedConstructorParamRector::class => [
|
||||||
|
__DIR__.'/tests',
|
||||||
|
],
|
||||||
|
RemoveUnusedPrivatePropertyRector::class => [
|
||||||
|
__DIR__.'/tests',
|
||||||
|
],
|
||||||
|
AddArrowFunctionReturnTypeRector::class => [
|
||||||
|
__DIR__.'/tests',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->withPreparedSets(
|
||||||
|
deadCode: true,
|
||||||
|
codeQuality: true,
|
||||||
|
typeDeclarations: true,
|
||||||
|
privatization: true,
|
||||||
|
earlyReturn: true,
|
||||||
|
)
|
||||||
|
->withPhpSets();
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
---
|
||||||
|
name: pest-testing
|
||||||
|
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, architecture tests, or faster test runs with Test Impact Analysis. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, Tia (--tia), sharding, and all Pest 5 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
author: laravel
|
||||||
|
---
|
||||||
|
@php
|
||||||
|
/** @var \Laravel\Boost\Install\GuidelineAssist $assist */
|
||||||
|
@endphp
|
||||||
|
# Pest Testing 5
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Use `search-docs` for detailed Pest 5 patterns and documentation.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Creating Tests
|
||||||
|
|
||||||
|
All tests must be written using Pest. Use `{{ $assist->artisanCommand('make:test --pest {name}') }}`.
|
||||||
|
|
||||||
|
The `{name}` argument should include only the path and test name, but should not include the test suite.
|
||||||
|
- Incorrect: `{{ $assist->artisanCommand('make:test --pest Feature/SomeFeatureTest') }}` will generate `tests/Feature/Feature/SomeFeatureTest.php`
|
||||||
|
- Correct: `{{ $assist->artisanCommand('make:test --pest SomeControllerTest') }}` will generate `tests/Feature/SomeControllerTest.php`
|
||||||
|
- Incorrect: `{{ $assist->artisanCommand('make:test --pest --unit Unit/SomeServiceTest') }}` will generate `tests/Unit/Unit/SomeServiceTest.php`
|
||||||
|
- Correct: `{{ $assist->artisanCommand('make:test --pest --unit SomeServiceTest') }}` will generate `tests/Unit/SomeServiceTest.php`
|
||||||
|
|
||||||
|
### Test Organization
|
||||||
|
|
||||||
|
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||||
|
- Browser tests: `tests/Browser/` directory.
|
||||||
|
- Do NOT remove tests without approval - these are core application code.
|
||||||
|
|
||||||
|
### Basic Test Structure
|
||||||
|
|
||||||
|
Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`.
|
||||||
|
|
||||||
|
@boostsnippet("Basic Pest Test Example", "php")
|
||||||
|
it('is true', function () {
|
||||||
|
expect(true)->toBeTrue();
|
||||||
|
});
|
||||||
|
@endboostsnippet
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
- Run minimal tests with filter before finalizing: `{{ $assist->artisanCommand('test --compact --filter=testName') }}`.
|
||||||
|
- Run all tests: `{{ $assist->artisanCommand('test --compact') }}`.
|
||||||
|
- Run file: `{{ $assist->artisanCommand('test --compact tests/Feature/ExampleTest.php') }}`.
|
||||||
|
- Run only tests affected by recent changes (Tia): `./vendor/bin/pest --parallel --tia`.
|
||||||
|
|
||||||
|
## Assertions
|
||||||
|
|
||||||
|
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||||
|
|
||||||
|
@boostsnippet("Pest Response Assertion", "php")
|
||||||
|
it('returns all', function () {
|
||||||
|
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||||
|
});
|
||||||
|
@endboostsnippet
|
||||||
|
|
||||||
|
| Use | Instead of |
|
||||||
|
|-----|------------|
|
||||||
|
| `assertSuccessful()` | `assertStatus(200)` |
|
||||||
|
| `assertNotFound()` | `assertStatus(404)` |
|
||||||
|
| `assertForbidden()` | `assertStatus(403)` |
|
||||||
|
|
||||||
|
## Mocking
|
||||||
|
|
||||||
|
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||||
|
|
||||||
|
## Datasets
|
||||||
|
|
||||||
|
Use datasets for repetitive tests (validation rules, etc.):
|
||||||
|
|
||||||
|
@boostsnippet("Pest Dataset Example", "php")
|
||||||
|
it('has emails', function (string $email) {
|
||||||
|
expect($email)->not->toBeEmpty();
|
||||||
|
})->with([
|
||||||
|
'james' => 'james@laravel.com',
|
||||||
|
'taylor' => 'taylor@laravel.com',
|
||||||
|
]);
|
||||||
|
@endboostsnippet
|
||||||
|
|
||||||
|
## Pest 5 Features
|
||||||
|
|
||||||
|
| Feature | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| Tia (Test Impact Analysis) | Rerun only tests affected by recent changes |
|
||||||
|
| Time-Balanced Sharding | Split tests across CI shards by execution time |
|
||||||
|
| New Validation Expectations | `toBeEmail()`, `toBeUlid()`, `toBeIpAddress()`, and more |
|
||||||
|
| Browser Testing | Full integration tests in real browsers |
|
||||||
|
| Smoke Testing | Validate multiple pages quickly |
|
||||||
|
| Visual Regression | Compare screenshots for visual changes |
|
||||||
|
| Architecture Testing | Enforce code conventions |
|
||||||
|
|
||||||
|
### Tia (Test Impact Analysis)
|
||||||
|
|
||||||
|
Tia reruns only tests affected by recent changes and replays cached results for the rest, dramatically reducing suite duration:
|
||||||
|
|
||||||
|
@boostsnippet("Tia Example", "shell")
|
||||||
|
./vendor/bin/pest --parallel --tia
|
||||||
|
@endboostsnippet
|
||||||
|
|
||||||
|
- Replayed tests are not skipped — cached tests store everything they produced, including covered lines and branches.
|
||||||
|
- Detects Laravel, Symfony, Livewire, and Inertia automatically.
|
||||||
|
|
||||||
|
### New Validation Expectations
|
||||||
|
|
||||||
|
Pest 5 ships eight new validation matchers, all supporting `.not` negation:
|
||||||
|
|
||||||
|
@boostsnippet("Pest 5 Validation Expectations", "php")
|
||||||
|
expect('nuno@pestphp.com')->toBeEmail();
|
||||||
|
expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid();
|
||||||
|
expect('192.168.1.1')->toBeIpAddress();
|
||||||
|
expect('00:1a:2b:3c:4d:5e')->toBeMacAddress();
|
||||||
|
expect('example.com')->toBeHostname();
|
||||||
|
expect('example.co.uk')->toBeDomain();
|
||||||
|
expect('Zm9vYmFy')->toBeBase64();
|
||||||
|
expect('deadbeef')->toBeHexadecimal();
|
||||||
|
@endboostsnippet
|
||||||
|
|
||||||
|
### Time-Balanced Sharding
|
||||||
|
|
||||||
|
Distribute tests across CI shards by execution time rather than count:
|
||||||
|
|
||||||
|
@boostsnippet("Pest Sharding Example", "shell")
|
||||||
|
./vendor/bin/pest --update-shards
|
||||||
|
./vendor/bin/pest --shard=1/4
|
||||||
|
@endboostsnippet
|
||||||
|
|
||||||
|
Commit `tests/.pest/shards.json` to the repository so CI shards stay consistent.
|
||||||
|
|
||||||
|
### Browser Test Example
|
||||||
|
|
||||||
|
Browser tests run in real browsers for full integration testing:
|
||||||
|
|
||||||
|
- Browser tests live in `tests/Browser/`.
|
||||||
|
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||||
|
- Use `RefreshDatabase` for clean state per test.
|
||||||
|
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||||
|
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||||
|
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||||
|
- Switch color schemes (light/dark mode) when appropriate.
|
||||||
|
- Take screenshots or pause tests for debugging.
|
||||||
|
|
||||||
|
@boostsnippet("Pest Browser Test Example", "php")
|
||||||
|
it('may reset the password', function () {
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
$page = visit('/sign-in');
|
||||||
|
|
||||||
|
$page->assertSee('Sign In')
|
||||||
|
->assertNoJavaScriptErrors()
|
||||||
|
->click('Forgot Password?')
|
||||||
|
->fill('email', 'nuno@laravel.com')
|
||||||
|
->click('Send Reset Link')
|
||||||
|
->assertSee('We have emailed your password reset link!');
|
||||||
|
|
||||||
|
Notification::assertSent(ResetPassword::class);
|
||||||
|
});
|
||||||
|
@endboostsnippet
|
||||||
|
|
||||||
|
### Smoke Testing
|
||||||
|
|
||||||
|
Quickly validate multiple pages have no JavaScript errors:
|
||||||
|
|
||||||
|
@boostsnippet("Pest Smoke Testing Example", "php")
|
||||||
|
$pages = visit(['/', '/about', '/contact']);
|
||||||
|
|
||||||
|
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
||||||
|
@endboostsnippet
|
||||||
|
|
||||||
|
### Visual Regression Testing
|
||||||
|
|
||||||
|
Capture and compare screenshots to detect visual changes.
|
||||||
|
|
||||||
|
### Architecture Testing
|
||||||
|
|
||||||
|
@boostsnippet("Architecture Test Example", "php")
|
||||||
|
arch('controllers')
|
||||||
|
->expect('App\Http\Controllers')
|
||||||
|
->toExtendNothing()
|
||||||
|
->toHaveSuffix('Controller');
|
||||||
|
@endboostsnippet
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||||
|
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||||
|
- Forgetting datasets for repetitive validation tests
|
||||||
|
- Deleting tests without approval
|
||||||
|
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||||
|
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/** @var string $type */
|
||||||
|
/** @var string $content */
|
||||||
|
[$bgBadgeColor, $bgBadgeText] = match ($type) {
|
||||||
|
'INFO' => ['blue', 'INFO'],
|
||||||
|
'ERROR' => ['red', 'ERROR'],
|
||||||
|
'WARN' => ['yellow', 'WARN'],
|
||||||
|
'SUCCESS' => ['green', 'SUCCESS'],
|
||||||
|
};
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="my-1">
|
||||||
|
<span class="ml-2 px-1 bg-<?php echo $bgBadgeColor ?> font-bold"><?php echo htmlspecialchars($bgBadgeText) ?></span>
|
||||||
|
<span class="ml-1">
|
||||||
|
<?php echo htmlspecialchars($content) ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<div></div>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="flex mx-2 max-w-150">
|
||||||
|
<span>
|
||||||
|
<?php echo htmlspecialchars($left) ?>
|
||||||
|
</span>
|
||||||
|
<span class="flex-1 content-repeat-[.] text-gray ml-1"></span>
|
||||||
|
<?php if ($right !== '') { ?>
|
||||||
|
<span class="ml-1 text-gray">
|
||||||
|
<?php echo htmlspecialchars($right) ?>
|
||||||
|
</span>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<div class="mx-2 mb-1">
|
||||||
|
<p>
|
||||||
|
<span>Using the <span class="text-yellow font-bold">visit()</span> function requires the Pest Plugin Browser to be installed.</span>
|
||||||
|
|
||||||
|
<span class="ml-1 text-yellow font-bold">Run:</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-gray mr-1">- </span>
|
||||||
|
<span>composer require pestphp/pest-plugin-browser:^5.0 --dev</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-gray mr-1">- </span>
|
||||||
|
<span>npm install playwright@latest</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-gray mr-1">- </span>
|
||||||
|
<span>npx playwright install</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="mx-2">
|
||||||
|
<span class="text-yellow font-bold">USAGE:</span><span class="ml-1">pest</span><span class="ml-1 text-gray"><?php echo htmlspecialchars('<file>') ?> [options]</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="my-1 mx-2">
|
||||||
|
<span>Pest Testing Framework</span><span class="ml-1 text-blue font-bold"><?php echo htmlspecialchars($version) ?></span><span>.</span>
|
||||||
|
</div>
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Actions;
|
|
||||||
|
|
||||||
use NunoMaduro\Collision\Adapters\Phpunit\Printer;
|
|
||||||
use Pest\Logging\JUnit;
|
|
||||||
use Pest\Logging\TeamCity;
|
|
||||||
use PHPUnit\TextUI\DefaultResultPrinter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class AddsDefaults
|
|
||||||
{
|
|
||||||
private const PRINTER = 'printer';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds default arguments to the given `arguments` array.
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $arguments
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public static function to(array $arguments): array
|
|
||||||
{
|
|
||||||
if (!array_key_exists(self::PRINTER, $arguments)) {
|
|
||||||
$arguments[self::PRINTER] = new Printer(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($arguments[self::PRINTER] === \PHPUnit\Util\Log\TeamCity::class) {
|
|
||||||
$arguments[self::PRINTER] = new TeamCity(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load our junit logger instead.
|
|
||||||
if (array_key_exists('junitLogfile', $arguments)) {
|
|
||||||
$arguments['listeners'][] = new JUnit(
|
|
||||||
$arguments['junitLogfile']
|
|
||||||
);
|
|
||||||
unset($arguments['junitLogfile']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $arguments;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Actions;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use PHPUnit\Framework\TestSuite;
|
|
||||||
use PHPUnit\Framework\WarningTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class AddsTests
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Adds tests to the given test suite.
|
|
||||||
*
|
|
||||||
* @param TestSuite<\PHPUnit\Framework\TestCase> $testSuite
|
|
||||||
*/
|
|
||||||
public static function to(TestSuite $testSuite, \Pest\TestSuite $pestTestSuite): void
|
|
||||||
{
|
|
||||||
self::removeTestClosureWarnings($testSuite);
|
|
||||||
|
|
||||||
$testSuites = [];
|
|
||||||
$pestTestSuite->tests->build($pestTestSuite, function (TestCase $testCase) use (&$testSuites): void {
|
|
||||||
$testCaseClass = get_class($testCase);
|
|
||||||
if (!array_key_exists($testCaseClass, $testSuites)) {
|
|
||||||
$testSuites[$testCaseClass] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$testSuites[$testCaseClass][] = $testCase;
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach ($testSuites as $testCaseName => $testCases) {
|
|
||||||
$testTestSuite = new TestSuite($testCaseName);
|
|
||||||
$testTestSuite->setTests([]);
|
|
||||||
foreach ($testCases as $testCase) {
|
|
||||||
$testTestSuite->addTest($testCase, $testCase->getGroups());
|
|
||||||
}
|
|
||||||
$testSuite->addTestSuite($testTestSuite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TestSuite<\PHPUnit\Framework\TestCase> $testSuite
|
|
||||||
*/
|
|
||||||
private static function removeTestClosureWarnings(TestSuite $testSuite): void
|
|
||||||
{
|
|
||||||
$tests = $testSuite->tests();
|
|
||||||
|
|
||||||
foreach ($tests as $key => $test) {
|
|
||||||
if ($test instanceof TestSuite) {
|
|
||||||
self::removeTestClosureWarnings($test);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($test instanceof WarningTestCase) {
|
|
||||||
unset($tests[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$testSuite->setTests($tests);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Actions;
|
|
||||||
|
|
||||||
use Pest\Contracts\Plugins\AddsOutput;
|
|
||||||
use Pest\Contracts\Plugins\HandlesArguments;
|
|
||||||
use Pest\Plugin\Loader;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class InteractsWithPlugins
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Transform the input arguments by passing it to the relevant plugins.
|
|
||||||
*
|
|
||||||
* @param array<int, string> $argv
|
|
||||||
*
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
public static function handleArguments(array $argv): array
|
|
||||||
{
|
|
||||||
$plugins = Loader::getPlugins(HandlesArguments::class);
|
|
||||||
|
|
||||||
/** @var HandlesArguments $plugin */
|
|
||||||
foreach ($plugins as $plugin) {
|
|
||||||
$argv = $plugin->handleArguments($argv);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $argv;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides an opportunity for any plugins that want
|
|
||||||
* to provide additional output after test execution.
|
|
||||||
*/
|
|
||||||
public static function addOutput(int $result): int
|
|
||||||
{
|
|
||||||
$plugins = Loader::getPlugins(AddsOutput::class);
|
|
||||||
|
|
||||||
/** @var AddsOutput $plugin */
|
|
||||||
foreach ($plugins as $plugin) {
|
|
||||||
$result = $plugin->addOutput($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Actions;
|
|
||||||
|
|
||||||
use Pest\Support\Str;
|
|
||||||
|
|
||||||
use function Pest\testDirectory;
|
|
||||||
|
|
||||||
use PHPUnit\Util\FileLoader;
|
|
||||||
use RecursiveDirectoryIterator;
|
|
||||||
use RecursiveIteratorIterator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class LoadStructure
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The Pest convention.
|
|
||||||
*
|
|
||||||
* @var array<int, string>
|
|
||||||
*/
|
|
||||||
private const STRUCTURE = [
|
|
||||||
'Expectations.php',
|
|
||||||
'Datasets.php',
|
|
||||||
'Helpers.php',
|
|
||||||
'Pest.php',
|
|
||||||
'Datasets',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the configuration in the given `configuration`.
|
|
||||||
*/
|
|
||||||
public static function in(string $rootPath): void
|
|
||||||
{
|
|
||||||
$testsPath = $rootPath . DIRECTORY_SEPARATOR . testDirectory();
|
|
||||||
|
|
||||||
$load = function ($filename): bool {
|
|
||||||
return file_exists($filename) && (bool) FileLoader::checkAndLoad($filename);
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (self::STRUCTURE as $filename) {
|
|
||||||
$filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename);
|
|
||||||
|
|
||||||
if (!file_exists($filename)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_dir($filename)) {
|
|
||||||
$directory = new RecursiveDirectoryIterator($filename);
|
|
||||||
$iterator = new RecursiveIteratorIterator($directory);
|
|
||||||
foreach ($iterator as $file) {
|
|
||||||
/* @phpstan-ignore-next-line */
|
|
||||||
$filename = $file->__toString();
|
|
||||||
if (Str::endsWith($filename, '.php') && file_exists($filename)) {
|
|
||||||
require_once $filename;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$load($filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Actions;
|
|
||||||
|
|
||||||
use Pest\Exceptions\AttributeNotSupportedYet;
|
|
||||||
use Pest\Exceptions\FileOrFolderNotFound;
|
|
||||||
use PHPUnit\TextUI\XmlConfiguration\Loader;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class ValidatesConfiguration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private const CONFIGURATION_KEY = 'configuration';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the configuration in the given `configuration`.
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $arguments
|
|
||||||
*/
|
|
||||||
public static function in($arguments): void
|
|
||||||
{
|
|
||||||
if (!array_key_exists(self::CONFIGURATION_KEY, $arguments) || !file_exists($arguments[self::CONFIGURATION_KEY])) {
|
|
||||||
throw new FileOrFolderNotFound('phpunit.xml');
|
|
||||||
}
|
|
||||||
|
|
||||||
$configuration = (new Loader())->load($arguments[self::CONFIGURATION_KEY])->phpunit();
|
|
||||||
|
|
||||||
if ($configuration->processIsolation()) {
|
|
||||||
throw new AttributeNotSupportedYet('processIsolation', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Actions;
|
|
||||||
|
|
||||||
use Pest\Exceptions\FileOrFolderNotFound;
|
|
||||||
use Pest\TestSuite;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class ValidatesEnvironment
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The need files on the root path.
|
|
||||||
*
|
|
||||||
* @var array<int, string>
|
|
||||||
*/
|
|
||||||
private const NEEDED_FILES = [
|
|
||||||
'composer.json',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the environment.
|
|
||||||
*/
|
|
||||||
public static function in(TestSuite $testSuite): void
|
|
||||||
{
|
|
||||||
$rootPath = $testSuite->rootPath;
|
|
||||||
|
|
||||||
$exists = function ($neededFile) use ($rootPath): bool {
|
|
||||||
return file_exists(sprintf('%s%s%s', $rootPath, DIRECTORY_SEPARATOR, $neededFile));
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (self::NEEDED_FILES as $neededFile) {
|
|
||||||
if (!$exists($neededFile)) {
|
|
||||||
throw new FileOrFolderNotFound($neededFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
use Pest\Arch\Contracts\ArchExpectation;
|
||||||
|
use Pest\Expectation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
abstract class AbstractPreset // @pest-arch-ignore-line
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int, Expectation<mixed>|ArchExpectation>
|
||||||
|
*/
|
||||||
|
protected array $expectations = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $userNamespaces
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $userNamespaces,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
abstract public function execute(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callable(Expectation<string>): ArchExpectation ...$callbacks
|
||||||
|
*/
|
||||||
|
final public function eachUserNamespace(callable ...$callbacks): void
|
||||||
|
{
|
||||||
|
foreach ($this->userNamespaces as $namespace) {
|
||||||
|
foreach ($callbacks as $callback) {
|
||||||
|
$this->expectations[] = $callback(expect($namespace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final public function flush(): void
|
||||||
|
{
|
||||||
|
$this->expectations = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Arch\Contracts\ArchExpectation;
|
||||||
|
use Pest\Expectation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Custom extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
$this->expectations = ($this->execute)($this->userNamespaces);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Laravel extends AbstractPreset
|
||||||
|
{
|
||||||
|
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')
|
||||||
|
->ignoring('App\Features\Concerns');
|
||||||
|
|
||||||
|
$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')
|
||||||
|
->classes()
|
||||||
|
->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')
|
||||||
|
->classes()
|
||||||
|
->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')
|
||||||
|
->classes()
|
||||||
|
->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', 'App\Providers']);
|
||||||
|
|
||||||
|
$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');
|
||||||
|
|
||||||
|
$this->expectations[] = expect('App\Attributes')
|
||||||
|
->classes()
|
||||||
|
->toImplement('Illuminate\Contracts\Container\ContextualAttribute')
|
||||||
|
->toHaveAttribute('Attribute')
|
||||||
|
->toHaveMethod('resolve');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Php extends AbstractPreset
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
use Pest\Arch\Contracts\ArchExpectation;
|
||||||
|
use Pest\Expectation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Relaxed extends AbstractPreset
|
||||||
|
{
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Security extends AbstractPreset
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
'mb_parse_str',
|
||||||
|
'dl',
|
||||||
|
'assert',
|
||||||
|
])->not->toBeUsed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\ArchPresets;
|
||||||
|
|
||||||
|
use Pest\Arch\Contracts\ArchExpectation;
|
||||||
|
use Pest\Expectation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Strict extends AbstractPreset
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Bootstrappers;
|
||||||
|
|
||||||
|
use Pest\Contracts\Bootstrapper;
|
||||||
|
use PHPUnit\Util\ExcludeList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BootExcludeList implements Bootstrapper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int, non-empty-string>
|
||||||
|
*/
|
||||||
|
private const array EXCLUDE_LIST = [
|
||||||
|
'bin',
|
||||||
|
'overrides',
|
||||||
|
'resources',
|
||||||
|
'src',
|
||||||
|
'stubs',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$baseDirectory = dirname(__DIR__, 2);
|
||||||
|
|
||||||
|
foreach (self::EXCLUDE_LIST as $directory) {
|
||||||
|
ExcludeList::addDirectory($baseDirectory.DIRECTORY_SEPARATOR.$directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Bootstrappers;
|
||||||
|
|
||||||
|
use Pest\Contracts\Bootstrapper;
|
||||||
|
use Pest\Exceptions\FatalException;
|
||||||
|
use Pest\Support\DatasetInfo;
|
||||||
|
use Pest\Support\Str;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use RecursiveDirectoryIterator;
|
||||||
|
use RecursiveIteratorIterator;
|
||||||
|
use SebastianBergmann\FileIterator\Facade as PhpUnitFileIterator;
|
||||||
|
|
||||||
|
use function Pest\testDirectory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BootFiles implements Bootstrapper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private const array STRUCTURE = [
|
||||||
|
'Expectations',
|
||||||
|
'Expectations.php',
|
||||||
|
'Helpers',
|
||||||
|
'Helpers.php',
|
||||||
|
'Pest.php',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$rootPath = TestSuite::getInstance()->rootPath;
|
||||||
|
$testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory();
|
||||||
|
|
||||||
|
if (! is_dir($testsPath)) {
|
||||||
|
throw new FatalException(sprintf('The test directory [%s] does not exist.', $testsPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::STRUCTURE as $filename) {
|
||||||
|
$filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename);
|
||||||
|
|
||||||
|
if (! file_exists($filename)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_dir($filename)) {
|
||||||
|
$directory = new RecursiveDirectoryIterator($filename);
|
||||||
|
$iterator = new RecursiveIteratorIterator($directory);
|
||||||
|
/** @var \DirectoryIterator $file */
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
$this->load($file->__toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->load($filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bootDatasets($testsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function load(string $filename): void
|
||||||
|
{
|
||||||
|
if (! Str::endsWith($filename, '.php')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (! file_exists($filename)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
include_once $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bootDatasets(string $testsPath): void
|
||||||
|
{
|
||||||
|
assert($testsPath !== '');
|
||||||
|
|
||||||
|
$files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (DatasetInfo::isADatasetsFile($file) || DatasetInfo::isInsideADatasetsDirectory($file)) {
|
||||||
|
$this->load($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Bootstrappers;
|
||||||
|
|
||||||
|
use Pest\Contracts\Bootstrapper;
|
||||||
|
use Pest\KernelDump;
|
||||||
|
use Pest\Support\Container;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class BootKernelDump implements Bootstrapper
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private OutputInterface $output,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
Container::getInstance()->add(KernelDump::class, $kernelDump = new KernelDump(
|
||||||
|
$this->output,
|
||||||
|
));
|
||||||
|
|
||||||
|
$kernelDump->enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Bootstrappers;
|
||||||
|
|
||||||
|
use Pest\Contracts\Bootstrapper;
|
||||||
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BootOverrides implements Bootstrapper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
public const array FILES = [
|
||||||
|
'ParaTest/WrapperRunner/ProgressPrinterOutput.php',
|
||||||
|
'Runner/Filter/NameFilterIterator.php',
|
||||||
|
'Runner/TestRunHistory/DefaultTestRunHistory.php',
|
||||||
|
'Runner/TestSuiteLoader.php',
|
||||||
|
'Runner/TestSuiteSorter.php',
|
||||||
|
'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
|
||||||
|
'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
|
||||||
|
'TextUI/TestSuiteFilterProcessor.php',
|
||||||
|
'Event/Value/ThrowableBuilder.php',
|
||||||
|
'Logging/JUnit/JunitXmlLogger.php',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
foreach (self::FILES as $file) {
|
||||||
|
$file = __DIR__."/../../overrides/$file";
|
||||||
|
|
||||||
|
if (! file_exists($file)) {
|
||||||
|
throw ShouldNotHappen::fromMessage(sprintf('File [%s] does not exist.', $file));
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Bootstrappers;
|
||||||
|
|
||||||
|
use Pest\Contracts\Bootstrapper;
|
||||||
|
use PHPUnit\TextUI\Configuration\Builder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BootPhpUnitConfiguration implements Bootstrapper
|
||||||
|
{
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
(new Builder)->build(['pest']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Bootstrappers;
|
||||||
|
|
||||||
|
use Pest\Contracts\Bootstrapper;
|
||||||
|
use Pest\Subscribers;
|
||||||
|
use Pest\Support\Container;
|
||||||
|
use PHPUnit\Event;
|
||||||
|
use PHPUnit\Event\Subscriber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class BootSubscribers implements Bootstrapper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int, class-string<Subscriber>>
|
||||||
|
*/
|
||||||
|
private const array SUBSCRIBERS = [
|
||||||
|
Subscribers\EnsureConfigurationIsAvailable::class,
|
||||||
|
Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
|
||||||
|
Subscribers\EnsureKernelDumpIsFlushed::class,
|
||||||
|
Subscribers\EnsureTeamCityEnabled::class,
|
||||||
|
Subscribers\EnsureTiaIsRunningPestTestsOnly::class,
|
||||||
|
Subscribers\EnsureTiaStarts::class,
|
||||||
|
Subscribers\EnsureTiaEnds::class,
|
||||||
|
Subscribers\EnsureTiaResultsAreCollected::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnPassed::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnFailed::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnErrored::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnSkipped::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnIncomplete::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnRisky::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnNoticeTriggered::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnPhpNoticeTriggered::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnDeprecationTriggered::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnPhpDeprecationTriggered::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnWarningTriggered::class,
|
||||||
|
Subscribers\EnsureTiaResultIsRecordedOnPhpWarningTriggered::class,
|
||||||
|
Subscribers\EnsureTiaAssertionsAreRecordedOnFinished::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private Container $container,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
foreach (self::SUBSCRIBERS as $subscriber) {
|
||||||
|
$instance = $this->container->get($subscriber);
|
||||||
|
|
||||||
|
assert($instance instanceof Subscriber);
|
||||||
|
|
||||||
|
Event\Facade::instance()->registerSubscriber($instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Bootstrappers;
|
||||||
|
|
||||||
|
use Pest\Contracts\Bootstrapper;
|
||||||
|
use Pest\Support\View;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class BootView implements Bootstrapper
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private OutputInterface $output
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
View::renderUsing($this->output);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
private static ?OutputInterface $output = null;
|
||||||
|
|
||||||
|
public static function setOutput(OutputInterface $output): void
|
||||||
|
{
|
||||||
|
self::$output = $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,13 +14,10 @@ trait Expectable
|
|||||||
/**
|
/**
|
||||||
* @template TValue
|
* @template TValue
|
||||||
*
|
*
|
||||||
* Creates a new expectation.
|
* @param TValue $value
|
||||||
*
|
|
||||||
* @param TValue $value
|
|
||||||
*
|
|
||||||
* @return Expectation<TValue>
|
* @return Expectation<TValue>
|
||||||
*/
|
*/
|
||||||
public function expect($value): Expectation
|
public function expect(mixed $value): Expectation
|
||||||
{
|
{
|
||||||
return new Expectation($value);
|
return new Expectation($value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,51 +4,30 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest\Concerns;
|
namespace Pest\Concerns;
|
||||||
|
|
||||||
use BadMethodCallException;
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
*
|
||||||
|
* @template T of object
|
||||||
*/
|
*/
|
||||||
trait Extendable
|
trait Extendable
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var array<string, Closure>
|
* @var array<string, Closure>
|
||||||
*/
|
*/
|
||||||
private static $extends = [];
|
private static array $extends = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a custom extend.
|
* @param-closure-this T $extend
|
||||||
*/
|
*/
|
||||||
public static function extend(string $name, Closure $extend): void
|
public function extend(string $name, Closure $extend): void
|
||||||
{
|
{
|
||||||
static::$extends[$name] = $extend;
|
static::$extends[$name] = $extend;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if extend is registered.
|
|
||||||
*/
|
|
||||||
public static function hasExtend(string $name): bool
|
public static function hasExtend(string $name): bool
|
||||||
{
|
{
|
||||||
return array_key_exists($name, static::$extends);
|
return array_key_exists($name, static::$extends);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Dynamically handle calls to the class.
|
|
||||||
*
|
|
||||||
* @param array<int, mixed> $parameters
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function __call(string $method, array $parameters)
|
|
||||||
{
|
|
||||||
if (!static::hasExtend($method)) {
|
|
||||||
throw new BadMethodCallException("$method is not a callable method name.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var Closure $extend */
|
|
||||||
$extend = static::$extends[$method]->bindTo($this, static::class);
|
|
||||||
|
|
||||||
return $extend(...$parameters);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Concerns;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
trait Pipeable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, array<Closure(Closure, mixed ...$arguments): void>>
|
||||||
|
*/
|
||||||
|
private static array $pipes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array<Closure(Closure, mixed ...$arguments): void>>
|
||||||
|
*/
|
||||||
|
private static array $interceptors = [];
|
||||||
|
|
||||||
|
public function pipe(string $name, Closure $pipe): void
|
||||||
|
{
|
||||||
|
self::$pipes[$name][] = $pipe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|Closure(mixed $value, mixed ...$arguments):bool $filter
|
||||||
|
*/
|
||||||
|
public function intercept(string $name, string|Closure $filter, Closure $handler): void
|
||||||
|
{
|
||||||
|
if (is_string($filter)) {
|
||||||
|
$filter = fn ($value): bool => $value instanceof $filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$interceptors[$name][] = $handler;
|
||||||
|
|
||||||
|
$this->pipe($name, function ($next, ...$arguments) use ($handler, $filter): void {
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
|
if ($filter($this->value, ...$arguments)) {
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
$handler->bindTo($this, $this::class)(...$arguments);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, Closure>
|
||||||
|
*/
|
||||||
|
private function pipes(string $name, object $context, string $scope): array
|
||||||
|
{
|
||||||
|
return array_map(fn (Closure $pipe): Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,19 +7,17 @@ namespace Pest\Concerns;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
trait RetrievesValues
|
trait Retrievable
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
* @template TRetrievableValue
|
||||||
* @template TRetrievableValue
|
* @template TRetrievableValue
|
||||||
*
|
*
|
||||||
* Safely retrieve the value at the given key from an object or array.
|
* @param array<string, TRetrievableValue>|object $value
|
||||||
*
|
* @param TRetrievableValue|null $default
|
||||||
* @param array<string, TRetrievableValue>|object $value
|
|
||||||
* @param TRetrievableValue|null $default
|
|
||||||
*
|
|
||||||
* @return TRetrievableValue|null
|
* @return TRetrievableValue|null
|
||||||
*/
|
*/
|
||||||
private function retrieve(string $key, $value, $default = null)
|
private function retrieve(string $key, mixed $value, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
if (is_array($value)) {
|
if (is_array($value)) {
|
||||||
return $value[$key] ?? $default;
|
return $value[$key] ?? $default;
|
||||||
+419
-185
@@ -5,213 +5,160 @@ declare(strict_types=1);
|
|||||||
namespace Pest\Concerns;
|
namespace Pest\Concerns;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Pest\Exceptions\DatasetArgumentsMismatch;
|
||||||
|
use Pest\Panic;
|
||||||
|
use Pest\Plugins\Tia;
|
||||||
|
use Pest\Plugins\Tia\Collectors;
|
||||||
|
use Pest\Plugins\Tia\Enums\ReplayType;
|
||||||
|
use Pest\Plugins\Tia\Recorder;
|
||||||
|
use Pest\Preset;
|
||||||
use Pest\Support\ChainableClosure;
|
use Pest\Support\ChainableClosure;
|
||||||
|
use Pest\Support\Container;
|
||||||
use Pest\Support\ExceptionTrace;
|
use Pest\Support\ExceptionTrace;
|
||||||
|
use Pest\Support\Reflection;
|
||||||
|
use Pest\Support\Shell;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use PHPUnit\Framework\ExecutionOrderDependency;
|
use PHPUnit\Framework\AssertionFailedError;
|
||||||
|
use PHPUnit\Framework\Attributes\PostCondition;
|
||||||
use function sprintf;
|
use PHPUnit\Framework\IncompleteTest;
|
||||||
|
use PHPUnit\Framework\SkippedTest;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use PHPUnit\Framework\TestCase\ExceptionExpectation;
|
||||||
|
use PHPUnit\Framework\TestCase\OutputBuffer;
|
||||||
|
use ReflectionException;
|
||||||
|
use ReflectionFunction;
|
||||||
|
use ReflectionParameter;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To avoid inheritance conflicts, all the fields related
|
|
||||||
* to Pest only will be prefixed by double underscore.
|
|
||||||
*
|
|
||||||
* @internal
|
* @internal
|
||||||
|
*
|
||||||
|
* @mixin TestCase
|
||||||
*/
|
*/
|
||||||
trait Testable
|
trait Testable
|
||||||
{
|
{
|
||||||
/**
|
private string $__description;
|
||||||
* The test case description. Contains the first
|
|
||||||
* argument of global functions like `it` and `test`.
|
private static string $__latestDescription;
|
||||||
*
|
|
||||||
* @var string
|
private static array $__latestAssignees = [];
|
||||||
*/
|
|
||||||
private $__description;
|
private static array $__latestNotes = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the test closure function.
|
* @var array<int, int>
|
||||||
*
|
|
||||||
* @var Closure
|
|
||||||
*/
|
*/
|
||||||
private $__test;
|
private static array $__latestIssues = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds a global/shared beforeEach ("set up") closure if one has been
|
* @var array<int, int>
|
||||||
* defined.
|
|
||||||
*
|
|
||||||
* @var Closure|null
|
|
||||||
*/
|
*/
|
||||||
private $beforeEach = null;
|
private static array $__latestPrs = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds a global/shared afterEach ("tear down") closure if one has been
|
* @var array<int, string>
|
||||||
* defined.
|
|
||||||
*
|
|
||||||
* @var Closure|null
|
|
||||||
*/
|
*/
|
||||||
private $afterEach = null;
|
public array $__describing = [];
|
||||||
|
|
||||||
/**
|
public bool $__ran = false;
|
||||||
* Holds a global/shared beforeAll ("set up before") closure if one has been
|
|
||||||
* defined.
|
|
||||||
*
|
|
||||||
* @var Closure|null
|
|
||||||
*/
|
|
||||||
private static $beforeAll = null;
|
|
||||||
|
|
||||||
/**
|
private ReplayType $__replay = ReplayType::None;
|
||||||
* Holds a global/shared afterAll ("tear down after") closure if one has
|
|
||||||
* been defined.
|
|
||||||
*
|
|
||||||
* @var Closure|null
|
|
||||||
*/
|
|
||||||
private static $afterAll = null;
|
|
||||||
|
|
||||||
/**
|
private int $__replayAssertions = 0;
|
||||||
* Creates a new instance of the test case.
|
|
||||||
*/
|
private Closure $__test;
|
||||||
public function __construct(Closure $test, string $description, array $data)
|
|
||||||
|
private ?Closure $__beforeEach = null;
|
||||||
|
|
||||||
|
private ?Closure $__afterEach = null;
|
||||||
|
|
||||||
|
private static ?Closure $__beforeAll = null;
|
||||||
|
|
||||||
|
private static ?Closure $__afterAll = null;
|
||||||
|
|
||||||
|
private array $__snapshotChanges = [];
|
||||||
|
|
||||||
|
public static function flush(): void
|
||||||
{
|
{
|
||||||
$this->__test = $test;
|
self::$__beforeAll = null;
|
||||||
$this->__description = $description;
|
self::$__afterAll = null;
|
||||||
self::$beforeAll = null;
|
|
||||||
self::$afterAll = null;
|
|
||||||
|
|
||||||
parent::__construct('__test', $data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function note(array|string $note): self
|
||||||
* Adds the groups to the current test case.
|
|
||||||
*/
|
|
||||||
public function addGroups(array $groups): void
|
|
||||||
{
|
{
|
||||||
$groups = array_unique(array_merge($this->getGroups(), $groups));
|
$note = is_array($note) ? $note : [$note];
|
||||||
|
|
||||||
$this->setGroups($groups);
|
self::$__latestNotes = array_merge(self::$__latestNotes, $note);
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function __addBeforeAll(?Closure $hook): void
|
||||||
* Add dependencies to the test case and map them to instances of ExecutionOrderDependency.
|
|
||||||
*/
|
|
||||||
public function addDependencies(array $tests): void
|
|
||||||
{
|
{
|
||||||
$className = get_class($this);
|
if (! $hook instanceof Closure) {
|
||||||
|
|
||||||
$tests = array_map(function (string $test) use ($className): ExecutionOrderDependency {
|
|
||||||
if (strpos($test, '::') === false) {
|
|
||||||
$test = "{$className}::{$test}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ExecutionOrderDependency($test, null, '');
|
|
||||||
}, $tests);
|
|
||||||
|
|
||||||
$this->setDependencies($tests);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a shared/"global" before all test hook that will execute **before**
|
|
||||||
* the test defined `beforeAll` hook(s).
|
|
||||||
*/
|
|
||||||
public function addBeforeAll(?Closure $hook): void
|
|
||||||
{
|
|
||||||
if (!$hook) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self::$beforeAll = (self::$beforeAll instanceof Closure)
|
self::$__beforeAll = (self::$__beforeAll instanceof Closure)
|
||||||
? ChainableClosure::fromStatic(self::$beforeAll, $hook)
|
? ChainableClosure::boundStatically(self::$__beforeAll, $hook)
|
||||||
: $hook;
|
: $hook;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function __addAfterAll(?Closure $hook): void
|
||||||
* Add a shared/"global" after all test hook that will execute **before**
|
|
||||||
* the test defined `afterAll` hook(s).
|
|
||||||
*/
|
|
||||||
public function addAfterAll(?Closure $hook): void
|
|
||||||
{
|
{
|
||||||
if (!$hook) {
|
if (! $hook instanceof Closure) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self::$afterAll = (self::$afterAll instanceof Closure)
|
self::$__afterAll = (self::$__afterAll instanceof Closure)
|
||||||
? ChainableClosure::fromStatic(self::$afterAll, $hook)
|
? ChainableClosure::boundStatically(self::$__afterAll, $hook)
|
||||||
: $hook;
|
: $hook;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function __addBeforeEach(?Closure $hook): void
|
||||||
* Add a shared/"global" before each test hook that will execute **before**
|
|
||||||
* the test defined `beforeEach` hook.
|
|
||||||
*/
|
|
||||||
public function addBeforeEach(?Closure $hook): void
|
|
||||||
{
|
{
|
||||||
$this->addHook('beforeEach', $hook);
|
$this->__addHook('__beforeEach', $hook);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function __addAfterEach(?Closure $hook): void
|
||||||
* Add a shared/"global" after each test hook that will execute **before**
|
|
||||||
* the test defined `afterEach` hook.
|
|
||||||
*/
|
|
||||||
public function addAfterEach(?Closure $hook): void
|
|
||||||
{
|
{
|
||||||
$this->addHook('afterEach', $hook);
|
$this->__addHook('__afterEach', $hook);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function __addHook(string $property, ?Closure $hook): void
|
||||||
* Add a shared/global hook and compose them if more than one is passed.
|
|
||||||
*/
|
|
||||||
private function addHook(string $property, ?Closure $hook): void
|
|
||||||
{
|
{
|
||||||
if (!$hook) {
|
if (! $hook instanceof Closure) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->{$property} = ($this->{$property} instanceof Closure)
|
$this->{$property} = ($this->{$property} instanceof Closure)
|
||||||
? ChainableClosure::from($this->{$property}, $hook)
|
? ChainableClosure::bound($this->{$property}, $hook)
|
||||||
: $hook;
|
: $hook;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the test case name. Note that, in Pest
|
|
||||||
* we ignore withDataset argument as the description
|
|
||||||
* already contains the dataset description.
|
|
||||||
*/
|
|
||||||
public function getName(bool $withDataSet = true): string
|
|
||||||
{
|
|
||||||
return $this->__description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function __getFileName(): string
|
|
||||||
{
|
|
||||||
return self::$__filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called before the first test of this test class is run.
|
|
||||||
*/
|
|
||||||
public static function setUpBeforeClass(): void
|
public static function setUpBeforeClass(): void
|
||||||
{
|
{
|
||||||
parent::setUpBeforeClass();
|
parent::setUpBeforeClass();
|
||||||
|
|
||||||
$beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename);
|
$beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename);
|
||||||
|
|
||||||
if (self::$beforeAll instanceof Closure) {
|
if (self::$__beforeAll instanceof Closure) {
|
||||||
$beforeAll = ChainableClosure::fromStatic(self::$beforeAll, $beforeAll);
|
$beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
call_user_func(Closure::bind($beforeAll, null, self::class));
|
try {
|
||||||
|
call_user_func(Closure::bind($beforeAll, null, self::class));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Panic::with($e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called after the last test of this test class is run.
|
|
||||||
*/
|
|
||||||
public static function tearDownAfterClass(): void
|
public static function tearDownAfterClass(): void
|
||||||
{
|
{
|
||||||
$afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename);
|
$afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename);
|
||||||
|
|
||||||
if (self::$afterAll instanceof Closure) {
|
if (self::$__afterAll instanceof Closure) {
|
||||||
$afterAll = ChainableClosure::fromStatic(self::$afterAll, $afterAll);
|
$afterAll = ChainableClosure::boundStatically(self::$__afterAll, $afterAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
call_user_func(Closure::bind($afterAll, null, self::class));
|
call_user_func(Closure::bind($afterAll, null, self::class));
|
||||||
@@ -219,92 +166,379 @@ trait Testable
|
|||||||
parent::tearDownAfterClass();
|
parent::tearDownAfterClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected function setUp(...$arguments): void
|
||||||
* Gets executed before the test.
|
|
||||||
*/
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
{
|
||||||
TestSuite::getInstance()->test = $this;
|
TestSuite::getInstance()->test = $this;
|
||||||
|
|
||||||
|
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
||||||
|
|
||||||
|
$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) {
|
||||||
|
$matches = [];
|
||||||
|
preg_match('/\((.*?)\)/', $description, $matches);
|
||||||
|
|
||||||
|
if (count($matches) > 1) {
|
||||||
|
if (str_contains($description, 'with '.$matches[0].' /')) {
|
||||||
|
$description = str_replace('with '.$matches[0].' /', '', $description);
|
||||||
|
} else {
|
||||||
|
$description = str_replace('with '.$matches[0], '', $description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$description .= ' @ repetition '.($matches[1].' of '.$method->repetitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->__description = self::$__latestDescription = $description;
|
||||||
|
self::$__latestAssignees = $method->assignees;
|
||||||
|
self::$__latestNotes = $method->notes;
|
||||||
|
self::$__latestIssues = $method->issues;
|
||||||
|
self::$__latestPrs = $method->prs;
|
||||||
|
|
||||||
|
/** @var Tia $tia */
|
||||||
|
$tia = Container::getInstance()->get(Tia::class);
|
||||||
|
$status = $tia->getStatus(self::$__filename, $this->valueObjectForEvents()->id());
|
||||||
|
$replay = ReplayType::fromStatus($status);
|
||||||
|
|
||||||
|
if ($replay !== ReplayType::None) {
|
||||||
|
assert($status !== null);
|
||||||
|
|
||||||
|
$this->__replay = $replay;
|
||||||
|
|
||||||
|
match ($replay) {
|
||||||
|
ReplayType::Pass, ReplayType::Risky => $this->__beginReplay($replay, $tia),
|
||||||
|
ReplayType::Skipped => $this->markTestSkipped($status->message()),
|
||||||
|
ReplayType::Incomplete => $this->markTestIncomplete($status->message()),
|
||||||
|
ReplayType::Failure => throw new AssertionFailedError($status->message() ?: 'Cached failure'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$recorder = Container::getInstance()->get(Recorder::class);
|
||||||
|
assert($recorder instanceof Recorder);
|
||||||
|
|
||||||
|
if ($recorder->isActive()) {
|
||||||
|
$recorder->beginTest($this::class, $this->name(), self::$__filename);
|
||||||
|
}
|
||||||
|
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename);
|
Collectors::armAll($recorder);
|
||||||
|
|
||||||
if ($this->beforeEach instanceof Closure) {
|
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename)[1];
|
||||||
$beforeEach = ChainableClosure::from($this->beforeEach, $beforeEach);
|
|
||||||
|
if ($this->__beforeEach instanceof Closure) {
|
||||||
|
$beforeEach = ChainableClosure::bound($this->__beforeEach, $beforeEach);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->__callClosure($beforeEach, func_get_args());
|
$this->__callClosure($beforeEach, $arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function __beginReplay(ReplayType $replay, Tia $tia): void
|
||||||
* Gets executed after the test.
|
|
||||||
*/
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
{
|
||||||
|
$this->__replay = $replay;
|
||||||
|
$this->__replayAssertions = $tia->getAssertionCount($this->valueObjectForEvents()->id());
|
||||||
|
$this->__ran = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __initializeTestCase(): void
|
||||||
|
{
|
||||||
|
if (isset($this->__test)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $this->name();
|
||||||
|
$test = TestSuite::getInstance()->tests->get(self::$__filename);
|
||||||
|
|
||||||
|
if ($test->hasMethod($name)) {
|
||||||
|
$method = $test->getMethod($name);
|
||||||
|
$this->__description = self::$__latestDescription = $method->description;
|
||||||
|
self::$__latestAssignees = $method->assignees;
|
||||||
|
self::$__latestNotes = $method->notes;
|
||||||
|
self::$__latestIssues = $method->issues;
|
||||||
|
self::$__latestPrs = $method->prs;
|
||||||
|
$this->__describing = $method->describing;
|
||||||
|
$this->__test = $method->getClosure();
|
||||||
|
|
||||||
|
$method->setUp($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(...$arguments): void
|
||||||
|
{
|
||||||
|
if ($this->__replay !== ReplayType::None) {
|
||||||
|
TestSuite::getInstance()->test = null;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
|
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
|
||||||
|
|
||||||
if ($this->afterEach instanceof Closure) {
|
if ($this->__afterEach instanceof Closure) {
|
||||||
$afterEach = ChainableClosure::from($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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the test case as string.
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function toString(): string
|
private function __runTest(Closure $closure, ...$args): mixed
|
||||||
{
|
{
|
||||||
return sprintf(
|
if ($this->__replay === ReplayType::Pass || $this->__replay === ReplayType::Risky) {
|
||||||
'%s::%s',
|
if ($this->__replay === ReplayType::Pass && $this->__replayAssertions === 0) {
|
||||||
self::$__filename,
|
$this->expectNotToPerformAssertions();
|
||||||
$this->__description
|
}
|
||||||
|
|
||||||
|
$this->addToAssertionCount($this->__replayAssertions);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments = $this->__resolveTestArguments($args);
|
||||||
|
$this->__ensureDatasetArgumentNameAndNumberMatches($arguments);
|
||||||
|
|
||||||
|
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
||||||
|
|
||||||
|
if ($method->flakyTries === null) {
|
||||||
|
return $this->__callClosure($closure, $arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastException = null;
|
||||||
|
$initialProperties = get_object_vars($this);
|
||||||
|
|
||||||
|
for ($attempt = 1; $attempt <= $method->flakyTries; $attempt++) {
|
||||||
|
try {
|
||||||
|
return $this->__callClosure($closure, $arguments);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($e instanceof SkippedTest
|
||||||
|
|| $e instanceof IncompleteTest
|
||||||
|
|| $this->__isExpectedException($e)) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastException = $e;
|
||||||
|
|
||||||
|
if ($attempt < $method->flakyTries) {
|
||||||
|
if ($this->__snapshotChanges !== []) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->tearDown();
|
||||||
|
|
||||||
|
Closure::bind(fn (): array => $this->mockObjects = [], $this, TestCase::class)();
|
||||||
|
|
||||||
|
foreach (array_keys(array_diff_key(get_object_vars($this), $initialProperties)) as $property) {
|
||||||
|
unset($this->{$property});
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputBuffer = Closure::bind(fn () => $this->outputBuffer, $this, TestCase::class)();
|
||||||
|
|
||||||
|
if ($outputBuffer->hasExpectation()) {
|
||||||
|
ob_clean();
|
||||||
|
|
||||||
|
Closure::bind(function (): void {
|
||||||
|
$this->expectedString = null;
|
||||||
|
$this->expectedRegularExpression = null;
|
||||||
|
}, $outputBuffer, OutputBuffer::class)();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $lastException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __isExpectedException(Throwable $e): bool
|
||||||
|
{
|
||||||
|
$expectation = Closure::bind(fn () => $this->exceptionExpectation, $this, TestCase::class)();
|
||||||
|
|
||||||
|
$read = fn (string $property): mixed => Closure::bind(fn () => $this->{$property}, $expectation, ExceptionExpectation::class)();
|
||||||
|
|
||||||
|
$expectedClass = $read('expectedException');
|
||||||
|
|
||||||
|
if ($expectedClass !== null) {
|
||||||
|
return $e instanceof $expectedClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedMessage = $read('expectedMessage');
|
||||||
|
|
||||||
|
if ($expectedMessage !== null) {
|
||||||
|
return str_contains($e->getMessage(), (string) $expectedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedMessageRegex = $read('expectedMessageRegularExpression');
|
||||||
|
|
||||||
|
if ($expectedMessageRegex !== null) {
|
||||||
|
return preg_match($expectedMessageRegex, $e->getMessage()) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedCode = $read('expectedCode');
|
||||||
|
|
||||||
|
if ($expectedCode !== null) {
|
||||||
|
return $e->getCode() === $expectedCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
private function __resolveTestArguments(array $arguments): array
|
||||||
|
{
|
||||||
|
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
||||||
|
|
||||||
|
if ($method->repetitions > 1) {
|
||||||
|
$firstArgument = array_shift($arguments);
|
||||||
|
$arguments[] = $firstArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
$underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure');
|
||||||
|
$testParameterTypesByName = Reflection::getFunctionArguments($underlyingTest);
|
||||||
|
$testParameterTypes = array_values($testParameterTypesByName);
|
||||||
|
|
||||||
|
if (count($arguments) !== 1) {
|
||||||
|
foreach ($arguments as $argumentIndex => $argumentValue) {
|
||||||
|
if (! $argumentValue instanceof Closure) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parameterType = is_string($argumentIndex)
|
||||||
|
? $testParameterTypesByName[$argumentIndex]
|
||||||
|
: $testParameterTypes[$argumentIndex];
|
||||||
|
|
||||||
|
if (in_array($parameterType, [Closure::class, 'callable', 'mixed'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments[$argumentIndex] = $this->__callClosure($argumentValue, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! isset($arguments[0]) || ! $arguments[0] instanceof Closure) {
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($testParameterTypes[0]) && in_array($testParameterTypes[0], [Closure::class, 'callable'])) {
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
$boundDatasetResult = $this->__callClosure($arguments[0], []);
|
||||||
|
if (count($testParameterTypes) === 1) {
|
||||||
|
return [$boundDatasetResult];
|
||||||
|
}
|
||||||
|
if (! is_array($boundDatasetResult)) {
|
||||||
|
return [$boundDatasetResult];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $boundDatasetResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ReflectionException
|
||||||
|
* @throws DatasetArgumentsMismatch
|
||||||
|
*/
|
||||||
|
private function __ensureDatasetArgumentNameAndNumberMatches(array $arguments): void
|
||||||
|
{
|
||||||
|
if ($arguments === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure');
|
||||||
|
$testReflection = new ReflectionFunction($underlyingTest);
|
||||||
|
$requiredParametersCount = $testReflection->getNumberOfRequiredParameters();
|
||||||
|
$suppliedParametersCount = count($arguments);
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($testParameterNames[0]) && $suppliedParametersCount >= $requiredParametersCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DatasetArgumentsMismatch($requiredParametersCount, $suppliedParametersCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the test.
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*
|
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function __test()
|
private function __callClosure(Closure $closure, array $arguments): mixed
|
||||||
{
|
{
|
||||||
return $this->__callClosure($this->__test, $this->resolveTestArguments(func_get_args()));
|
return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function preset(): Preset
|
||||||
* Resolve the passed arguments. Any Closures will be bound to the testcase and resolved.
|
|
||||||
*
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
private function resolveTestArguments(array $arguments): array
|
|
||||||
{
|
{
|
||||||
return array_map(function ($data) {
|
return new Preset;
|
||||||
return $data instanceof Closure ? $this->__callClosure($data, []) : $data;
|
|
||||||
}, $arguments);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[PostCondition]
|
||||||
* @return mixed
|
protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void
|
||||||
*
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
private function __callClosure(Closure $closure, array $arguments)
|
|
||||||
{
|
{
|
||||||
return ExceptionTrace::ensure(function () use ($closure, $arguments) {
|
if (count($this->__snapshotChanges) === 0) {
|
||||||
return call_user_func_array(Closure::bind($closure, $this, get_class($this)), $arguments);
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
$this->markTestIncomplete(implode('. ', $this->__snapshotChanges));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPrintableTestCaseName(): string
|
public static function getPrintableTestCaseName(): string
|
||||||
{
|
{
|
||||||
return ltrim(self::class, 'P\\');
|
return preg_replace('/P\\\/', '', self::class, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrintableTestCaseMethodName(): string
|
||||||
|
{
|
||||||
|
return $this->__description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getLatestPrintableTestCaseMethodName(): string
|
||||||
|
{
|
||||||
|
return self::$__latestDescription ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPrintableContext(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'assignees' => self::$__latestAssignees,
|
||||||
|
'issues' => self::$__latestIssues,
|
||||||
|
'prs' => self::$__latestPrs,
|
||||||
|
'notes' => self::$__latestNotes,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shell(): void
|
||||||
|
{
|
||||||
|
Shell::open();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest;
|
||||||
|
|
||||||
|
use Pest\PendingCalls\BeforeEachCall;
|
||||||
|
use Pest\PendingCalls\UsesCall;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @mixin UsesCall
|
||||||
|
*/
|
||||||
|
final readonly class Configuration
|
||||||
|
{
|
||||||
|
private string $filename;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $filename,
|
||||||
|
) {
|
||||||
|
$this->filename = str_ends_with($filename, DIRECTORY_SEPARATOR.'Pest.php') ? dirname($filename) : $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function in(string ...$targets): UsesCall
|
||||||
|
{
|
||||||
|
return new UsesCall($this->filename, [])->in(...$targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function extend(string ...$classAndTraits): UsesCall
|
||||||
|
{
|
||||||
|
return new UsesCall(
|
||||||
|
$this->filename,
|
||||||
|
array_values($classAndTraits)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function extends(string ...$classAndTraits): UsesCall
|
||||||
|
{
|
||||||
|
return $this->extend(...$classAndTraits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function group(string ...$groups): UsesCall
|
||||||
|
{
|
||||||
|
return new UsesCall($this->filename, [])->group(...$groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function only(): void
|
||||||
|
{
|
||||||
|
new BeforeEachCall(TestSuite::getInstance(), $this->filename)->only();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function use(string ...$classAndTraits): UsesCall
|
||||||
|
{
|
||||||
|
return $this->extend(...$classAndTraits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uses(string ...$classAndTraits): UsesCall
|
||||||
|
{
|
||||||
|
return $this->extends(...$classAndTraits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function printer(): Configuration\Printer
|
||||||
|
{
|
||||||
|
return new Configuration\Printer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function presets(): Configuration\Presets
|
||||||
|
{
|
||||||
|
return new Configuration\Presets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function project(): Configuration\Project
|
||||||
|
{
|
||||||
|
return Configuration\Project::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function browser(): Browser\Configuration
|
||||||
|
{
|
||||||
|
return new Browser\Configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function evals(): Evals\Configuration // @phpstan-ignore-line
|
||||||
|
{
|
||||||
|
return new Evals\Configuration; // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tia(): Plugins\Tia\Configuration
|
||||||
|
{
|
||||||
|
return new Plugins\Tia\Configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<array-key, mixed> $arguments
|
||||||
|
*/
|
||||||
|
public function __call(string $name, array $arguments): mixed
|
||||||
|
{
|
||||||
|
return $this->uses()->$name(...$arguments); // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Configuration;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Preset;
|
||||||
|
|
||||||
|
final class Presets
|
||||||
|
{
|
||||||
|
public function custom(string $name, Closure $execute): void
|
||||||
|
{
|
||||||
|
Preset::custom($name, $execute);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Configuration;
|
||||||
|
|
||||||
|
use NunoMaduro\Collision\Adapters\Phpunit\Printers\DefaultPrinter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class Printer
|
||||||
|
{
|
||||||
|
public function compact(): self
|
||||||
|
{
|
||||||
|
DefaultPrinter::compact(true);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Project
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public string $assignees = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public string $issues = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public string $prs = '';
|
||||||
|
|
||||||
|
private static ?self $instance = null;
|
||||||
|
|
||||||
|
public static function getInstance(): self
|
||||||
|
{
|
||||||
|
return self::$instance ??= new self;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function gitlab(string $project, string $hostname = 'gitlab.com'): self
|
||||||
|
{
|
||||||
|
$hostname = parse_url($hostname, PHP_URL_HOST) ?? $hostname;
|
||||||
|
|
||||||
|
$this->issues = "https://{$hostname}/{$project}/-/work_items/%s";
|
||||||
|
$this->prs = "https://{$hostname}/{$project}/-/merge_requests/%s";
|
||||||
|
|
||||||
|
$this->assignees = "https://{$hostname}/%s";
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function custom(string $issues, string $prs, string $assignees): self
|
||||||
|
{
|
||||||
|
$this->issues = $issues;
|
||||||
|
$this->prs = $prs;
|
||||||
|
|
||||||
|
$this->assignees = $assignees;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Console;
|
|
||||||
|
|
||||||
use function is_dir;
|
|
||||||
|
|
||||||
use Pest\Actions\AddsDefaults;
|
|
||||||
use Pest\Actions\AddsTests;
|
|
||||||
use Pest\Actions\InteractsWithPlugins;
|
|
||||||
use Pest\Actions\LoadStructure;
|
|
||||||
use Pest\Actions\ValidatesConfiguration;
|
|
||||||
use Pest\Plugins\Version;
|
|
||||||
use Pest\Support\Container;
|
|
||||||
use Pest\TestSuite;
|
|
||||||
use PHPUnit\Framework\TestSuite as BaseTestSuite;
|
|
||||||
use PHPUnit\TextUI\Command as BaseCommand;
|
|
||||||
use PHPUnit\TextUI\TestRunner;
|
|
||||||
use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class Command extends BaseCommand
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Holds the current testing suite.
|
|
||||||
*
|
|
||||||
* @var TestSuite
|
|
||||||
*/
|
|
||||||
private $testSuite;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the current console output.
|
|
||||||
*
|
|
||||||
* @var OutputInterface
|
|
||||||
*/
|
|
||||||
private $output;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new instance of the command class.
|
|
||||||
*/
|
|
||||||
public function __construct(TestSuite $testSuite, OutputInterface $output)
|
|
||||||
{
|
|
||||||
$this->testSuite = $testSuite;
|
|
||||||
$this->output = $output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*
|
|
||||||
* @phpstan-ignore-next-line
|
|
||||||
*
|
|
||||||
* @param array<int, string> $argv
|
|
||||||
*/
|
|
||||||
protected function handleArguments(array $argv): void
|
|
||||||
{
|
|
||||||
$argv = InteractsWithPlugins::handleArguments($argv);
|
|
||||||
|
|
||||||
parent::handleArguments($argv);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Let's validate the configuration. Making
|
|
||||||
* sure all options are yet supported by Pest.
|
|
||||||
*/
|
|
||||||
ValidatesConfiguration::in($this->arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new PHPUnit test runner.
|
|
||||||
*/
|
|
||||||
protected function createRunner(): TestRunner
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* First, let's add the defaults we use on `pest`. Those
|
|
||||||
* are the printer class, and others that may be appear.
|
|
||||||
*/
|
|
||||||
$this->arguments = AddsDefaults::to($this->arguments);
|
|
||||||
|
|
||||||
$testRunner = new TestRunner($this->arguments['loader']);
|
|
||||||
$testSuite = $this->arguments['test'];
|
|
||||||
|
|
||||||
if (is_string($testSuite)) {
|
|
||||||
if (is_dir($testSuite)) {
|
|
||||||
/** @var string[] $files */
|
|
||||||
$files = (new FileIteratorFacade())->getFilesAsArray(
|
|
||||||
$testSuite,
|
|
||||||
$this->arguments['testSuffixes']
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$files = [$testSuite];
|
|
||||||
}
|
|
||||||
|
|
||||||
$testSuite = new BaseTestSuite($testSuite);
|
|
||||||
|
|
||||||
$testSuite->addTestFiles($files);
|
|
||||||
|
|
||||||
$this->arguments['test'] = $testSuite;
|
|
||||||
}
|
|
||||||
|
|
||||||
AddsTests::to($testSuite, $this->testSuite);
|
|
||||||
|
|
||||||
return $testRunner;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*
|
|
||||||
* @phpstan-ignore-next-line
|
|
||||||
*
|
|
||||||
* @param array<int, string> $argv
|
|
||||||
*/
|
|
||||||
public function run(array $argv, bool $exit = true): int
|
|
||||||
{
|
|
||||||
LoadStructure::in($this->testSuite->rootPath);
|
|
||||||
|
|
||||||
$result = parent::run($argv, false);
|
|
||||||
$result = InteractsWithPlugins::addOutput($result);
|
|
||||||
|
|
||||||
exit($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function showHelp(): void
|
|
||||||
{
|
|
||||||
/** @var Version $version */
|
|
||||||
$version = Container::getInstance()->get(Version::class);
|
|
||||||
$version->handleArguments(['--version']);
|
|
||||||
parent::showHelp();
|
|
||||||
|
|
||||||
(new Help($this->output))();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,10 +9,12 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Help
|
final readonly class Help
|
||||||
{
|
{
|
||||||
/** @var array<int, string> */
|
/**
|
||||||
private const HELP_MESSAGES = [
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private const array HELP_MESSAGES = [
|
||||||
'<comment>Pest Options:</comment>',
|
'<comment>Pest Options:</comment>',
|
||||||
' <info>--init</info> Initialise a standard Pest configuration',
|
' <info>--init</info> Initialise a standard Pest configuration',
|
||||||
' <info>--coverage</info> Enable coverage and output to standard output',
|
' <info>--coverage</info> Enable coverage and output to standard output',
|
||||||
@@ -20,12 +22,9 @@ final class Help
|
|||||||
' <info>--group=<fg=cyan><name></></info> Only runs tests from the specified group(s)',
|
' <info>--group=<fg=cyan><name></></info> Only runs tests from the specified group(s)',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @var OutputInterface */
|
public function __construct(private OutputInterface $output)
|
||||||
private $output;
|
|
||||||
|
|
||||||
public function __construct(OutputInterface $output)
|
|
||||||
{
|
{
|
||||||
$this->output = $output;
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __invoke(): void
|
public function __invoke(): void
|
||||||
|
|||||||
+50
-34
@@ -4,63 +4,79 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest\Console;
|
namespace Pest\Console;
|
||||||
|
|
||||||
|
use Pest\Bootstrappers\BootView;
|
||||||
|
use Pest\Support\View;
|
||||||
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
|
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
|
||||||
use Symfony\Component\Console\Input\ArrayInput;
|
use Symfony\Component\Console\Input\ArrayInput;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Thanks
|
final readonly class Thanks
|
||||||
{
|
{
|
||||||
/** @var array<int, string> */
|
/**
|
||||||
private const FUNDING_MESSAGES = [
|
* @var array<string, string>
|
||||||
'',
|
*/
|
||||||
' - Star or contribute to Pest:',
|
private const array FUNDING_MESSAGES = [
|
||||||
' <options=bold>https://github.com/pestphp/pest</>',
|
'Star' => 'https://github.com/pestphp/pest',
|
||||||
' - Tweet something about Pest on Twitter:',
|
'YouTube' => 'https://youtube.com/@nunomaduro',
|
||||||
' <options=bold>https://twitter.com/pestphp</>',
|
'TikTok' => 'https://tiktok.com/@enunomaduro',
|
||||||
' - Sponsor the creator:',
|
'Twitch' => 'https://twitch.tv/nunomaduro',
|
||||||
' <options=bold>https://github.com/sponsors/nunomaduro</>',
|
'LinkedIn' => 'https://linkedin.com/in/nunomaduro',
|
||||||
|
'Instagram' => 'https://instagram.com/enunomaduro',
|
||||||
|
'X' => 'https://x.com/enunomaduro',
|
||||||
|
'Sponsor' => 'https://github.com/sponsors/nunomaduro',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @var OutputInterface */
|
public function __construct(
|
||||||
private $output;
|
private InputInterface $input,
|
||||||
|
private OutputInterface $output
|
||||||
public function __construct(OutputInterface $output)
|
) {
|
||||||
{
|
//
|
||||||
$this->output = $output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Asks the user to support Pest.
|
|
||||||
*/
|
|
||||||
public function __invoke(): void
|
public function __invoke(): void
|
||||||
{
|
{
|
||||||
$wantsToSupport = (new SymfonyQuestionHelper())->ask(
|
$bootstrapper = new BootView($this->output);
|
||||||
new ArrayInput([]),
|
$bootstrapper->boot();
|
||||||
$this->output,
|
|
||||||
new ConfirmationQuestion(
|
$wantsToSupport = false;
|
||||||
'Can you quickly <options=bold>star our GitHub repository</>? 🙏🏻',
|
|
||||||
true,
|
if (getenv('PEST_NO_SUPPORT') !== 'true' && $this->input->isInteractive()) {
|
||||||
)
|
$wantsToSupport = (new SymfonyQuestionHelper)->ask(
|
||||||
);
|
new ArrayInput([]),
|
||||||
|
$this->output,
|
||||||
|
new ConfirmationQuestion(
|
||||||
|
' <options=bold>Wanna show Pest some love by starring it on GitHub?</>',
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
View::render('components.new-line');
|
||||||
|
|
||||||
|
foreach (self::FUNDING_MESSAGES as $message => $link) {
|
||||||
|
View::render('components.two-column-detail', [
|
||||||
|
'left' => $message,
|
||||||
|
'right' => $link,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
View::render('components.new-line');
|
||||||
|
}
|
||||||
|
|
||||||
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (self::FUNDING_MESSAGES as $message) {
|
|
||||||
$this->output->writeln($message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface ArchPreset {}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface Bootstrapper
|
||||||
|
{
|
||||||
|
public function boot(): void;
|
||||||
|
}
|
||||||
@@ -4,18 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest\Contracts;
|
namespace Pest\Contracts;
|
||||||
|
|
||||||
if (interface_exists(\NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName::class)) {
|
use NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName as BaseHasPrintableTestCaseName;
|
||||||
/**
|
|
||||||
* @internal
|
/**
|
||||||
*/
|
* @internal
|
||||||
interface HasPrintableTestCaseName extends \NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName
|
*/
|
||||||
{
|
interface HasPrintableTestCaseName extends BaseHasPrintableTestCaseName
|
||||||
}
|
{
|
||||||
} else {
|
//
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
interface HasPrintableTestCaseName
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface Panicable
|
||||||
|
{
|
||||||
|
public function render(OutputInterface $output): void;
|
||||||
|
|
||||||
|
public function exitCode(): int;
|
||||||
|
}
|
||||||
@@ -9,8 +9,5 @@ namespace Pest\Contracts\Plugins;
|
|||||||
*/
|
*/
|
||||||
interface AddsOutput
|
interface AddsOutput
|
||||||
{
|
{
|
||||||
/**
|
public function addOutput(int $exitCode): int;
|
||||||
* Allows to add custom output after the test suite was executed.
|
|
||||||
*/
|
|
||||||
public function addOutput(int $testReturnCode): int;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts\Plugins;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface Bootable
|
||||||
|
{
|
||||||
|
public function boot(): void;
|
||||||
|
}
|
||||||
@@ -10,11 +10,8 @@ namespace Pest\Contracts\Plugins;
|
|||||||
interface HandlesArguments
|
interface HandlesArguments
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Allows to handle custom command line arguments.
|
* @param array<int, string> $arguments
|
||||||
*
|
* @return array<int, string>
|
||||||
* @param array<int, string> $arguments
|
|
||||||
*
|
|
||||||
* @return array<int, string> the updated list of arguments
|
|
||||||
*/
|
*/
|
||||||
public function handleArguments(array $arguments): array;
|
public function handleArguments(array $arguments): array;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts\Plugins;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface HandlesOriginalArguments
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
*/
|
||||||
|
public function handleOriginalArguments(array $arguments): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts\Plugins;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface Terminable
|
||||||
|
{
|
||||||
|
public function terminate(): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface Restarter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
*/
|
||||||
|
public function maybeRestart(string $projectRoot, array $arguments): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts;
|
||||||
|
|
||||||
|
interface TestCaseFilter
|
||||||
|
{
|
||||||
|
public function accept(string $testCaseFilename): bool;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts;
|
||||||
|
|
||||||
|
use Pest\Factories\TestCaseMethodFactory;
|
||||||
|
|
||||||
|
interface TestCaseMethodFilter
|
||||||
|
{
|
||||||
|
public function accept(TestCaseMethodFactory $factory): bool;
|
||||||
|
}
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Pest\Exceptions\DatasetAlreadyExist;
|
|
||||||
use Pest\Exceptions\DatasetDoesNotExist;
|
|
||||||
use SebastianBergmann\Exporter\Exporter;
|
|
||||||
use Traversable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class Datasets
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Holds the datasets.
|
|
||||||
*
|
|
||||||
* @var array<int|string, Closure|iterable<int|string, mixed>>
|
|
||||||
*/
|
|
||||||
private static $datasets = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the given.
|
|
||||||
*
|
|
||||||
* @param Closure|iterable<int|string, mixed> $data
|
|
||||||
*/
|
|
||||||
public static function set(string $name, $data): void
|
|
||||||
{
|
|
||||||
if (array_key_exists($name, self::$datasets)) {
|
|
||||||
throw new DatasetAlreadyExist($name);
|
|
||||||
}
|
|
||||||
|
|
||||||
self::$datasets[$name] = $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Closure|iterable<int|string, mixed>
|
|
||||||
*/
|
|
||||||
public static function get(string $name)
|
|
||||||
{
|
|
||||||
if (!array_key_exists($name, self::$datasets)) {
|
|
||||||
throw new DatasetDoesNotExist($name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::$datasets[$name];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the current dataset to an array value.
|
|
||||||
*
|
|
||||||
* @param array<Closure|iterable<int|string, mixed>|string> $datasets
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public static function resolve(string $description, array $datasets): array
|
|
||||||
{
|
|
||||||
/* @phpstan-ignore-next-line */
|
|
||||||
if (empty($datasets)) {
|
|
||||||
return [$description => []];
|
|
||||||
}
|
|
||||||
|
|
||||||
$datasets = self::processDatasets($datasets);
|
|
||||||
|
|
||||||
$datasetCombinations = self::getDataSetsCombinations($datasets);
|
|
||||||
|
|
||||||
$dataSetDescriptions = [];
|
|
||||||
$dataSetValues = [];
|
|
||||||
|
|
||||||
foreach ($datasetCombinations as $datasetCombination) {
|
|
||||||
$partialDescriptions = [];
|
|
||||||
$values = [];
|
|
||||||
|
|
||||||
foreach ($datasetCombination as $dataset_data) {
|
|
||||||
$partialDescriptions[] = $dataset_data['label'];
|
|
||||||
$values = array_merge($values, $dataset_data['values']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dataSetDescriptions[] = $description . ' with ' . implode(' / ', $partialDescriptions);
|
|
||||||
$dataSetValues[] = $values;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (array_count_values($dataSetDescriptions) as $descriptionToCheck => $count) {
|
|
||||||
if ($count > 1) {
|
|
||||||
$index = 1;
|
|
||||||
foreach ($dataSetDescriptions as $i => $dataSetDescription) {
|
|
||||||
if ($dataSetDescription === $descriptionToCheck) {
|
|
||||||
$dataSetDescriptions[$i] .= sprintf(' #%d', $index++);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$namedData = [];
|
|
||||||
foreach ($dataSetDescriptions as $i => $dataSetDescription) {
|
|
||||||
$namedData[$dataSetDescription] = $dataSetValues[$i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $namedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<Closure|iterable<int|string, mixed>|string> $datasets
|
|
||||||
*
|
|
||||||
* @return array<array>
|
|
||||||
*/
|
|
||||||
private static function processDatasets(array $datasets): array
|
|
||||||
{
|
|
||||||
$processedDatasets = [];
|
|
||||||
|
|
||||||
foreach ($datasets as $index => $data) {
|
|
||||||
$processedDataset = [];
|
|
||||||
|
|
||||||
if (is_string($data)) {
|
|
||||||
$datasets[$index] = self::get($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_callable($datasets[$index])) {
|
|
||||||
$datasets[$index] = call_user_func($datasets[$index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($datasets[$index] instanceof Traversable) {
|
|
||||||
$datasets[$index] = iterator_to_array($datasets[$index], false);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($datasets[$index] as $key => $values) {
|
|
||||||
$values = is_array($values) ? $values : [$values];
|
|
||||||
$processedDataset[] = [
|
|
||||||
'label' => self::getDataSetDescription($key, $values),
|
|
||||||
'values' => $values,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$processedDatasets[] = $processedDataset;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $processedDatasets;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<array> $combinations
|
|
||||||
*
|
|
||||||
* @return array<array>
|
|
||||||
*/
|
|
||||||
private static function getDataSetsCombinations(array $combinations): array
|
|
||||||
{
|
|
||||||
$result = [[]];
|
|
||||||
foreach ($combinations as $index => $values) {
|
|
||||||
$tmp = [];
|
|
||||||
foreach ($result as $resultItem) {
|
|
||||||
foreach ($values as $value) {
|
|
||||||
$tmp[] = array_merge($resultItem, [$index => $value]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$result = $tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int|string $key
|
|
||||||
* @param array<int, mixed> $data
|
|
||||||
*/
|
|
||||||
private static function getDataSetDescription($key, array $data): string
|
|
||||||
{
|
|
||||||
$exporter = new Exporter();
|
|
||||||
|
|
||||||
if (is_int($key)) {
|
|
||||||
return \sprintf('(%s)', $exporter->shortenedRecursiveExport($data));
|
|
||||||
}
|
|
||||||
|
|
||||||
return \sprintf('data set "%s"', $key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @mixin Expectation
|
|
||||||
*/
|
|
||||||
final class Each
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var Expectation
|
|
||||||
*/
|
|
||||||
private $original;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
private $opposite = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an expectation on each item of the iterable "value".
|
|
||||||
*/
|
|
||||||
public function __construct(Expectation $original)
|
|
||||||
{
|
|
||||||
$this->original = $original;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new expectation.
|
|
||||||
*
|
|
||||||
* @param mixed $value
|
|
||||||
*/
|
|
||||||
public function and($value): Expectation
|
|
||||||
{
|
|
||||||
return $this->original->and($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the opposite expectation for the value.
|
|
||||||
*/
|
|
||||||
public function not(): Each
|
|
||||||
{
|
|
||||||
$this->opposite = true;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dynamically calls methods on the class with the given arguments on each item.
|
|
||||||
*
|
|
||||||
* @param array<int|string, mixed> $arguments
|
|
||||||
*/
|
|
||||||
public function __call(string $name, array $arguments): Each
|
|
||||||
{
|
|
||||||
foreach ($this->original->value as $item) {
|
|
||||||
/* @phpstan-ignore-next-line */
|
|
||||||
$this->opposite ? expect($item)->not()->$name(...$arguments) : expect($item)->$name(...$arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->opposite = false;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dynamically calls methods on the class without any arguments on each item.
|
|
||||||
*/
|
|
||||||
public function __get(string $name): Each
|
|
||||||
{
|
|
||||||
/* @phpstan-ignore-next-line */
|
|
||||||
return $this->$name();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Evaluators;
|
||||||
|
|
||||||
|
use Pest\Factories\Attribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Attributes
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
|||||||
*/
|
*/
|
||||||
final class AfterAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
final class AfterAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Creates a new instance of after all already exist exception.
|
|
||||||
*/
|
|
||||||
public function __construct(string $filename)
|
public function __construct(string $filename)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('The afterAll already exist in the filename `%s`.', $filename));
|
parent::__construct(sprintf('The [afterAll] hook is already defined in [%s]. Each test file may only define it once.', $filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-5
@@ -12,13 +12,10 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class BeforeEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
final class AfterAllWithinDescribe extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Creates a new instance of before each already exist exception.
|
|
||||||
*/
|
|
||||||
public function __construct(string $filename)
|
public function __construct(string $filename)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('The beforeEach already exist in the filename `%s`.', $filename));
|
parent::__construct(sprintf('The [afterAll] hook may not be used inside a [describe] block. Please move it to the top level of [%s].', $filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
public function __construct(string $filename)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('The [after] hook may only be chained onto [beforeEach] inside a [describe] block. Please move it inside one in [%s].', $filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?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 AttributeNotSupportedYet extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Creates a new instance of attribute not supported yet.
|
|
||||||
*/
|
|
||||||
public function __construct(string $attribute, string $value)
|
|
||||||
{
|
|
||||||
parent::__construct(sprintf('The PHPUnit attribute `%s` with value `%s` is not supported yet.', $attribute, $value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Exceptions;
|
||||||
|
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderlessEditor;
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
||||||
|
use Pest\Contracts\Panicable;
|
||||||
|
use Pest\Support\View;
|
||||||
|
use RuntimeException;
|
||||||
|
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BaselineFetchFailed extends RuntimeException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $headline,
|
||||||
|
private readonly string $hint,
|
||||||
|
private readonly bool $hasAnchor = false,
|
||||||
|
) {
|
||||||
|
parent::__construct($headline);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(OutputInterface $output): void
|
||||||
|
{
|
||||||
|
View::renderUsing($output);
|
||||||
|
|
||||||
|
if (! $this->hasAnchor) {
|
||||||
|
View::render('components.badge', ['type' => 'ERROR', 'content' => $this->headline]);
|
||||||
|
$this->renderChild($output, $this->hint.' Or use [--fresh] to record locally.');
|
||||||
|
$output->writeln('');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->renderChild($output, $this->headline);
|
||||||
|
$this->renderChild($output, $this->hint.' Or use [--fresh] to record locally.');
|
||||||
|
$output->writeln('');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exitCode(): int
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderChild(OutputInterface $output, string $text): void
|
||||||
|
{
|
||||||
|
$output->writeln(sprintf(' <fg=gray>─ %s</>', $text));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,13 +12,10 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class AfterEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
final class BeforeAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Creates a new instance of after each already exist exception.
|
|
||||||
*/
|
|
||||||
public function __construct(string $filename)
|
public function __construct(string $filename)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('The afterEach already exist in the filename `%s`.', $filename));
|
parent::__construct(sprintf('The [beforeAll] hook is already defined in [%s]. Each test file may only define it once.', $filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user