Compare commits

...

282 Commits
2.x ... v3.3.1

Author SHA1 Message Date
1e3156a5b6 release: v3.3.1 2024-10-11 09:31:24 +01:00
97713c0832 chore: bumps dependencies 2024-10-11 09:31:16 +01:00
0a7bff0d24 release: v3.3.0 2024-10-06 19:25:27 +01:00
7618434580 chore: uses phpunit v11.4.0 2024-10-06 19:25:20 +01:00
1e0bb88b73 release: v3.2.5 2024-10-01 11:55:18 +01:00
83b76d7c2e chore: bumps dependencies 2024-10-01 11:51:26 +01:00
5a870b3940 chore: style changes 2024-10-01 11:51:16 +01:00
1115c64186 chore: style changes 2024-10-01 11:48:14 +01:00
e38a271ca2 Merge pull request #1279 from midnite81/bug/declare-strict-types-with-comments-above
Strict types expectation allows for comments above declaration
2024-09-29 19:12:06 +01:00
43703ab40a Merge pull request #1280 from CamKem/fix/middleware-method-preset
Fix: Add middleware to the allowable public methods for Laravel Preset
2024-09-29 19:11:40 +01:00
86452765a4 fix: add middleware to the allowable public methods on the laravel preset 2024-09-29 16:07:39 +10:00
b8a1b7e5cc Add tests for strict types expectation
Introduced new test cases to ensure strict type declaration handling. Files with and without strict types are tested, including scenarios with comments preceding the declaration. Updated the regex in `Expectation.php` to accommodate comments and whitespaces before the `declare(strict_types=1)` statement.
2024-09-28 17:31:33 +01:00
5fe79d9c18 release: v3.2.4 2024-09-26 23:53:39 +01:00
2744da4292 Merge pull request #1277 from MuhammedAlkhudiry/ignore-handler-in-laravel-preset
ignore App\Exceptions\Handler.php in arch laravel preset
2024-09-26 23:47:26 +01:00
87f4e5e7b3 fix 2024-09-26 16:44:30 +03:00
bb3decf3cc ignore App\Exceptions\Handler.php in arch laravel preset 2024-09-26 16:41:43 +03:00
4e2987d438 release: v3.2.3 2024-09-25 16:19:39 +01:00
a25158bce8 Merge pull request #1275 from jeremynikolic/laravel-presets-ignore-concerns
feat: add ignoring of Concerns folder inside App\Enums and App\Features
2024-09-25 16:16:26 +01:00
49e77b1d4c feat: add ignoring of Concerns folder inside App\Enums and App\Features 2024-09-25 17:12:42 +02:00
989e43d1a0 release: v3.2.2 2024-09-24 10:23:43 +01:00
7cd42aafd8 fix: auto-complete on presets 2024-09-24 10:23:32 +01:00
48a1de273f release: v3.2.1 2024-09-23 14:09:55 +01:00
970e16e949 Ignores 2024-09-23 14:08:30 +01:00
432ff221c6 fix: missing != and !== on new toUseStrictEquality arch expectation 2024-09-23 14:08:21 +01:00
a55da85dd2 release: v3.2.0 2024-09-23 13:14:03 +01:00
f291cd1603 chore: bumps dependencies 2024-09-23 13:11:49 +01:00
5de0c2254a release: v3.1.0 2024-09-19 23:39:07 +01:00
b98ce0ced3 feat: adds mutates 2024-09-19 23:32:28 +01:00
28772c2609 chore: dont run integration tests yet on php 8.4 2024-09-19 13:42:01 +01:00
452ffaf8df chore: fixes windows build 2024-09-19 13:38:35 +01:00
e8338405b5 chore: tests against PHP 8.4 2024-09-19 13:36:41 +01:00
1b014e4b18 release: v3.0.8 2024-09-19 13:04:42 +01:00
034715e8b1 Merge pull request #1266 from julien-boudry/3.x
Fix #1265 -  issue parameter cannot be int (one done, pr, todo, wip)
2024-09-19 12:53:34 +01:00
09eff785c4 release: v3.0.7 2024-09-19 12:29:38 +01:00
22cc7805d7 chore: bumps dependencies 2024-09-19 12:29:38 +01:00
669dc0da71 Fix #1265 - issue parameter cannot be int (one done, pr, todo, wip) 2024-09-19 09:49:36 +00:00
689da4ed4e Merge pull request #1254 from pestphp/bugfix/jira-url
fix: update assignee URL for Jira
2024-09-18 21:48:09 +01:00
2f15861b0d fix: update assignee URL for Jira 2024-09-16 12:18:21 +01:00
0d50d35b5e release: v3.0.6 2024-09-11 18:59:43 +01:00
ce61ced8e1 Merge pull request #1237 from smirok/teamcity-fix-for-tests-with-dataset
fix: unify the `locationHint` prefix and prettify both `locationHint` and `name` parameters for testing with datasets
2024-09-11 18:51:04 +01:00
7227d24611 fix: unify the locationHint prefix and prettify both locationHint and name parameters for testing with datasets 2024-09-11 16:42:06 +02:00
45f16484d5 Merge pull request #1235 from pestphp/3.x_herd_fix
Fixes parallel mutation testing when using Laravel Herd
2024-09-11 15:13:49 +01:00
b16e8650da Fixes parallel mutation testing when using Laravel Herd. 2024-09-11 15:11:47 +01:00
c2f30e0148 Fixes parallel mutation testing when using Laravel Herd. 2024-09-11 15:04:44 +01:00
47ce45de56 release: v3.0.4 2024-09-11 00:48:29 +01:00
32881774d2 fix: global afterEach being called twice 2024-09-11 00:40:41 +01:00
ea72461f1b release: v3.0.3 2024-09-10 22:29:09 +01:00
49f15521e0 fix: printer method name 2024-09-10 22:29:01 +01:00
95c5394b66 Bumps dependencies 2024-09-10 16:59:38 +01:00
8de30cc8b7 release: v3.0.1 2024-09-09 15:29:44 +01:00
b17feef3f4 release: v3.0.0 2024-09-09 11:01:08 +01:00
d8e4a405ad chore: bumps dependencies 2024-09-09 10:57:31 +01:00
04af21183a chore: fixes style 2024-09-09 10:53:45 +01:00
09edaa9c2d chore: updates snapshots 2024-09-09 10:49:48 +01:00
fa41a67be9 chore: fixes type checking 2024-09-09 10:37:20 +01:00
1a8f7025fa release: v3.0.0 2024-09-09 01:20:30 +01:00
6afd2ec9df release: v3.0.0 2024-09-09 01:16:11 +01:00
d772069db2 release: v3.0.0 2024-09-09 01:12:41 +01:00
bb1a0b5e79 chore: locks phpunit 2024-09-09 01:12:33 +01:00
3c333ebbb8 docs: updates banner 2024-09-09 01:10:33 +01:00
868ac1840f chore: bumps dependencies 2024-09-08 23:59:12 +01:00
f857b4889c fix: load of mutate 2024-09-08 23:56:48 +01:00
c6b81e6e12 More fixes 2024-09-06 20:59:54 +01:00
c78d33b69e wip 2024-09-05 23:37:40 +01:00
bfd351783e wip 2024-09-05 22:41:26 +01:00
526af2a75e wp 2024-09-05 21:56:51 +01:00
bf9d011045 Merge pull request #1220 from gehrisandro/fix/accept-traits-in-covers-function 2024-09-05 21:41:53 +01:00
aaee0e420b Fix covers function to accept traits 2024-09-05 22:36:34 +02:00
772448db80 wip 2024-09-05 20:53:10 +01:00
e22fb2e6c0 w 2024-09-05 19:48:03 +01:00
49aa44c470 w 2024-09-05 19:43:20 +01:00
1cae035887 fix 2024-09-05 18:26:13 +01:00
15183c4145 fix 2024-09-05 18:23:57 +01:00
ae288d1123 fix tests 2024-09-05 18:17:30 +01:00
2d80ff19ec feat(mutate): only 2024-09-05 02:49:52 +01:00
c82f77ea75 fix 2024-09-05 02:05:28 +01:00
5050ae304f fixes tests 2024-09-05 01:24:55 +01:00
98e947e0cc more fixes 2024-09-05 01:19:17 +01:00
68785986a0 fix 2024-09-05 00:55:37 +01:00
8c078087ff Adds covered classes to mutation 2024-09-05 00:14:12 +01:00
65f74f620c feat: adds covers 2024-09-05 00:10:29 +01:00
dd20323ca7 feat: custom presets 2024-09-04 20:53:33 +01:00
a7ca7afe4e fix: preset laravel 2024-09-04 11:51:01 +01:00
baf764f286 feat(mutate): requires it by default 2024-09-04 11:38:59 +01:00
3a907c886b feat(mutate): requires it by default 2024-09-04 11:38:53 +01:00
e6823679dd feat: adds not->toHavePrivateMethods and related 2024-09-04 11:18:41 +01:00
a021b5b8c3 feat(laravel-preset): traits in traits folders 2024-09-04 11:17:32 +01:00
e2d360b1b5 chore: adjusts tests 2024-09-03 14:09:03 +01:00
8920b850e1 feat(arch-presets): adds another rule to laravel preset 2024-09-03 14:08:11 +01:00
509074b3fa chore: bumps dependencies 2024-09-03 14:07:56 +01:00
6f9ea14c68 Merge pull request #1215 from jbrooksuk/patch-1
Add `dump` to Laravel preset
2024-09-03 12:17:01 +01:00
60dcfb36a8 Merge pull request #1207 from JonPurvis/coverage-fix
Fix coverage result for 99.95%+
2024-09-03 12:16:37 +01:00
ca25d5b13f Merge pull request #1208 from benjamincrozat/patch-1
Exclude Global Scopes from the required "Model" suffix
2024-09-03 12:15:59 +01:00
5cba63e2ba Merge pull request #1209 from phh/patch-1
chore: Use new rector syntax
2024-09-03 12:15:43 +01:00
dd45a5c655 Add dump to Laravel preset 2024-09-03 12:15:25 +01:00
dde943b993 refacto: toHaveMethod and toHaveMethods 2024-09-02 19:38:38 +01:00
bb8677549a chore: fixes snapshots 2024-09-02 18:30:12 +01:00
5ae5ac9a54 fix: removes both toHaveMethod and toHaveMethods 2024-08-28 16:30:29 -05:00
cc6f1b43f6 chore: Remove InlineConstructorDefaultToPropertyRector rule
Since its already a part of the codeQuality ruleset.
2024-08-28 08:26:47 +02:00
88197fe1d5 chore: use new rector syntax 2024-08-28 08:22:18 +02:00
f53f855e9c Exclude scopes from the required "Model" suffix 2024-08-27 19:57:09 +02:00
df69b0b791 fix coverage result for 99.95%+ 2024-08-26 21:58:16 +01:00
ecdbe7a472 release: 3.0.0-rc.1 2024-08-22 21:36:19 +01:00
2c6c3119d2 Merge branch '2.x' into 3.x 2024-08-22 21:21:52 +01:00
9b11857ae6 chore: updates snpahosts 2024-08-20 22:59:25 +01:00
bb29e97200 Merge branch '2.x' into 3.x 2024-08-20 22:57:52 +01:00
8fe2698c28 chore: fixes test suite 2024-08-20 22:57:10 +01:00
f72d6f2278 Removes PHP 8.4 2024-08-20 22:39:00 +01:00
71811d6e3a Bumps PHP version 2024-08-20 22:37:34 +01:00
dfdbd357e9 chore: bumps dependencies 2024-08-20 22:37:27 +01:00
4d9ed8768c fix: panic with TestDescriptionMissing 2024-08-19 02:24:18 +01:00
6638d279e1 preset(strict): final first 2024-08-12 21:39:41 +01:00
b5cd0ffb65 chore: updates snapshots 2024-08-12 01:13:33 +01:00
7ef40760c2 fixes filter 2024-08-12 00:46:31 +01:00
ce4495b093 w 2024-08-11 23:41:19 +01:00
868547114f w 2024-08-11 22:52:16 +01:00
9c07dd9990 wip 2024-08-11 15:47:48 +01:00
09beb812d4 fix 2024-08-10 17:27:21 +01:00
4e98dfe3c3 pr 2024-08-10 16:57:48 +01:00
ad6dca94fa Adds done 2024-08-10 14:27:18 +01:00
86f46c2efd Adds alias 2024-08-10 13:56:40 +01:00
ccfd4fd77a beta 2 2024-08-10 11:49:44 +01:00
e4d2dac354 beta 2 2024-08-10 11:49:40 +01:00
7e4c51e13d Style 2024-08-10 11:48:32 +01:00
aacd874ebe fix 2024-08-09 00:38:16 +01:00
1c236aab26 fixes and styles 2024-08-09 00:30:06 +01:00
b6bf01148f feat: toHavePrivateMethodsBesides, toHaveProtectedMethodsBesides, toHavePublicMethodsBesides 2024-08-09 00:24:24 +01:00
347bcfd8a8 Bumps dependencies 2024-08-09 00:24:09 +01:00
0ced3171b0 fix: missing methods 2024-08-07 11:30:19 +01:00
38638e865f chore: fixes tests 2024-08-07 11:27:30 +01:00
adbc6b4a89 chore: fixes tests 2024-08-07 11:26:56 +01:00
9353015691 feat(presets): reworks code 2024-08-07 11:08:29 +01:00
17058d1709 chore: bumps dependencies 2024-08-07 11:06:41 +01:00
8ffa66dc7c fix(autoloader): issue when errors when loading the file 2024-08-07 11:06:34 +01:00
af680ca8aa feat(todos): adjusts params order 2024-08-03 18:48:53 +01:00
651aab560c Allows to set context on todos 2024-08-03 18:42:31 +01:00
41e50cac05 Adds assignees 2024-08-03 17:05:34 +01:00
6fb1133d52 Adjusts for Collision 8.4 2024-08-03 16:36:01 +01:00
63ba117b33 Fixes tests 2024-08-03 00:13:01 +01:00
33d36d77cb Merge branch '2.x' into 3.x 2024-08-02 23:53:26 +01:00
4e7db91ee8 chore: updates dependencies 2024-08-02 23:46:31 +01:00
45cce6ce93 Style 2024-07-24 21:54:52 +01:00
101e26749a fix: properties and methods documented 2024-07-20 17:45:41 +01:00
b3c8c24aea Reworks 2024-07-20 14:15:28 +01:00
a7553b7593 feat(to-have-all*): improvements 2024-07-19 14:32:46 +01:00
f2691623cf feat: toHaveAllMethodsDocumented and toHaveAllPropertiesDocumented 2024-07-19 14:03:59 +01:00
99107544ff chore: fixes types 2024-07-18 20:45:47 +01:00
2e411893d2 feat: adds toHaveLineCountLessThan 2024-07-18 20:43:39 +01:00
135c8a0d46 docs: updates features 2024-07-18 20:43:11 +01:00
1cdd7d6744 chore: updates dependencies 2024-07-18 20:42:52 +01:00
fca0c3a10c chore: bumps dependencies 2024-07-16 23:52:00 +01:00
0331a87be1 feat: adds toHaveFileSystemPermissions expectation 2024-07-14 23:16:04 +01:00
d3be6b72dd Merge pull request #1189 from MrPunyapal/fix/breaking-in-windows
feat: update filename check in Configuration class
2024-07-09 15:58:14 +01:00
7d3118db65 feat: update filename check in Configuration class 2024-07-09 20:24:28 +05:30
eac7abebcb Merge pull request #1184 from edjw/patch-1
Include ds from Laradumps in ArchPresets
2024-07-07 22:47:49 +01:00
6896dd486a fix: laravel preset 2024-07-07 22:46:15 +01:00
1e5b399603 feat: adjusts default conf 2024-07-06 19:30:05 +01:00
ccdf43726d fix: pest() 2024-07-06 18:25:43 +01:00
67dbce2d42 feat: more presets rules 2024-07-06 16:45:58 +01:00
ee32f25485 feat: pr and issue 2024-07-04 00:53:58 +01:00
09ca7a1fd5 chore: features 2024-07-03 22:18:11 +01:00
dade84e6b6 fix: handle arguments 2024-07-03 22:17:47 +01:00
1c4bc8b1dc fix: before each globally 2024-07-03 22:15:11 +01:00
0d2f3eb60e Include ds from Laradumps in ArchPresets 2024-06-28 22:33:15 +01:00
29787d1ff1 Update Features.md 2024-06-27 11:32:55 +01:00
474b9b7e17 chore: adjusts tests 2024-06-27 01:41:34 +01:00
5c3bf469d5 feat: note() 2024-06-27 01:26:54 +01:00
d9252e85d6 chore: optimize preset 2024-06-26 22:32:58 +01:00
0289466ce8 Allows other folders to be queuable 2024-06-25 23:06:25 +01:00
57ef989df8 feat(presets): improve laravel preset 2024-06-25 22:02:23 +01:00
9d02b649e2 feat: adds toUseTraits expectation 2024-06-25 21:56:08 +01:00
00643312b7 chore: updates snapshots 2024-06-25 21:13:05 +01:00
eac6585a2e refactor: logger 2024-06-25 21:09:10 +01:00
04c39bae2e Adjust style 2024-06-25 21:02:52 +01:00
c65755725d feat: improves type hinting with @param-closure-this 2024-06-15 15:37:35 +01:00
ec58040f6e feat: improves type hinting 2024-06-15 15:24:03 +01:00
3fa73e40cc Bumps dependencies 2024-06-15 15:23:05 +01:00
c07513c6a0 chore: fixes tests 2024-06-11 23:11:38 +01:00
85d91d5652 Merge pull request #1170 from pestphp/feat/presets
[3.x] Arch Presets
2024-06-11 22:54:12 +01:00
02bae3b649 Merge branch '3.x' into feat/presets 2024-06-11 22:54:05 +01:00
3ba2b68afc Merge pull request #1178 from MrPunyapal/feat/some-functions-to-avoid
Feat: some functions to avoid
2024-06-11 22:03:29 +01:00
ed3ec79aab pint 2024-06-11 21:26:40 +05:30
894dca83f7 chore: update Arch.php to ignore 'assert' function in presets 2024-06-11 21:24:57 +05:30
b873b89b62 Restrict additional dangerous functions in Security.php 2024-06-11 21:20:32 +05:30
1bee283d15 Update Base.php to include 'ereg' and 'eregi' in the list of restricted functions 2024-06-11 21:09:15 +05:30
7b4dd410f6 chore: update Arch.php to ignore additional functions in presets 2024-06-11 21:08:02 +05:30
4396ee2e03 feat(presets): update Security.php to restrict additional dangerous functions 2024-06-11 21:02:19 +05:30
e4550c8d51 Update Base.php to include 'global' in the list of restricted functions 2024-06-11 20:58:52 +05:30
a25cfb435c Update Base.php to include 'mysql_*' in the list of restricted functions 2024-06-11 20:56:49 +05:30
fe4fe12df1 chore: updates snapshots 2024-06-10 23:10:20 +01:00
3bcc99a372 Merge branch '2.x' into 3.x 2024-06-10 23:04:34 +01:00
3ad788dddb feat(presets): adjusts laravel preset 2024-06-10 22:38:40 +01:00
2108d18be5 Merge pull request #1176 from MrPunyapal/feat/laravel-preset
Feat: laravel preset
2024-06-10 22:36:21 +01:00
aa4a5fcd15 Merge pull request #1175 from Shotman/preset-buggregator-trap
Add trap to list
2024-06-10 22:34:02 +01:00
1688888f15 feat(presets): update Laravel preset to include expectation for Laravel service provider suffix 2024-06-10 21:49:03 +05:30
40539ca720 feat(presets): update Laravel preset to include expectation for Laravel service provider suffix 2024-06-10 21:48:25 +05:30
7144d6dfbd feat(presets): add expectation for Laravel notification class 2024-06-10 21:47:12 +05:30
7240250a15 feat(presets): add expectation for Laravel job handle method 2024-06-10 21:47:05 +05:30
508e42a2ff feat(presets): update Laravel preset to include expectation for Laravel request suffix 2024-06-10 21:45:17 +05:30
d8156fee53 feat(presets): add expectation for Laravel middleware handle method 2024-06-10 21:44:49 +05:30
abc245bf85 feat(presets): add expectation for Laravel command handle method 2024-06-10 21:40:16 +05:30
65dacd5647 feat(presets): add expectation for Laravel mail class 2024-06-10 21:38:20 +05:30
917f7a64a0 feat(presets): add expectation for Laravel exception suffix 2024-06-10 21:36:59 +05:30
e8b09d6f8c feat(presets): add expectation for Laravel command suffix 2024-06-10 21:35:42 +05:30
0c4e6de823 Add trap to list
Adding trap function from https://github.com/buggregator/trap docs:  https://docs.buggregator.dev/trap/getting-started.html
2024-06-10 18:03:46 +02:00
52282cc590 feat(presets): add expectation for Laravel request suffix 2024-06-10 21:29:12 +05:30
a46142d8c7 feat(presets): update Laravel preset to exclude models with suffix 'Model' 2024-06-10 21:28:08 +05:30
241dcf8f34 feat(presets): add expectation for Laravel controller suffix 2024-06-10 21:25:51 +05:30
927cee609e Update src/ArchPresets/Base.php
Co-authored-by: Joshua Gigg <giggsey@gmail.com>
2024-06-10 15:19:55 +01:00
98e4ebb8fd feat(presets): fixes return type 2024-06-10 11:40:25 +01:00
c173e3e86b feat(presets): allows usage of laravel preset 2024-06-10 11:39:31 +01:00
c73655f4f9 feat(presets): adds xdebug_* functions 2024-06-10 11:22:51 +01:00
4ac1c6efc6 feat(presets): adds goto 2024-06-10 11:16:46 +01:00
2e5a308b0d Merge pull request #1174 from ClaraLeigh/feat/presets
feature(presets): Add security preset
2024-06-10 11:13:50 +01:00
7b8e4aec08 feature(presets): Add security preset
Looks for functions often seen as insecure
2024-06-10 11:44:45 +10:00
13fb66f15c feat(presets): ignores ddd 2024-06-10 02:22:41 +01:00
dd1bd92910 feat(presets): more rules 2024-06-10 01:15:21 +01:00
d665b53b22 Merge pull request #1173 from faissaloux/add-echo-and-print-to-base
Add `echo` and `print` to base preset
2024-06-10 00:49:15 +01:00
c54b7e400e echo and print not to be used in base preset 2024-06-10 00:00:57 +01:00
c1e1fff0d0 feat(presets): ignores ddd 2024-06-09 23:47:53 +01:00
2e4a8329a6 feat(presets): keeps improving base presets 2024-06-09 22:46:21 +01:00
878988a02d feat(presets): ignores eval for now 2024-06-09 22:38:30 +01:00
ceb7244b43 feat(presets): refactors code 2024-06-09 22:23:10 +01:00
84256aa8b9 Merge pull request #1172 from MrPunyapal/feat/add-ini_set-into-base
feat: add 'ini_set' to list of expected functions in Base.php
2024-06-09 15:03:33 +01:00
d6b59e4e96 feat: add 'ini_set' to list of expected functions in Base.php 2024-06-09 19:32:49 +05:30
087d09120a Merge pull request #1171 from MrPunyapal/feat/add-eval-in-base
feat: add 'eval' to list of expected functions in Base.php
2024-06-09 14:47:25 +01:00
cc41a7f81d feat: add 'eval' to list of expected functions in Base.php 2024-06-09 19:16:29 +05:30
bd16769b93 fixes tests 2024-06-09 00:05:18 +01:00
60b1e63c23 feat: initial work on presets 2024-06-08 20:54:46 +01:00
c7bcb6eb7b chore: updates snapshots 2024-06-08 14:01:41 +01:00
d25ec50384 chore: bumps dependencies 2024-06-08 14:01:31 +01:00
9e27813897 Adjusts configuration 2024-06-08 13:12:17 +01:00
ba914fa2fb fix: add more extends and traits 2024-05-14 20:28:49 +01:00
c919bb5bc4 feat: adds pest function 2024-05-14 01:58:44 +01:00
8169382362 feat: adds after 2024-05-08 01:24:30 +01:00
04b099e87c docs: adds backlog 2024-05-08 01:24:16 +01:00
fecdb7f572 chore: updates deps 2024-05-08 01:24:08 +01:00
b611d0d444 wording 2024-04-30 20:48:49 +01:00
ac7199c96d wip 2024-04-30 20:48:27 +01:00
7756457dc4 Merge pull request #1146 from JHWelch/interpolated-dataset-names
Interpolated dataset names
2024-04-30 19:16:54 +01:00
10da81eee4 Check on non named tests 2024-04-29 18:10:35 -05:00
8bbee3c1e5 Add working interpolated dataset name 2024-04-29 18:01:09 -05:00
16125df77b chore: fixes test suite 2024-04-28 12:30:56 +01:00
80530cb1e0 wip: runs integration tests undert the v flag 2024-04-28 12:18:11 +01:00
2070538fd3 chore: fixes test suite 2024-04-28 12:02:42 +01:00
a2cb78710d Fixes isset 2024-04-28 11:16:37 +01:00
335bfdb79d fix: test suite 2024-04-28 11:02:24 +01:00
cfa00da885 Fixes test suite 2024-04-27 11:36:22 +01:00
f49d1e0e18 Merge branch '2.x' into 3.x 2024-04-26 23:14:53 +01:00
0c51b159a7 chore: fixes test suite 2024-04-01 13:24:30 +01:00
c6984323c3 chore: fixes test suite 2024-04-01 13:15:33 +01:00
831d9bf49a tests 2024-03-29 23:12:10 +00:00
12f6aa604c chore: versions 2024-03-29 23:12:00 +00:00
265f0c7da9 Merge branch '2.x' into 3.x 2024-03-29 23:09:19 +00:00
7b9bae0415 fix: usage of named arguments 2024-02-01 13:45:06 +00:00
3dffdf7cb8 Merge branch '2.x' into 3.x 2024-02-01 11:53:10 +00:00
815ae3c644 Merge pull request #975 from Katalam/repeat
[2.x] Sharing `repeat` iteration as `dataset` variable
2024-01-25 15:01:34 +00:00
4e31973040 Merge branch '2.x' into 3.x 2024-01-23 18:15:06 +00:00
6a48e9d44b Merge branch '2.x' into 3.x 2024-01-23 18:06:23 +00:00
9ce52ee7ce chore: fixes snaphots 2024-01-11 16:03:48 +00:00
3ff41bcb68 Merge branch '2.x' into 3.x 2024-01-11 16:03:33 +00:00
2b094b4188 Merge branch '2.x' into 3.x 2024-01-11 15:37:31 +00:00
dd4d5bbd4e chore: adjusts snapshots 2024-01-11 11:33:02 +00:00
ab64912c70 chore: adjusts coding style 2024-01-11 11:30:11 +00:00
1506d8bb27 chore: uses PHP 8.2 for static testing 2024-01-11 11:28:45 +00:00
5aa13b8e97 chore: adjusts coding style 2024-01-11 11:26:48 +00:00
b143ed7aac chore: uses Symfony Console 7.0.2 2024-01-11 11:17:21 +00:00
26dd5f298f chore: adjusts tests 2024-01-11 10:51:45 +00:00
d939ee938e chore: bumps dependencies 2024-01-11 10:36:24 +00:00
515de3972f Merge branch '2.x' into 3.x 2024-01-10 11:51:11 +00:00
bf573b3cac chore: makes tests pass 2024-01-08 13:16:53 +00:00
53dc9ffa06 feat: always use attributes instead of annotations 2024-01-05 18:00:14 +00:00
04d2fa5ce8 feat: moves covers nothing to attribute 2024-01-05 14:37:33 +00:00
7764a7a162 chore: bumps dependencies 2024-01-05 14:37:24 +00:00
727a427837 feat: adjust overrides 2024-01-05 14:37:13 +00:00
f69a3cf832 chore: bumps dependencies 2024-01-05 11:09:32 +00:00
ed0bf1786f chore: fixes conflict 2024-01-05 10:24:49 +00:00
2d1d8a81e1 Keeps working on dependencies 2024-01-05 09:50:45 +00:00
d515cf965e chore: bumps dependencies 2024-01-04 18:41:22 +00:00
67e452e9ed chore: add docs 2023-10-06 15:10:02 +02:00
ecff90da1c fix: add repeat iteration as the last argument when combined with dataset 2023-10-06 15:07:48 +02:00
3ee5c29a00 feat: add repeat iteration as function argument if no extra dataset is provided 2023-10-05 23:07:03 +02:00
169 changed files with 4386 additions and 943 deletions

View File

@ -24,15 +24,15 @@ jobs:
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: 8.1 php-version: 8.2
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
- name: Install Dependencies - name: Install Dependencies
run: composer update --prefer-stable --no-interaction --no-progress --ansi run: composer update --prefer-stable --no-interaction --no-progress --ansi
- name: Type Check # - name: Type Check
run: composer test:type:check # run: composer test:type:check
- name: Type Coverage - name: Type Coverage
run: composer test:type:coverage run: composer test:type:coverage

View File

@ -13,12 +13,9 @@ jobs:
fail-fast: true fail-fast: true
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
symfony: ['6.4', '7.0'] symfony: ['7.1']
php: ['8.1', '8.2', '8.3'] php: ['8.2', '8.3', '8.4']
dependency_version: [prefer-lowest, prefer-stable] dependency_version: [prefer-lowest, prefer-stable]
exclude:
- php: '8.1'
symfony: '7.0'
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }} name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
@ -39,7 +36,13 @@ jobs:
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install PHP dependencies - name: Install PHP dependencies
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:~${{ matrix.symfony }}" shell: bash
run: |
if [[ "${{ matrix.php }}" == "8.4" ]]; then
composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}" --ignore-platform-req=php
else
composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
fi
- name: Unit Tests - name: Unit Tests
run: composer test:unit run: composer test:unit
@ -48,4 +51,5 @@ jobs:
run: composer test:parallel run: composer test:parallel
- name: Integration Tests - name: Integration Tests
if: ${{ matrix.php != '8.4' }}
run: composer test:integration run: composer test:integration

View File

@ -69,7 +69,7 @@ If you want to check things work against a specific version of PHP, you may incl
the `PHP` build argument when building the image: the `PHP` build argument when building the image:
```bash ```bash
make build ARGS="--build-arg PHP=8.2" make build ARGS="--build-arg PHP=8.3"
``` ```
The default PHP version will always be the lowest version of PHP supported by Pest. The default PHP version will always be the lowest version of PHP supported by Pest.

View File

@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/pestphp/art/master/v2/banner.png" width="600" alt="PEST"> <img src="https://raw.githubusercontent.com/pestphp/art/master/v3/banner.png" width="600" alt="PEST">
<p align="center"> <p align="center">
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (master)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=2.x&label=Tests%202.x"></a> <a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (master)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=2.x&label=Tests%202.x"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a> <a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
@ -9,6 +9,9 @@
</p> </p>
------ ------
> Pest v3 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest3-now-available)**.
**Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP. **Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP.
- Explore our docs at **[pestphp.com »](https://pestphp.com)** - Explore our docs at **[pestphp.com »](https://pestphp.com)**

View File

@ -2,10 +2,10 @@
When releasing a new version of Pest there are some checks and updates that need to be done: When releasing a new version of Pest there are some checks and updates that need to be done:
> **For Pest v1 you should use the `1.x` branch instead.** > **For Pest v2 you should use the `2.x` branch instead.**
- Clear your local repository with: `git add . && git reset --hard && git checkout 2.x` - Clear your local repository with: `git add . && git reset --hard && git checkout 3.x`
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...2.x](https://github.com/pestphp/pest/compare/{latest_version}...2.x) - On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...3.x](https://github.com/pestphp/pest/compare/{latest_version}...3.x)
- Update the version number in [src/Pest.php](src/Pest.php) - Update the version number in [src/Pest.php](src/Pest.php)
- Run the tests locally using: `composer test` - Run the tests locally using: `composer test`
- Commit the Pest file with the message: `git commit -m "release: vX.X.X"` - Commit the Pest file with the message: `git commit -m "release: vX.X.X"`

101
bin/pest
View File

@ -1,9 +1,15 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php declare(strict_types=1); <?php
declare(strict_types=1);
use Pest\Kernel; use Pest\Kernel;
use Pest\Panic; use Pest\Panic;
use Pest\TestCaseFilters\GitDirtyTestCaseFilter; use Pest\TestCaseFilters\GitDirtyTestCaseFilter;
use Pest\TestCaseMethodFilters\AssigneeTestCaseFilter;
use Pest\TestCaseMethodFilters\IssueTestCaseFilter;
use Pest\TestCaseMethodFilters\NotesTestCaseFilter;
use Pest\TestCaseMethodFilters\PrTestCaseFilter;
use Pest\TestCaseMethodFilters\TodoTestCaseFilter; use Pest\TestCaseMethodFilters\TodoTestCaseFilter;
use Pest\TestSuite; use Pest\TestSuite;
use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArgvInput;
@ -17,8 +23,10 @@ use Symfony\Component\Console\Output\ConsoleOutput;
$dirty = false; $dirty = false;
$todo = false; $todo = false;
$notes = false;
foreach ($arguments as $key => $value) { foreach ($arguments as $key => $value) {
if ($value === '--compact') { if ($value === '--compact') {
$_SERVER['COLLISION_PRINTER_COMPACT'] = 'true'; $_SERVER['COLLISION_PRINTER_COMPACT'] = 'true';
unset($arguments[$key]); unset($arguments[$key]);
@ -29,8 +37,14 @@ use Symfony\Component\Console\Output\ConsoleOutput;
unset($arguments[$key]); unset($arguments[$key]);
} }
if (str_contains($value, '--test-directory')) { if (str_contains($value, '--test-directory=')) {
unset($arguments[$key]); unset($arguments[$key]);
} elseif ($value === '--test-directory') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
} }
if ($value === '--dirty') { if ($value === '--dirty') {
@ -43,6 +57,61 @@ use Symfony\Component\Console\Output\ConsoleOutput;
unset($arguments[$key]); unset($arguments[$key]);
} }
if ($value === '--notes') {
$notes = true;
unset($arguments[$key]);
}
if (str_contains($value, '--assignee=')) {
unset($arguments[$key]);
} elseif ($value === '--assignee') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--issue=')) {
unset($arguments[$key]);
} elseif ($value === '--issue') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--ticket=')) {
unset($arguments[$key]);
} elseif ($value === '--ticket') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--pr=')) {
unset($arguments[$key]);
} elseif ($value === '--pr') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--pull-request=')) {
unset($arguments[$key]);
} elseif ($value === '--pull-request') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--teamcity')) { if (str_contains($value, '--teamcity')) {
unset($arguments[$key]); unset($arguments[$key]);
$arguments[] = '--no-output'; $arguments[] = '--no-output';
@ -66,7 +135,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
// Get $rootPath based on $autoloadPath // Get $rootPath based on $autoloadPath
$rootPath = dirname($autoloadPath, 2); $rootPath = dirname($autoloadPath, 2);
$input = new ArgvInput(); $input = new ArgvInput;
$testSuite = TestSuite::getInstance( $testSuite = TestSuite::getInstance(
$rootPath, $rootPath,
@ -78,7 +147,31 @@ use Symfony\Component\Console\Output\ConsoleOutput;
} }
if ($todo) { if ($todo) {
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter()); $testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter);
}
if ($notes) {
$testSuite->tests->addTestCaseMethodFilter(new NotesTestCaseFilter);
}
if ($assignee = $input->getParameterOption('--assignee')) {
$testSuite->tests->addTestCaseMethodFilter(new AssigneeTestCaseFilter((string) $assignee));
}
if ($issue = $input->getParameterOption('--issue')) {
$testSuite->tests->addTestCaseMethodFilter(new IssueTestCaseFilter((int) $issue));
}
if ($issue = $input->getParameterOption('--ticket')) {
$testSuite->tests->addTestCaseMethodFilter(new IssueTestCaseFilter((int) $issue));
}
if ($pr = $input->getParameterOption('--pr')) {
$testSuite->tests->addTestCaseMethodFilter(new PrTestCaseFilter((int) $pr));
}
if ($pr = $input->getParameterOption('--pull-request')) {
$testSuite->tests->addTestCaseMethodFilter(new PrTestCaseFilter((int) $pr));
} }
$isDecorated = $input->getParameterOption('--colors', 'always') !== 'never'; $isDecorated = $input->getParameterOption('--colors', 'always') !== 'never';

View File

@ -17,17 +17,18 @@
} }
], ],
"require": { "require": {
"php": "^8.1.0", "php": "^8.2.0",
"brianium/paratest": "^7.3.1", "brianium/paratest": "^7.5.7",
"nunomaduro/collision": "^7.10.0|^8.4.0", "nunomaduro/collision": "^8.4.0",
"nunomaduro/termwind": "^1.15.1|^2.0.1", "nunomaduro/termwind": "^2.1.0",
"pestphp/pest-plugin": "^2.1.1", "pestphp/pest-plugin": "^3.0.0",
"pestphp/pest-plugin-arch": "^2.7.0", "pestphp/pest-plugin-arch": "^3.0.0",
"phpunit/phpunit": "^10.5.17" "pestphp/pest-plugin-mutate": "^3.0.5",
"phpunit/phpunit": "^11.4.1"
}, },
"conflict": { "conflict": {
"phpunit/phpunit": ">10.5.17", "phpunit/phpunit": ">11.4.1",
"sebastian/exporter": "<5.1.0", "sebastian/exporter": "<6.0.0",
"webmozart/assert": "<1.11.0" "webmozart/assert": "<1.11.0"
}, },
"autoload": { "autoload": {
@ -51,9 +52,9 @@
] ]
}, },
"require-dev": { "require-dev": {
"pestphp/pest-dev-tools": "^2.16.0", "pestphp/pest-dev-tools": "^3.0.0",
"pestphp/pest-plugin-type-coverage": "^2.8.5", "pestphp/pest-plugin-type-coverage": "^3.1.0",
"symfony/process": "^6.4.0|^7.1.3" "symfony/process": "^7.1.5"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,
@ -77,7 +78,7 @@
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact", "test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml", "test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
"test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3", "test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3",
"test:integration": "php bin/pest --colors=always --group=integration", "test:integration": "php bin/pest --colors=always --group=integration -v",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
"test": [ "test": [
"@test:refacto", "@test:refacto",
@ -92,6 +93,8 @@
"extra": { "extra": {
"pest": { "pest": {
"plugins": [ "plugins": [
"Pest\\Mutate\\Plugins\\Mutate",
"Pest\\Plugins\\Configuration",
"Pest\\Plugins\\Bail", "Pest\\Plugins\\Bail",
"Pest\\Plugins\\Cache", "Pest\\Plugins\\Cache",
"Pest\\Plugins\\Coverage", "Pest\\Plugins\\Coverage",

View File

@ -1,5 +1,37 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
@ -22,7 +54,7 @@ use PHPUnit\Util\ThrowableToStringMapper;
/** /**
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
final class ThrowableBuilder final readonly class ThrowableBuilder
{ {
/** /**
* @throws Exception * @throws Exception

View File

@ -308,7 +308,6 @@ final class JunitXmlLogger
new TestFinishedSubscriber($this), new TestFinishedSubscriber($this),
new TestErroredSubscriber($this), new TestErroredSubscriber($this),
new TestFailedSubscriber($this), new TestFailedSubscriber($this),
new TestMarkedIncompleteSubscriber($this),
new TestSkippedSubscriber($this), new TestSkippedSubscriber($this),
new TestRunnerExecutionFinishedSubscriber($this), new TestRunnerExecutionFinishedSubscriber($this),
); );

View File

@ -32,19 +32,27 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Runner\Filter; namespace PHPUnit\Runner\Filter;
use Exception;
use Pest\Contracts\HasPrintableTestCaseName; use Pest\Contracts\HasPrintableTestCaseName;
use PHPUnit\Framework\SelfDescribing;
use PHPUnit\Framework\Test; use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\PhptTestCase;
use RecursiveFilterIterator; use RecursiveFilterIterator;
use RecursiveIterator; use RecursiveIterator;
use function end; use function end;
use function implode;
use function preg_match; use function preg_match;
use function sprintf; use function sprintf;
use function str_replace; use function str_replace;
@ -52,22 +60,30 @@ use function str_replace;
/** /**
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
final class NameFilterIterator extends RecursiveFilterIterator abstract class NameFilterIterator extends RecursiveFilterIterator
{ {
private ?string $filter = null; /**
* @psalm-var non-empty-string
*/
private readonly string $regularExpression;
private ?int $filterMin = null; private readonly ?int $dataSetMinimum;
private ?int $filterMax = null; private readonly ?int $dataSetMaximum;
/** /**
* @throws Exception * @psalm-param RecursiveIterator<int, Test> $iterator
* @psalm-param non-empty-string $filter
*/ */
public function __construct(RecursiveIterator $iterator, string $filter) public function __construct(RecursiveIterator $iterator, string $filter)
{ {
parent::__construct($iterator); parent::__construct($iterator);
$this->setFilter($filter); $preparedFilter = $this->prepareFilter($filter);
$this->regularExpression = $preparedFilter['regularExpression'];
$this->dataSetMinimum = $preparedFilter['dataSetMinimum'];
$this->dataSetMaximum = $preparedFilter['dataSetMaximum'];
} }
public function accept(): bool public function accept(): bool
@ -78,29 +94,38 @@ final class NameFilterIterator extends RecursiveFilterIterator
return true; return true;
} }
$tmp = $this->describe($test); if ($test instanceof PhptTestCase) {
return false;
}
if ($tmp[0] !== '') { if ($test instanceof HasPrintableTestCaseName) {
$name = implode('::', $tmp); $name = $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName();
} else { } else {
$name = $tmp[1]; $name = $test::class.'::'.$test->nameWithDataSet();
} }
$accepted = @preg_match($this->filter, $name, $matches); $accepted = @preg_match($this->regularExpression, $name, $matches) === 1;
if ($accepted && isset($this->filterMax)) { if ($accepted && isset($this->dataSetMaximum)) {
$set = end($matches); $set = end($matches);
$accepted = $set >= $this->filterMin && $set <= $this->filterMax; $accepted = $set >= $this->dataSetMinimum && $set <= $this->dataSetMaximum;
} }
return (bool) $accepted; return $this->doAccept($accepted);
} }
abstract protected function doAccept(bool $result): bool;
/** /**
* @throws Exception * @psalm-param non-empty-string $filter
*
* @psalm-return array{regularExpression: non-empty-string, dataSetMinimum: ?int, dataSetMaximum: ?int}
*/ */
private function setFilter(string $filter): void private function prepareFilter(string $filter): array
{ {
$dataSetMinimum = null;
$dataSetMaximum = null;
if (@preg_match($filter, '') === false) { if (@preg_match($filter, '') === false) {
// Handles: // Handles:
// * testAssertEqualsSucceeds#4 // * testAssertEqualsSucceeds#4
@ -108,17 +133,17 @@ final class NameFilterIterator extends RecursiveFilterIterator
if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) { if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) {
if (isset($matches[3]) && $matches[2] < $matches[3]) { if (isset($matches[3]) && $matches[2] < $matches[3]) {
$filter = sprintf( $filter = sprintf(
'%s.*with dataset #(\d+)$', '%s.*with data set #(\d+)$',
$matches[1] $matches[1],
); );
$this->filterMin = (int) $matches[2]; $dataSetMinimum = (int) $matches[2];
$this->filterMax = (int) $matches[3]; $dataSetMaximum = (int) $matches[3];
} else { } else {
$filter = sprintf( $filter = sprintf(
'%s.*with dataset #%s$', '%s.*with data set #%s$',
$matches[1], $matches[1],
$matches[2] $matches[2],
); );
} }
} // Handles: } // Handles:
@ -126,9 +151,9 @@ final class NameFilterIterator extends RecursiveFilterIterator
// * testDetermineJsonError@JSON.* // * testDetermineJsonError@JSON.*
elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) { elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) {
$filter = sprintf( $filter = sprintf(
'%s.*with dataset "%s"$', '%s.*with data set "%s"$',
$matches[1], $matches[1],
$matches[2] $matches[2],
); );
} }
@ -139,34 +164,15 @@ final class NameFilterIterator extends RecursiveFilterIterator
str_replace( str_replace(
'/', '/',
'\\/', '\\/',
$filter $filter,
) ),
); );
} }
$this->filter = $filter; return [
} 'regularExpression' => $filter,
'dataSetMinimum' => $dataSetMinimum,
/** 'dataSetMaximum' => $dataSetMaximum,
* @psalm-return array{0: string, 1: string} ];
*/
private function describe(Test $test): array
{
if ($test instanceof HasPrintableTestCaseName) {
return [
$test::getPrintableTestCaseName(),
$test->getPrintableTestCaseMethodName(),
];
}
if ($test instanceof TestCase) {
return [$test::class, $test->nameWithDataSet()];
}
if ($test instanceof SelfDescribing) {
return ['', $test->toString()];
}
return ['', $test::class];
} }
} }

View File

@ -59,6 +59,7 @@ use function file_get_contents;
use function file_put_contents; use function file_put_contents;
use function is_array; use function is_array;
use function is_dir; use function is_dir;
use function is_file;
use function json_decode; use function json_decode;
use function json_encode; use function json_encode;
use function Pest\version; use function Pest\version;
@ -80,11 +81,6 @@ final class DefaultResultCache implements ResultCache
*/ */
private array $defects = []; private array $defects = [];
/**
* @psalm-var array<string, TestStatus>
*/
private array $currentDefects = [];
/** /**
* @psalm-var array<string, float> * @psalm-var array<string, float>
*/ */
@ -101,10 +97,11 @@ final class DefaultResultCache implements ResultCache
public function setStatus(string $id, TestStatus $status): void public function setStatus(string $id, TestStatus $status): void
{ {
if ($status->isFailure() || $status->isError()) { if ($status->isSuccess()) {
$this->currentDefects[$id] = $status; return;
$this->defects[$id] = $status;
} }
$this->defects[$id] = $status;
} }
public function status(string $id): TestStatus public function status(string $id): TestStatus
@ -114,10 +111,6 @@ final class DefaultResultCache implements ResultCache
public function setTime(string $id, float $time): void public function setTime(string $id, float $time): void
{ {
if (! isset($this->currentDefects[$id])) {
unset($this->defects[$id]);
}
$this->times[$id] = $time; $this->times[$id] = $time;
} }
@ -128,7 +121,11 @@ final class DefaultResultCache implements ResultCache
public function load(): void public function load(): void
{ {
$contents = @file_get_contents($this->cacheFilename); if (! is_file($this->cacheFilename)) {
return;
}
$contents = file_get_contents($this->cacheFilename);
if ($contents === false) { if ($contents === false) {
return; return;
@ -184,7 +181,7 @@ final class DefaultResultCache implements ResultCache
file_put_contents( file_put_contents(
$this->cacheFilename, $this->cacheFilename,
json_encode($data), json_encode($data),
LOCK_EX LOCK_EX,
); );
} }

View File

@ -38,11 +38,13 @@ namespace PHPUnit\Runner;
use Exception; use Exception;
use Pest\Contracts\HasPrintableTestCaseName; use Pest\Contracts\HasPrintableTestCaseName;
use Pest\Panic;
use Pest\TestCases\IgnorableTestCase; use Pest\TestCases\IgnorableTestCase;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use ReflectionClass; use ReflectionClass;
use ReflectionException; use ReflectionException;
use Throwable;
use function array_diff; use function array_diff;
use function array_values; use function array_values;
@ -86,7 +88,11 @@ final class TestSuiteLoader
$suiteClassName = $this->classNameFromFileName($suiteClassFile); $suiteClassName = $this->classNameFromFileName($suiteClassFile);
(static function () use ($suiteClassFile) { (static function () use ($suiteClassFile) {
include_once $suiteClassFile; try {
include_once $suiteClassFile;
} catch (Throwable $e) {
Panic::with($e);
}
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile); TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
})(); })();

View File

@ -45,6 +45,8 @@ declare(strict_types=1);
namespace PHPUnit\TextUI\Command; namespace PHPUnit\TextUI\Command;
use const PHP_EOL;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry; use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Configuration\Configuration;
use PHPUnit\TextUI\Configuration\NoCoverageCacheDirectoryException; use PHPUnit\TextUI\Configuration\NoCoverageCacheDirectoryException;
@ -55,11 +57,11 @@ use SebastianBergmann\Timer\Timer;
/** /**
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
final class WarmCodeCoverageCacheCommand implements Command final readonly class WarmCodeCoverageCacheCommand implements Command
{ {
private readonly Configuration $configuration; private Configuration $configuration;
private readonly CodeCoverageFilterRegistry $codeCoverageFilterRegistry; private CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
public function __construct(Configuration $configuration, CodeCoverageFilterRegistry $codeCoverageFilterRegistry) public function __construct(Configuration $configuration, CodeCoverageFilterRegistry $codeCoverageFilterRegistry)
{ {
@ -76,16 +78,16 @@ final class WarmCodeCoverageCacheCommand implements Command
if (! $this->configuration->hasCoverageCacheDirectory()) { if (! $this->configuration->hasCoverageCacheDirectory()) {
return Result::from( return Result::from(
'Cache for static analysis has not been configured'.PHP_EOL, 'Cache for static analysis has not been configured'.PHP_EOL,
Result::FAILURE Result::FAILURE,
); );
} }
$this->codeCoverageFilterRegistry->init($this->configuration); $this->codeCoverageFilterRegistry->init($this->configuration, true);
if (! $this->codeCoverageFilterRegistry->configured()) { if (! $this->codeCoverageFilterRegistry->configured()) {
return Result::from( return Result::from(
'Filter for code coverage has not been configured'.PHP_EOL, 'Filter for code coverage has not been configured'.PHP_EOL,
Result::FAILURE Result::FAILURE,
); );
} }
@ -96,7 +98,7 @@ final class WarmCodeCoverageCacheCommand implements Command
$this->configuration->coverageCacheDirectory(), $this->configuration->coverageCacheDirectory(),
! $this->configuration->disableCodeCoverageIgnore(), ! $this->configuration->disableCodeCoverageIgnore(),
$this->configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage(), $this->configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage(),
$this->codeCoverageFilterRegistry->get() $this->codeCoverageFilterRegistry->get(),
); );
return Result::from(); return Result::from();

View File

@ -43,7 +43,7 @@ declare(strict_types=1);
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\Test\SkippedSubscriber; use PHPUnit\Event\Test\SkippedSubscriber;
@ -51,21 +51,16 @@ use ReflectionClass;
/** /**
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
*
* This file is overridden to allow Pest Parallel to show todo items in the progress output.
*/ */
final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
{ {
/**
* Notifies the printer that a test was skipped.
*/
public function notify(Skipped $event): void public function notify(Skipped $event): void
{ {
if (str_contains($event->message(), '__TODO__')) { if (str_contains($event->message(), '__TODO__')) {
$this->printTodoItem(); $this->printTodoItem();
} }
$this->printer()->testSkipped(); $this->logger()->testSkipped($event);
} }
/** /**

View File

@ -33,7 +33,6 @@
*/ */
declare(strict_types=1); declare(strict_types=1);
/* /*
* This file is part of PHPUnit. * This file is part of PHPUnit.
* *
@ -57,7 +56,7 @@ use function array_map;
/** /**
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
final class TestSuiteFilterProcessor final readonly class TestSuiteFilterProcessor
{ {
/** /**
* @throws Event\RuntimeException * @throws Event\RuntimeException
@ -70,24 +69,24 @@ final class TestSuiteFilterProcessor
if (! $configuration->hasFilter() && if (! $configuration->hasFilter() &&
! $configuration->hasGroups() && ! $configuration->hasGroups() &&
! $configuration->hasExcludeGroups() && ! $configuration->hasExcludeGroups() &&
! $configuration->hasExcludeFilter() &&
! $configuration->hasTestsCovering() && ! $configuration->hasTestsCovering() &&
! $configuration->hasTestsUsing() && ! $configuration->hasTestsUsing() &&
! Only::isEnabled() ! Only::isEnabled()) {
) {
return; return;
} }
if ($configuration->hasExcludeGroups()) { if ($configuration->hasExcludeGroups()) {
$factory->addExcludeGroupFilter( $factory->addExcludeGroupFilter(
$configuration->excludeGroups() $configuration->excludeGroups(),
); );
} }
if (Only::isEnabled()) { if (Only::isEnabled()) {
$factory->addIncludeGroupFilter(['__pest_only']); $factory->addIncludeGroupFilter([Only::group()]);
} elseif ($configuration->hasGroups()) { } elseif ($configuration->hasGroups()) {
$factory->addIncludeGroupFilter( $factory->addIncludeGroupFilter(
$configuration->groups() $configuration->groups(),
); );
} }
@ -95,8 +94,8 @@ final class TestSuiteFilterProcessor
$factory->addIncludeGroupFilter( $factory->addIncludeGroupFilter(
array_map( array_map(
static fn (string $name): string => '__phpunit_covers_'.$name, static fn (string $name): string => '__phpunit_covers_'.$name,
$configuration->testsCovering() $configuration->testsCovering(),
) ),
); );
} }
@ -104,21 +103,27 @@ final class TestSuiteFilterProcessor
$factory->addIncludeGroupFilter( $factory->addIncludeGroupFilter(
array_map( array_map(
static fn (string $name): string => '__phpunit_uses_'.$name, static fn (string $name): string => '__phpunit_uses_'.$name,
$configuration->testsUsing() $configuration->testsUsing(),
) ),
);
}
if ($configuration->hasExcludeFilter()) {
$factory->addExcludeNameFilter(
$configuration->excludeFilter(),
); );
} }
if ($configuration->hasFilter()) { if ($configuration->hasFilter()) {
$factory->addNameFilter( $factory->addIncludeNameFilter(
$configuration->filter() $configuration->filter(),
); );
} }
$suite->injectFilter($factory); $suite->injectFilter($factory);
Event\Facade::emitter()->testSuiteFiltered( Event\Facade::emitter()->testSuiteFiltered(
Event\TestSuite\TestSuiteBuilder::from($suite) Event\TestSuite\TestSuiteBuilder::from($suite),
); );
} }
} }

View File

@ -2,30 +2,22 @@
declare(strict_types=1); declare(strict_types=1);
use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
use Rector\Config\RectorConfig; use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
use Rector\Set\ValueObject\SetList;
return static function (RectorConfig $rectorConfig): void { return RectorConfig::configure()
$rectorConfig->paths([ ->withPaths([
__DIR__.'/src', __DIR__.'/src',
]); ])
->withSkip([
$rectorConfig->rules([
InlineConstructorDefaultToPropertyRector::class,
]);
$rectorConfig->skip([
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php', __DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
]); ReturnNeverTypeRector::class,
])
$rectorConfig->sets([ ->withPreparedSets(
LevelSetList::UP_TO_PHP_81, deadCode: true,
SetList::CODE_QUALITY, codeQuality: true,
SetList::DEAD_CODE, typeDeclarations: true,
SetList::EARLY_RETURN, privatization: true,
SetList::TYPE_DECLARATION, earlyReturn: true,
SetList::PRIVATIZATION, )
]); ->withPhpSets();
};

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Default">
<directory>tests/</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
<directory>src</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/**
* @internal
*/
abstract class AbstractPreset // @pest-arch-ignore-line
{
/**
* The expectations.
*
* @var array<int, Expectation<mixed>|ArchExpectation>
*/
protected array $expectations = [];
/**
* Creates a new preset instance.
*
* @param array<int, string> $userNamespaces
*/
public function __construct(
private readonly array $userNamespaces,
) {
//
}
/**
* Executes the arch preset.
*
* @internal
*/
abstract public function execute(): void;
/**
* Ignores the given "targets" or "dependencies".
*
* @param array<int, string>|string $targetsOrDependencies
*/
final public function ignoring(array|string $targetsOrDependencies): void
{
$this->expectations = array_map(
fn (ArchExpectation|Expectation $expectation): Expectation|ArchExpectation => $expectation instanceof ArchExpectation ? $expectation->ignoring($targetsOrDependencies) : $expectation,
$this->expectations,
);
}
/**
* Runs the given callback for each namespace.
*
* @param callable(Expectation<string|null>): ArchExpectation ...$callbacks
*/
final public function eachUserNamespace(callable ...$callbacks): void
{
foreach ($this->userNamespaces as $namespace) {
foreach ($callbacks as $callback) {
$this->expectations[] = $callback(expect($namespace));
}
}
}
/**
* Flushes the expectations.
*/
final public function flush(): void
{
$this->expectations = [];
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
use Closure;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/**
* @internal
*/
final class Custom extends AbstractPreset
{
/**
* Creates a new preset instance.
*
* @param array<int, string> $userNamespaces
* @param Closure(array<int, string>): array<Expectation<mixed>|ArchExpectation> $execute
*/
public function __construct(
private readonly array $userNamespaces,
private readonly string $name,
private readonly Closure $execute,
) {
parent::__construct($userNamespaces);
}
/**
* Returns the name of the preset.
*/
public function name(): string
{
return $this->name;
}
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->expectations = ($this->execute)($this->userNamespaces);
}
}

166
src/ArchPresets/Laravel.php Normal file
View File

@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
use Throwable;
/**
* @internal
*/
final class Laravel extends AbstractPreset
{
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->expectations[] = expect('App\Traits')
->toBeTraits();
$this->expectations[] = expect('App\Concerns')
->toBeTraits();
$this->expectations[] = expect('App')
->not->toBeEnums()
->ignoring('App\Enums');
$this->expectations[] = expect('App\Enums')
->toBeEnums()
->ignoring('App\Enums\Concerns');
$this->expectations[] = expect('App\Features')
->toBeClasses()
->ignoring('App\Features\Concerns');
$this->expectations[] = expect('App\Features')
->toHaveMethod('resolve');
$this->expectations[] = expect('App\Exceptions')
->classes()
->toImplement('Throwable')
->ignoring('App\Exceptions\Handler');
$this->expectations[] = expect('App')
->not->toImplement(Throwable::class)
->ignoring('App\Exceptions');
$this->expectations[] = expect('App\Http\Middleware')
->classes()
->toHaveMethod('handle');
$this->expectations[] = expect('App\Models')
->classes()
->toExtend('Illuminate\Database\Eloquent\Model')
->ignoring('App\Models\Scopes');
$this->expectations[] = expect('App\Models')
->classes()
->not->toHaveSuffix('Model');
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Database\Eloquent\Model')
->ignoring('App\Models');
$this->expectations[] = expect('App\Http\Requests')
->classes()
->toHaveSuffix('Request');
$this->expectations[] = expect('App\Http\Requests')
->toExtend('Illuminate\Foundation\Http\FormRequest');
$this->expectations[] = expect('App\Http\Requests')
->toHaveMethod('rules');
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Foundation\Http\FormRequest')
->ignoring('App\Http\Requests');
$this->expectations[] = expect('App\Console\Commands')
->classes()
->toHaveSuffix('Command');
$this->expectations[] = expect('App\Console\Commands')
->classes()
->toExtend('Illuminate\Console\Command');
$this->expectations[] = expect('App\Console\Commands')
->classes()
->toHaveMethod('handle');
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Console\Command')
->ignoring('App\Console\Commands');
$this->expectations[] = expect('App\Mail')
->classes()
->toExtend('Illuminate\Mail\Mailable');
$this->expectations[] = expect('App\Mail')
->classes()
->toImplement('Illuminate\Contracts\Queue\ShouldQueue');
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Mail\Mailable')
->ignoring('App\Mail');
$this->expectations[] = expect('App\Jobs')
->classes()
->toImplement('Illuminate\Contracts\Queue\ShouldQueue');
$this->expectations[] = expect('App\Jobs')
->classes()
->toHaveMethod('handle');
$this->expectations[] = expect('App\Listeners')
->toHaveMethod('handle');
$this->expectations[] = expect('App\Notifications')
->toExtend('Illuminate\Notifications\Notification');
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Notifications\Notification')
->ignoring('App\Notifications');
$this->expectations[] = expect('App\Providers')
->toHaveSuffix('ServiceProvider');
$this->expectations[] = expect('App\Providers')
->toExtend('Illuminate\Support\ServiceProvider');
$this->expectations[] = expect('App\Providers')
->not->toBeUsed();
$this->expectations[] = expect('App')
->not->toExtend('Illuminate\Support\ServiceProvider')
->ignoring('App\Providers');
$this->expectations[] = expect('App')
->not->toHaveSuffix('ServiceProvider')
->ignoring('App\Providers');
$this->expectations[] = expect('App')
->not->toHaveSuffix('Controller')
->ignoring('App\Http\Controllers');
$this->expectations[] = expect('App\Http\Controllers')
->classes()
->toHaveSuffix('Controller');
$this->expectations[] = expect('App\Http')
->toOnlyBeUsedIn('App\Http');
$this->expectations[] = expect('App\Http\Controllers')
->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy', 'middleware']);
$this->expectations[] = expect([
'dd',
'ddd',
'dump',
'env',
'exit',
'ray',
])->not->toBeUsed();
}
}

93
src/ArchPresets/Php.php Normal file
View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
/**
* @internal
*/
final class Php extends AbstractPreset
{
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->expectations[] = expect([
'debug_zval_dump',
'debug_backtrace',
'debug_print_backtrace',
'dump',
'ray',
'ds',
'die',
'goto',
'global',
'var_dump',
'phpinfo',
'echo',
'ereg',
'eregi',
'mysql_connect',
'mysql_pconnect',
'mysql_query',
'mysql_select_db',
'mysql_fetch_array',
'mysql_fetch_assoc',
'mysql_fetch_object',
'mysql_fetch_row',
'mysql_num_rows',
'mysql_affected_rows',
'mysql_free_result',
'mysql_insert_id',
'mysql_error',
'mysql_real_escape_string',
'print',
'print_r',
'var_export',
'xdebug_break',
'xdebug_call_class',
'xdebug_call_file',
'xdebug_call_int',
'xdebug_call_line',
'xdebug_code_coverage_started',
'xdebug_connect_to_client',
'xdebug_debug_zval',
'xdebug_debug_zval_stdout',
'xdebug_dump_superglobals',
'xdebug_get_code_coverage',
'xdebug_get_collected_errors',
'xdebug_get_function_count',
'xdebug_get_function_stack',
'xdebug_get_gc_run_count',
'xdebug_get_gc_total_collected_roots',
'xdebug_get_gcstats_filename',
'xdebug_get_headers',
'xdebug_get_monitored_functions',
'xdebug_get_profiler_filename',
'xdebug_get_stack_depth',
'xdebug_get_tracefile_name',
'xdebug_info',
'xdebug_is_debugger_active',
'xdebug_memory_usage',
'xdebug_notify',
'xdebug_peak_memory_usage',
'xdebug_print_function_stack',
'xdebug_set_filter',
'xdebug_start_code_coverage',
'xdebug_start_error_collection',
'xdebug_start_function_monitor',
'xdebug_start_gcstats',
'xdebug_start_trace',
'xdebug_stop_code_coverage',
'xdebug_stop_error_collection',
'xdebug_stop_function_monitor',
'xdebug_stop_gcstats',
'xdebug_stop_trace',
'xdebug_time_index',
'xdebug_var_dump',
'trap',
])->not->toBeUsed();
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/**
* @internal
*/
final class Relaxed extends AbstractPreset
{
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->eachUserNamespace(
fn (Expectation $namespace): ArchExpectation => $namespace->not->toUseStrictTypes(),
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeFinal(),
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHavePrivateMethods(),
);
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
/**
* @internal
*/
final class Security extends AbstractPreset
{
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->expectations[] = expect([
'md5',
'sha1',
'uniqid',
'rand',
'mt_rand',
'tempnam',
'str_shuffle',
'shuffle',
'array_rand',
'eval',
'exec',
'shell_exec',
'system',
'passthru',
'create_function',
'unserialize',
'extract',
'parse_str',
'mb_parse_str',
'dl',
'assert',
])->not->toBeUsed();
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/**
* @internal
*/
final class Strict extends AbstractPreset
{
/**
* Executes the arch preset.
*/
public function execute(): void
{
$this->eachUserNamespace(
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHaveProtectedMethods(),
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeAbstract(),
fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictTypes(),
fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictEquality(),
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->toBeFinal(),
);
$this->expectations[] = expect([
'sleep',
'usleep',
])->not->toBeUsed();
}
}

View File

@ -12,13 +12,13 @@ use Symfony\Component\Console\Output\OutputInterface;
/** /**
* @internal * @internal
*/ */
final class BootKernelDump implements Bootstrapper final readonly class BootKernelDump implements Bootstrapper
{ {
/** /**
* Creates a new Boot Kernel Dump instance. * Creates a new Boot Kernel Dump instance.
*/ */
public function __construct( public function __construct(
private readonly OutputInterface $output, private OutputInterface $output,
) { ) {
// ... // ...
} }

View File

@ -18,14 +18,14 @@ final class BootOverrides implements Bootstrapper
* @var array<string, string> * @var array<string, string>
*/ */
public const FILES = [ public const FILES = [
'c7b9c8a96006dea314204a8f09a8764e51ce0b9b79aadd58da52e8c328db4870' => 'Runner/Filter/NameFilterIterator.php', '53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php',
'c7c09ab7c9378710b27f761a4b2948196cbbdf2a73e4389bcdca1e7c94fa9c21' => 'Runner/ResultCache/DefaultResultCache.php', 'a4a43de01f641c6944ee83d963795a46d32b5206b5ab3bbc6cce76e67190acbf' => 'Runner/ResultCache/DefaultResultCache.php',
'bc8718c89264f65800beabc23e51c6d3bcff87dfc764a12179ef5dbfde272c8b' => 'Runner/TestSuiteLoader.php', 'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php',
'f41e48d6cb546772a7de4f8e66b6b7ce894a5318d063eb52e354d206e96c701c' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', '3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
'cb7519f2d82893640b694492cf7ec9528da80773cc1d259634181b5d393528b5' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', '8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
'2f06e4b1a9f3a24145bfc7ea25df4f124117f940a2cde30a04d04d5678006bff' => 'TextUI/TestSuiteFilterProcessor.php', 'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php',
'ef64a657ed9c0067791483784944107827bf227c7e3200f212b6751876b99e25' => 'Event/Value/ThrowableBuilder.php', '357d5cd7007f8559b26e1b8cdf43bb6fb15b51b79db981779da6f31b7ec39dad' => 'Event/Value/ThrowableBuilder.php',
'c78f96e34b98ed01dd8106539d59b8aa8d67f733274118b827c01c5c4111c033' => 'Logging/JUnit/JunitXmlLogger.php', '676273f1fe483877cf2d95c5aedbf9ae5d6a8e2f4c12d6ce716df6591e6db023' => 'Logging/JUnit/JunitXmlLogger.php',
]; ];
/** /**

View File

@ -13,7 +13,7 @@ use PHPUnit\Event\Subscriber;
/** /**
* @internal * @internal
*/ */
final class BootSubscribers implements Bootstrapper final readonly class BootSubscribers implements Bootstrapper
{ {
/** /**
* The list of Subscribers. * The list of Subscribers.
@ -31,7 +31,7 @@ final class BootSubscribers implements Bootstrapper
* Creates a new instance of the Boot Subscribers. * Creates a new instance of the Boot Subscribers.
*/ */
public function __construct( public function __construct(
private readonly Container $container, private Container $container,
) {} ) {}
/** /**

View File

@ -11,13 +11,13 @@ use Symfony\Component\Console\Output\OutputInterface;
/** /**
* @internal * @internal
*/ */
final class BootView implements Bootstrapper final readonly class BootView implements Bootstrapper
{ {
/** /**
* Creates a new instance of the Boot View. * Creates a new instance of the Boot View.
*/ */
public function __construct( public function __construct(
private readonly OutputInterface $output private OutputInterface $output
) { ) {
// .. // ..
} }

100
src/Collision/Events.php Normal file
View File

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Pest\Collision;
use NunoMaduro\Collision\Adapters\Phpunit\TestResult;
use Pest\Configuration\Project;
use Symfony\Component\Console\Output\OutputInterface;
use function Termwind\render;
use function Termwind\renderUsing;
/**
* @internal
*/
final class Events
{
/**
* Sets the output.
*/
private static ?OutputInterface $output = null;
/**
* Sets the output.
*/
public static function setOutput(OutputInterface $output): void
{
self::$output = $output;
}
/**
* Fires before the test method description is printed.
*/
public static function beforeTestMethodDescription(TestResult $result, string $description): string
{
if (($context = $result->context) === []) {
return $description;
}
renderUsing(self::$output);
[
'assignees' => $assignees,
'issues' => $issues,
'prs' => $prs,
] = $context;
if (($link = Project::getInstance()->issues) !== '') {
$issuesDescription = array_map(fn (int $issue): string => sprintf('<a href="%s">#%s</a>', sprintf($link, $issue), $issue), $issues);
}
if (($link = Project::getInstance()->prs) !== '') {
$prsDescription = array_map(fn (int $pr): string => sprintf('<a href="%s">#%s</a>', sprintf($link, $pr), $pr), $prs);
}
if (($link = Project::getInstance()->assignees) !== '' && count($assignees) > 0) {
$assigneesDescription = array_map(fn (string $assignee): string => sprintf(
'<a href="%s">@%s</a>',
sprintf($link, $assignee),
$assignee,
), $assignees);
}
if (count($assignees) > 0 || count($issues) > 0 || count($prs) > 0) {
$description .= ' '.implode(', ', array_merge(
$issuesDescription ?? [],
$prsDescription ?? [],
isset($assigneesDescription) ? ['['.implode(', ', $assigneesDescription).']'] : [],
));
}
return $description;
}
/**
* Fires after the test method description is printed.
*/
public static function afterTestMethodDescription(TestResult $result): void
{
if (($context = $result->context) === []) {
return;
}
renderUsing(self::$output);
[
'notes' => $notes,
] = $context;
foreach ($notes as $note) {
render(sprintf(<<<'HTML'
<div class="ml-2">
<span class="text-gray"> // %s</span>
</div>
HTML, $note,
));
}
}
}

View File

@ -5,14 +5,17 @@ declare(strict_types=1);
namespace Pest\Concerns; namespace Pest\Concerns;
use Closure; use Closure;
use Pest\Exceptions\DatasetArgsCountMismatch; use Pest\Exceptions\DatasetArgumentsMismatch;
use Pest\Preset;
use Pest\Support\ChainableClosure; use Pest\Support\ChainableClosure;
use Pest\Support\ExceptionTrace; use Pest\Support\ExceptionTrace;
use Pest\Support\Reflection; use Pest\Support\Reflection;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\Attributes\PostCondition;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use ReflectionException; use ReflectionException;
use ReflectionFunction; use ReflectionFunction;
use ReflectionParameter;
use Throwable; use Throwable;
/** /**
@ -32,11 +35,40 @@ trait Testable
*/ */
private static string $__latestDescription; private static string $__latestDescription;
/**
* The test's assignees.
*/
private static array $__latestAssignees = [];
/**
* The test's notes.
*/
private static array $__latestNotes = [];
/**
* The test's issues.
*
* @var array<int, int>
*/
private static array $__latestIssues = [];
/**
* The test's PRs.
*
* @var array<int, int>
*/
private static array $__latestPrs = [];
/** /**
* The test's describing, if any. * The test's describing, if any.
*/ */
public ?string $__describing = null; public ?string $__describing = null;
/**
* Whether the test has ran or not.
*/
public bool $__ran = false;
/** /**
* The test's test closure. * The test's test closure.
*/ */
@ -67,15 +99,6 @@ trait Testable
*/ */
private array $__snapshotChanges = []; private array $__snapshotChanges = [];
/**
* Resets the test case static properties.
*/
public static function flush(): void
{
self::$__beforeAll = null;
self::$__afterAll = null;
}
/** /**
* Creates a new Test Case instance. * Creates a new Test Case instance.
*/ */
@ -88,11 +111,36 @@ trait Testable
if ($test->hasMethod($name)) { if ($test->hasMethod($name)) {
$method = $test->getMethod($name); $method = $test->getMethod($name);
$this->__description = self::$__latestDescription = $method->description; $this->__description = self::$__latestDescription = $method->description;
self::$__latestAssignees = $method->assignees;
self::$__latestNotes = $method->notes;
self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs;
$this->__describing = $method->describing; $this->__describing = $method->describing;
$this->__test = $method->getClosure($this); $this->__test = $method->getClosure();
} }
} }
/**
* Resets the test case static properties.
*/
public static function flush(): void
{
self::$__beforeAll = null;
self::$__afterAll = null;
}
/**
* Adds a new "note" to the Test Case.
*/
public function note(array|string $note): self
{
$note = is_array($note) ? $note : [$note];
self::$__latestNotes = array_merge(self::$__latestNotes, $note);
return $this;
}
/** /**
* Adds a new "setUpBeforeClass" to the Test Case. * Adds a new "setUpBeforeClass" to the Test Case.
*/ */
@ -186,14 +234,22 @@ trait Testable
/** /**
* Gets executed before the Test Case. * Gets executed before the Test Case.
*/ */
protected function setUp(): void protected function setUp(...$arguments): void
{ {
TestSuite::getInstance()->test = $this; TestSuite::getInstance()->test = $this;
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$description = $this->dataName() ? $method->description.' with '.$this->dataName() : $method->description; $method->setUp($this);
$description = htmlspecialchars(html_entity_decode($description), ENT_NOQUOTES);
$description = $method->description;
if ($this->dataName()) {
$description = str_contains((string) $description, ':dataset')
? str_replace(':dataset', str_replace('dataset ', '', $this->dataName()), (string) $description)
: $description.' with '.$this->dataName();
}
$description = htmlspecialchars(html_entity_decode((string) $description), ENT_NOQUOTES);
if ($method->repetitions > 1) { if ($method->repetitions > 1) {
$matches = []; $matches = [];
@ -211,6 +267,10 @@ trait Testable
} }
$this->__description = self::$__latestDescription = $description; $this->__description = self::$__latestDescription = $description;
self::$__latestAssignees = $method->assignees;
self::$__latestNotes = $method->notes;
self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs;
parent::setUp(); parent::setUp();
@ -220,13 +280,13 @@ trait Testable
$beforeEach = ChainableClosure::bound($this->__beforeEach, $beforeEach); $beforeEach = ChainableClosure::bound($this->__beforeEach, $beforeEach);
} }
$this->__callClosure($beforeEach, func_get_args()); $this->__callClosure($beforeEach, $arguments);
} }
/** /**
* Gets executed after the Test Case. * Gets executed after the Test Case.
*/ */
protected function tearDown(): void protected function tearDown(...$arguments): void
{ {
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename); $afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
@ -240,6 +300,9 @@ trait Testable
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);
} }
} }
@ -251,7 +314,7 @@ trait Testable
private function __runTest(Closure $closure, ...$args): mixed private function __runTest(Closure $closure, ...$args): mixed
{ {
$arguments = $this->__resolveTestArguments($args); $arguments = $this->__resolveTestArguments($args);
$this->__ensureDatasetArgumentNumberMatches($arguments); $this->__ensureDatasetArgumentNameAndNumberMatches($arguments);
return $this->__callClosure($closure, $arguments); return $this->__callClosure($closure, $arguments);
} }
@ -266,7 +329,12 @@ trait Testable
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
if ($method->repetitions > 1) { if ($method->repetitions > 1) {
array_shift($arguments); // If the test is repeated, the first argument is the iteration number
// we need to move it to the end of the arguments list
// so that the datasets are the first n arguments
// and the iteration number is the last argument
$firstArgument = array_shift($arguments);
$arguments[] = $firstArgument;
} }
$underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure'); $underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure');
@ -288,7 +356,7 @@ trait Testable
return $arguments; return $arguments;
} }
if (! $arguments[0] instanceof Closure) { if (! isset($arguments[0]) || ! $arguments[0] instanceof Closure) {
return $arguments; return $arguments;
} }
@ -311,9 +379,9 @@ trait Testable
* Ensures dataset items count matches underlying test case required parameters * Ensures dataset items count matches underlying test case required parameters
* *
* @throws ReflectionException * @throws ReflectionException
* @throws DatasetArgsCountMismatch * @throws DatasetArgumentsMismatch
*/ */
private function __ensureDatasetArgumentNumberMatches(array $arguments): void private function __ensureDatasetArgumentNameAndNumberMatches(array $arguments): void
{ {
if ($arguments === []) { if ($arguments === []) {
return; return;
@ -324,11 +392,21 @@ trait Testable
$requiredParametersCount = $testReflection->getNumberOfRequiredParameters(); $requiredParametersCount = $testReflection->getNumberOfRequiredParameters();
$suppliedParametersCount = count($arguments); $suppliedParametersCount = count($arguments);
if ($suppliedParametersCount >= $requiredParametersCount) { $datasetParameterNames = array_keys($arguments);
$testParameterNames = array_map(
fn (ReflectionParameter $reflectionParameter): string => $reflectionParameter->getName(),
array_filter($testReflection->getParameters(), fn (ReflectionParameter $reflectionParameter): bool => ! $reflectionParameter->isOptional()),
);
if (array_diff($testParameterNames, $datasetParameterNames) === []) {
return; return;
} }
throw new DatasetArgsCountMismatch($requiredParametersCount, $suppliedParametersCount); if (isset($testParameterNames[0]) && $suppliedParametersCount >= $requiredParametersCount) {
return;
}
throw new DatasetArgumentsMismatch($requiredParametersCount, $suppliedParametersCount);
} }
/** /**
@ -339,7 +417,15 @@ trait Testable
return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments)); return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
} }
/** @postCondition */ /**
* Uses the given preset on the test.
*/
public function preset(): Preset
{
return new Preset;
}
#[PostCondition]
protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void
{ {
if (count($this->__snapshotChanges) === 0) { if (count($this->__snapshotChanges) === 0) {
@ -380,4 +466,17 @@ trait Testable
{ {
return self::$__latestDescription; return self::$__latestDescription;
} }
/**
* The printable test case method context.
*/
public static function getPrintableContext(): array
{
return [
'assignees' => self::$__latestAssignees,
'issues' => self::$__latestIssues,
'prs' => self::$__latestPrs,
'notes' => self::$__latestNotes,
];
}
} }

114
src/Configuration.php Normal file
View File

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Pest;
use Pest\PendingCalls\UsesCall;
/**
* @internal
*
* @mixin UsesCall
*/
final readonly class Configuration
{
/**
* The filename of the configuration.
*/
private string $filename;
/**
* Creates a new configuration instance.
*/
public function __construct(
string $filename,
) {
$this->filename = str_ends_with($filename, DIRECTORY_SEPARATOR.'Pest.php') ? dirname($filename) : $filename;
}
/**
* Use the given classes and traits in the given targets.
*/
public function in(string ...$targets): UsesCall
{
return (new UsesCall($this->filename, []))->in(...$targets);
}
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function extend(string ...$classAndTraits): UsesCall
{
return new UsesCall(
$this->filename,
array_values($classAndTraits)
);
}
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function extends(string ...$classAndTraits): UsesCall
{
return $this->extend(...$classAndTraits);
}
/**
* Depending on where is called, it will add the given groups globally or locally.
*/
public function group(string ...$groups): UsesCall
{
return (new UsesCall($this->filename, []))->group(...$groups);
}
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function use(string ...$classAndTraits): UsesCall
{
return $this->extend(...$classAndTraits);
}
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function uses(string ...$classAndTraits): UsesCall
{
return $this->extends(...$classAndTraits);
}
/**
* Gets the printer configuration.
*/
public function printer(): Configuration\Printer
{
return new Configuration\Printer;
}
/**
* Gets the presets configuration.
*/
public function presets(): Configuration\Presets
{
return new Configuration\Presets;
}
/**
* Gets the project configuration.
*/
public function project(): Configuration\Project
{
return Configuration\Project::getInstance();
}
/**
* Proxies calls to the uses method.
*
* @param array<array-key, mixed> $arguments
*/
public function __call(string $name, array $arguments): mixed
{
return $this->uses()->$name(...$arguments); // @phpstan-ignore-line
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Configuration;
use Closure;
use Pest\Preset;
final class Presets
{
/**
* Creates a custom preset instance, and adds it to the list of presets.
*/
public function custom(string $name, Closure $execute): void
{
Preset::custom($name, $execute);
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Pest\Configuration;
use NunoMaduro\Collision\Adapters\Phpunit\Printers\DefaultPrinter;
/**
* @internal
*/
final readonly class Printer
{
/**
* Sets the theme to compact.
*/
public function compact(): self
{
DefaultPrinter::compact(true);
return $this;
}
}

View File

@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Pest\Configuration;
/**
* @internal
*/
final class Project
{
/**
* The assignees link.
*
* @internal
*/
public string $assignees = '';
/**
* The issues link.
*
* @internal
*/
public string $issues = '';
/**
* The PRs link.
*
* @internal
*/
public string $prs = '';
/**
* The singleton instance.
*/
private static ?self $instance = null;
/**
* Creates a new instance of the project.
*/
public static function getInstance(): self
{
return self::$instance ??= new self;
}
/**
* Sets the test project to GitHub.
*/
public function github(string $project): self
{
$this->issues = "https://github.com/{$project}/issues/%s";
$this->prs = "https://github.com/{$project}/pull/%s";
$this->assignees = 'https://github.com/%s';
return $this;
}
/**
* Sets the test project to GitLab.
*/
public function gitlab(string $project): self
{
$this->issues = "https://gitlab.com/{$project}/issues/%s";
$this->prs = "https://gitlab.com/{$project}/merge_requests/%s";
$this->assignees = 'https://gitlab.com/%s';
return $this;
}
/**
* Sets the test project to Bitbucket.
*/
public function bitbucket(string $project): self
{
$this->issues = "https://bitbucket.org/{$project}/issues/%s";
$this->prs = "https://bitbucket.org/{$project}/pull-requests/%s";
$this->assignees = 'https://bitbucket.org/%s';
return $this;
}
/**
* Sets the test project to Jira.
*/
public function jira(string $namespace, string $project): self
{
$this->issues = "https://{$namespace}.atlassian.net/browse/{$project}-%s";
$this->assignees = "https://{$namespace}.atlassian.net/secure/ViewProfile.jspa?name=%s";
return $this;
}
/**
* Sets the test project to custom.
*/
public function custom(string $issues, string $prs, string $assignees): self
{
$this->issues = $issues;
$this->prs = $prs;
$this->assignees = $assignees;
return $this;
}
}

View File

@ -9,7 +9,7 @@ use Symfony\Component\Console\Output\OutputInterface;
/** /**
* @internal * @internal
*/ */
final class Help final readonly class Help
{ {
/** /**
* The Command messages. * The Command messages.
@ -27,7 +27,7 @@ final class Help
/** /**
* Creates a new Console Command instance. * Creates a new Console Command instance.
*/ */
public function __construct(private readonly OutputInterface $output) public function __construct(private OutputInterface $output)
{ {
// .. // ..
} }

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Question\ConfirmationQuestion;
/** /**
* @internal * @internal
*/ */
final class Thanks final readonly class Thanks
{ {
/** /**
* The support options. * The support options.
@ -33,8 +33,8 @@ final class Thanks
* Creates a new Console Command instance. * Creates a new Console Command instance.
*/ */
public function __construct( public function __construct(
private readonly InputInterface $input, private InputInterface $input,
private readonly OutputInterface $output private OutputInterface $output
) { ) {
// .. // ..
} }
@ -72,13 +72,13 @@ final class Thanks
} }
if ($wantsToSupport === true) { if ($wantsToSupport === true) {
if (PHP_OS_FAMILY == 'Darwin') { if (PHP_OS_FAMILY === 'Darwin') {
exec('open https://github.com/pestphp/pest'); exec('open https://github.com/pestphp/pest');
} }
if (PHP_OS_FAMILY == 'Windows') { if (PHP_OS_FAMILY === 'Windows') {
exec('start https://github.com/pestphp/pest'); exec('start https://github.com/pestphp/pest');
} }
if (PHP_OS_FAMILY == 'Linux') { if (PHP_OS_FAMILY === 'Linux') {
exec('xdg-open https://github.com/pestphp/pest'); exec('xdg-open https://github.com/pestphp/pest');
} }
} }

View File

@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Contracts;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
interface AddsAnnotations
{
/**
* Adds annotations to the given test case method.
*
* @param array<int, string> $annotations
* @return array<int, string>
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array;
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Pest\Contracts;
/**
* @internal
*/
interface ArchPreset {}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Pest\Evaluators;
use Pest\Factories\Attribute;
/**
* @internal
*/
final class Attributes
{
/**
* Evaluates the given attributes and returns the code.
*
* @param iterable<int, Attribute> $attributes
*/
public static function code(iterable $attributes): string
{
return implode(PHP_EOL, array_map(function (Attribute $attribute): string {
$name = $attribute->name;
if ($attribute->arguments === []) {
return " #[\\{$name}]";
}
$arguments = array_map(fn (string $argument): string => var_export($argument, true), iterator_to_array($attribute->arguments));
return sprintf(' #[\\%s(%s)]', $name, implode(', ', $arguments));
}, iterator_to_array($attributes)));
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use InvalidArgumentException;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Symfony\Component\Console\Exception\ExceptionInterface;
/**
* @internal
*/
final class AfterBeforeTestFunction extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename)
{
parent::__construct('After method cannot be used with before the [test|it] functions in the filename `['.$filename.']`.');
}
}

View File

@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use Exception;
final class DatasetArgsCountMismatch extends Exception
{
public function __construct(int $requiredCount, int $suppliedCount)
{
parent::__construct(sprintf('Test expects %d arguments but dataset only provides %d', $requiredCount, $suppliedCount));
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use Exception;
final class DatasetArgumentsMismatch extends Exception
{
public function __construct(int $requiredCount, int $suppliedCount)
{
if ($requiredCount <= $suppliedCount) {
parent::__construct('Test argument names and dataset keys do not match');
} else {
parent::__construct(sprintf('Test expects %d arguments but dataset only provides %d', $requiredCount, $suppliedCount));
}
}
//
}

View File

@ -30,9 +30,12 @@ use Pest\Expectations\HigherOrderExpectation;
use Pest\Expectations\OppositeExpectation; use Pest\Expectations\OppositeExpectation;
use Pest\Matchers\Any; use Pest\Matchers\Any;
use Pest\Support\ExpectationPipeline; use Pest\Support\ExpectationPipeline;
use Pest\Support\Reflection;
use PHPUnit\Architecture\Elements\ObjectDescription; use PHPUnit\Architecture\Elements\ObjectDescription;
use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\ExpectationFailedException;
use ReflectionEnum; use ReflectionEnum;
use ReflectionMethod;
use ReflectionProperty;
/** /**
* @template TValue * @template TValue
@ -220,7 +223,7 @@ final class Expectation
throw new BadMethodCallException('Expectation value is not iterable.'); throw new BadMethodCallException('Expectation value is not iterable.');
} }
if (count($callbacks) == 0) { if ($callbacks === []) {
throw new InvalidArgumentException('No sequence expectations defined.'); throw new InvalidArgumentException('No sequence expectations defined.');
} }
@ -261,7 +264,7 @@ final class Expectation
$matched = false; $matched = false;
foreach ($expressions as $key => $callback) { foreach ($expressions as $key => $callback) {
if ($subject != $key) { if ($subject != $key) { // @pest-arch-ignore-line
continue; continue;
} }
@ -377,7 +380,7 @@ final class Expectation
if (self::hasExtend($name)) { if (self::hasExtend($name)) {
$extend = self::$extends[$name]->bindTo($this, Expectation::class); $extend = self::$extends[$name]->bindTo($this, Expectation::class);
if ($extend != false) { if ($extend != false) { // @pest-arch-ignore-line
return $extend; return $extend;
} }
} }
@ -434,6 +437,71 @@ final class Expectation
return ToUse::make($this, $targets); return ToUse::make($this, $targets);
} }
/**
* Asserts that the given expectation target does have the given permissions
*/
public function toHaveFileSystemPermissions(string $permissions): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) === $permissions,
sprintf('permissions to be [%s]', $permissions),
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
);
}
/**
* Asserts that the given expectation target to have line count less than the given number.
*/
public function toHaveLineCountLessThan(int $lines): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => count(file($object->path)) < $lines, // @phpstan-ignore-line
sprintf('to have less than %d lines of code', $lines),
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
);
}
/**
* Asserts that the given expectation target have all methods documented.
*/
public function toHaveMethodsDocumented(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| array_filter(
Reflection::getMethodsFromReflectionClass($object->reflectionClass),
fn (ReflectionMethod $method): bool => (enum_exists($object->name) === false || in_array($method->name, ['from', 'tryFrom', 'cases'], true) === false)
&& realpath($method->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
&& $method->getDocComment() === false,
) === [],
'to have methods with documentation / annotations',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
);
}
/**
* Asserts that the given expectation target have all properties documented.
*/
public function toHavePropertiesDocumented(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| array_filter(
Reflection::getPropertiesFromReflectionClass($object->reflectionClass),
fn (ReflectionProperty $property): bool => (enum_exists($object->name) === false || in_array($property->name, ['value', 'name'], true) === false)
&& realpath($property->getDeclaringClass()->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
&& $property->isPromoted() === false
&& $property->getDocComment() === false,
) === [],
'to have properties with documentation / annotations',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
);
}
/** /**
* Asserts that the given expectation target use the "declare(strict_types=1)" declaration. * Asserts that the given expectation target use the "declare(strict_types=1)" declaration.
*/ */
@ -441,12 +509,25 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => (bool) preg_match('/^<\?php\s+declare\(.*?strict_types\s?=\s?1.*?\);/', (string) file_get_contents($object->path)), fn (ObjectDescription $object): bool => (bool) preg_match('/^<\?php\s*(\/\*[\s\S]*?\*\/|\/\/[^\r\n]*(?:\r?\n|$)|\s)*declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;/m', (string) file_get_contents($object->path)),
'to use strict types', 'to use strict types',
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')), FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
); );
} }
/**
* Asserts that the given expectation target uses strict equality.
*/
public function toUseStrictEquality(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' == ') && ! str_contains((string) file_get_contents($object->path), ' != '),
'to use strict equality',
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' == ') || str_contains($line, ' != ')),
);
}
/** /**
* Asserts that the given expectation target is final. * Asserts that the given expectation target is final.
*/ */
@ -509,17 +590,79 @@ final class Expectation
/** /**
* Asserts that the given expectation target has a specific method. * Asserts that the given expectation target has a specific method.
*
* @param array<int, string>|string $method
*/ */
public function toHaveMethod(string $method): ArchExpectation public function toHaveMethod(array|string $method): ArchExpectation
{ {
$methods = is_array($method) ? $method : [$method];
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => $object->reflectionClass->hasMethod($method), fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => $object->reflectionClass->hasMethod($method))) === count($methods),
sprintf("to have method '%s'", $method), sprintf("to have method '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
/**
* Asserts that the given expectation target has a specific methods.
*
* @param array<int, string> $methods
*/
public function toHaveMethods(array $methods): ArchExpectation
{
return $this->toHaveMethod($methods);
}
/**
* Not supported.
*/
public function toHavePublicMethodsBesides(): void
{
throw InvalidExpectation::fromMethods(['toHavePublicMethodsBesides']);
}
/**
* Not supported.
*/
public function toHavePublicMethods(): void
{
throw InvalidExpectation::fromMethods(['toHavePublicMethods']);
}
/**
* Not supported.
*/
public function toHaveProtectedMethodsBesides(): void
{
throw InvalidExpectation::fromMethods(['toHaveProtectedMethodsBesides']);
}
/**
* Not supported.
*/
public function toHaveProtectedMethods(): void
{
throw InvalidExpectation::fromMethods(['toHaveProtectedMethods']);
}
/**
* Not supported.
*/
public function toHavePrivateMethodsBesides(): void
{
throw InvalidExpectation::fromMethods(['toHavePrivateMethodsBesides']);
}
/**
* Not supported.
*/
public function toHavePrivateMethods(): void
{
throw InvalidExpectation::fromMethods(['toHavePrivateMethods']);
}
/** /**
* Asserts that the given expectation target is enum. * Asserts that the given expectation target is enum.
*/ */
@ -585,8 +728,6 @@ final class Expectation
/** /**
* Asserts that the given expectation target to be subclass of the given class. * Asserts that the given expectation target to be subclass of the given class.
*
* @param class-string $class
*/ */
public function toExtend(string $class): ArchExpectation public function toExtend(string $class): ArchExpectation
{ {
@ -611,6 +752,39 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target to use the given trait.
*/
public function toUseTrait(string $trait): ArchExpectation
{
return $this->toUseTraits($trait);
}
/**
* Asserts that the given expectation target to use the given traits.
*
* @param array<int, string>|string $traits
*/
public function toUseTraits(array|string $traits): ArchExpectation
{
$traits = is_array($traits) ? $traits : [$traits];
return Targeted::make(
$this,
function (ObjectDescription $object) use ($traits): bool {
foreach ($traits as $trait) {
if (! in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
return false;
}
}
return true;
},
"to use traits '".implode("', '", $traits)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/** /**
* Asserts that the given expectation target to not implement any interfaces. * Asserts that the given expectation target to not implement any interfaces.
*/ */
@ -627,7 +801,7 @@ final class Expectation
/** /**
* Asserts that the given expectation target to only implement the given interfaces. * Asserts that the given expectation target to only implement the given interfaces.
* *
* @param array<int, class-string>|class-string $interfaces * @param array<int, string>|string $interfaces
*/ */
public function toOnlyImplement(array|string $interfaces): ArchExpectation public function toOnlyImplement(array|string $interfaces): ArchExpectation
{ {
@ -671,7 +845,7 @@ final class Expectation
/** /**
* Asserts that the given expectation target to implement the given interfaces. * Asserts that the given expectation target to implement the given interfaces.
* *
* @param array<int, class-string>|class-string $interfaces * @param array<int, string>|string $interfaces
*/ */
public function toImplement(array|string $interfaces): ArchExpectation public function toImplement(array|string $interfaces): ArchExpectation
{ {
@ -711,7 +885,10 @@ final class Expectation
return ToUseNothing::make($this); return ToUseNothing::make($this);
} }
public function toBeUsed(): never /**
* Not supported.
*/
public function toBeUsed(): void
{ {
throw InvalidExpectation::fromMethods(['toBeUsed']); throw InvalidExpectation::fromMethods(['toBeUsed']);
} }
@ -855,8 +1032,6 @@ final class Expectation
/** /**
* Asserts that the given expectation target to have the given attribute. * Asserts that the given expectation target to have the given attribute.
*
* @param class-string<Attribute> $attribute
*/ */
public function toHaveAttribute(string $attribute): ArchExpectation public function toHaveAttribute(string $attribute): ArchExpectation
{ {

View File

@ -17,6 +17,9 @@ use function expect;
*/ */
final class EachExpectation final class EachExpectation
{ {
/**
* Indicates if the expectation is the opposite.
*/
private bool $opposite = false; private bool $opposite = false;
/** /**

View File

@ -25,8 +25,14 @@ final class HigherOrderExpectation
*/ */
private Expectation|EachExpectation $expectation; private Expectation|EachExpectation $expectation;
/**
* Indicates if the expectation is the opposite.
*/
private bool $opposite = false; private bool $opposite = false;
/**
* Indicates if the expectation should reset the value.
*/
private bool $shouldReset = false; private bool $shouldReset = false;
/** /**

View File

@ -18,9 +18,13 @@ use Pest\Exceptions\InvalidExpectation;
use Pest\Expectation; use Pest\Expectation;
use Pest\Support\Arr; use Pest\Support\Arr;
use Pest\Support\Exporter; use Pest\Support\Exporter;
use Pest\Support\Reflection;
use PHPUnit\Architecture\Elements\ObjectDescription; use PHPUnit\Architecture\Elements\ObjectDescription;
use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\ExpectationFailedException;
use ReflectionMethod;
use ReflectionProperty;
use stdClass;
/** /**
* @internal * @internal
@ -29,14 +33,14 @@ use PHPUnit\Framework\ExpectationFailedException;
* *
* @mixin Expectation<TValue> * @mixin Expectation<TValue>
*/ */
final class OppositeExpectation final readonly class OppositeExpectation
{ {
/** /**
* Creates a new opposite expectation. * Creates a new opposite expectation.
* *
* @param Expectation<TValue> $original * @param Expectation<TValue> $original
*/ */
public function __construct(private readonly Expectation $original) {} public function __construct(private Expectation $original) {}
/** /**
* Asserts that the value array not has the provided $keys. * Asserts that the value array not has the provided $keys.
@ -75,6 +79,66 @@ final class OppositeExpectation
), is_string($targets) ? [$targets] : $targets)); ), is_string($targets) ? [$targets] : $targets));
} }
/**
* Asserts that the given expectation target does not have the given permissions
*/
public function toHaveFileSystemPermissions(string $permissions): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) !== $permissions,
sprintf('permissions not to be [%s]', $permissions),
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
);
}
/**
* Not supported.
*/
public function toHaveLineCountLessThan(): ArchExpectation
{
throw InvalidExpectation::fromMethods(['not', 'toHaveLineCountLessThan']);
}
/**
* Not supported.
*/
public function toHaveMethodsDocumented(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| array_filter(
Reflection::getMethodsFromReflectionClass($object->reflectionClass),
fn (ReflectionMethod $method): bool => (enum_exists($object->name) === false || in_array($method->name, ['from', 'tryFrom', 'cases'], true) === false)
&& realpath($method->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
&& $method->getDocComment() !== false,
) === [],
'to have methods without documentation / annotations',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
);
}
/**
* Not supported.
*/
public function toHavePropertiesDocumented(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| array_filter(
Reflection::getPropertiesFromReflectionClass($object->reflectionClass),
fn (ReflectionProperty $property): bool => (enum_exists($object->name) === false || in_array($property->name, ['value', 'name'], true) === false)
&& realpath($property->getDeclaringClass()->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line
&& $property->isPromoted() === false
&& $property->getDocComment() !== false,
) === [],
'to have properties without documentation / annotations',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
);
}
/** /**
* Asserts that the given expectation target does not use the "declare(strict_types=1)" declaration. * Asserts that the given expectation target does not use the "declare(strict_types=1)" declaration.
*/ */
@ -88,6 +152,19 @@ final class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation target does not use the strict equality operator.
*/
public function toUseStrictEquality(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' === ') && ! str_contains((string) file_get_contents($object->path), ' !== '),
'to use strict equality',
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' === ') || str_contains($line, ' !== ')),
);
}
/** /**
* Asserts that the given expectation target is not final. * Asserts that the given expectation target is not final.
*/ */
@ -150,17 +227,163 @@ final class OppositeExpectation
/** /**
* Asserts that the given expectation target does not have a specific method. * Asserts that the given expectation target does not have a specific method.
*
* @param array<int, string>|string $method
*/ */
public function toHaveMethod(string $method): ArchExpectation public function toHaveMethod(array|string $method): ArchExpectation
{ {
$methods = is_array($method) ? $method : [$method];
return Targeted::make( return Targeted::make(
$this->original, $this->original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod($method), fn (ObjectDescription $object): bool => array_filter(
'to not have method', $methods,
fn (string $method): bool => $object->reflectionClass->hasMethod($method),
) === [],
'to not have methods: '.implode(', ', $methods),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
/**
* Asserts that the given expectation target does not have the given methods.
*
* @param array<int, string> $methods
*/
public function toHaveMethods(array $methods): ArchExpectation
{
return $this->toHaveMethod($methods);
}
/**
* Asserts that the given expectation target not to have the public methods besides the given methods.
*
* @param array<int, string>|string $methods
*/
public function toHavePublicMethodsBesides(array|string $methods): ArchExpectation
{
$methods = is_array($methods) ? $methods : [$methods];
$state = new stdClass;
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PUBLIC)
: [];
foreach ($reflectionMethods as $reflectionMethod) {
if (! in_array($reflectionMethod->name, $methods, true)) {
$state->contains = 'public function '.$reflectionMethod->name;
return false;
}
}
return true;
},
$methods === []
? 'not to have public methods'
: sprintf("not to have public methods besides '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
);
}
/**
* Asserts that the given expectation target not to have the public methods.
*/
public function toHavePublicMethods(): ArchExpectation
{
return $this->toHavePublicMethodsBesides([]);
}
/**
* Asserts that the given expectation target not to have the protected methods besides the given methods.
*
* @param array<int, string>|string $methods
*/
public function toHaveProtectedMethodsBesides(array|string $methods): ArchExpectation
{
$methods = is_array($methods) ? $methods : [$methods];
$state = new stdClass;
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PROTECTED)
: [];
foreach ($reflectionMethods as $reflectionMethod) {
if (! in_array($reflectionMethod->name, $methods, true)) {
$state->contains = 'protected function '.$reflectionMethod->name;
return false;
}
}
return true;
},
$methods === []
? 'not to have protected methods'
: sprintf("not to have protected methods besides '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
);
}
/**
* Asserts that the given expectation target not to have the protected methods.
*/
public function toHaveProtectedMethods(): ArchExpectation
{
return $this->toHaveProtectedMethodsBesides([]);
}
/**
* Asserts that the given expectation target not to have the private methods besides the given methods.
*
* @param array<int, string>|string $methods
*/
public function toHavePrivateMethodsBesides(array|string $methods): ArchExpectation
{
$methods = is_array($methods) ? $methods : [$methods];
$state = new stdClass;
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PRIVATE)
: [];
foreach ($reflectionMethods as $reflectionMethod) {
if (! in_array($reflectionMethod->name, $methods, true)) {
$state->contains = 'private function '.$reflectionMethod->name;
return false;
}
}
return true;
},
$methods === []
? 'not to have private methods'
: sprintf("not to have private methods besides '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
);
}
/**
* Asserts that the given expectation target not to have the private methods.
*/
public function toHavePrivateMethods(): ArchExpectation
{
return $this->toHavePrivateMethodsBesides([]);
}
/** /**
* Asserts that the given expectation target is not enum. * Asserts that the given expectation target is not enum.
*/ */
@ -226,8 +449,6 @@ final class OppositeExpectation
/** /**
* Asserts that the given expectation target to be not subclass of the given class. * Asserts that the given expectation target to be not subclass of the given class.
*
* @param class-string $class
*/ */
public function toExtend(string $class): ArchExpectation public function toExtend(string $class): ArchExpectation
{ {
@ -252,10 +473,43 @@ final class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation target not to use the given trait.
*/
public function toUseTrait(string $trait): ArchExpectation
{
return $this->toUseTraits($trait);
}
/**
* Asserts that the given expectation target not to use the given traits.
*
* @param array<int, string>|string $traits
*/
public function toUseTraits(array|string $traits): ArchExpectation
{
$traits = is_array($traits) ? $traits : [$traits];
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($traits): bool {
foreach ($traits as $trait) {
if (in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
return false;
}
}
return true;
},
"not to use traits '".implode("', '", $traits)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/** /**
* Asserts that the given expectation target not to implement the given interfaces. * Asserts that the given expectation target not to implement the given interfaces.
* *
* @param array<int, class-string>|string $interfaces * @param array<int, string>|string $interfaces
*/ */
public function toImplement(array|string $interfaces): ArchExpectation public function toImplement(array|string $interfaces): ArchExpectation
{ {
@ -292,10 +546,8 @@ final class OppositeExpectation
/** /**
* Not supported. * Not supported.
*
* @param array<int, class-string>|string $interfaces
*/ */
public function toOnlyImplement(array|string $interfaces): never public function toOnlyImplement(): void
{ {
throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']); throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']);
} }
@ -328,10 +580,8 @@ final class OppositeExpectation
/** /**
* Not supported. * Not supported.
*
* @param array<int, string>|string $targets
*/ */
public function toOnlyUse(array|string $targets): never public function toOnlyUse(): void
{ {
throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']); throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']);
} }
@ -339,7 +589,7 @@ final class OppositeExpectation
/** /**
* Not supported. * Not supported.
*/ */
public function toUseNothing(): never public function toUseNothing(): void
{ {
throw InvalidExpectation::fromMethods(['not', 'toUseNothing']); throw InvalidExpectation::fromMethods(['not', 'toUseNothing']);
} }
@ -364,7 +614,7 @@ final class OppositeExpectation
), is_string($targets) ? [$targets] : $targets)); ), is_string($targets) ? [$targets] : $targets));
} }
public function toOnlyBeUsedIn(): never public function toOnlyBeUsedIn(): void
{ {
throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedIn']); throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedIn']);
} }
@ -372,7 +622,7 @@ final class OppositeExpectation
/** /**
* Asserts that the given expectation dependency is not used. * Asserts that the given expectation dependency is not used.
*/ */
public function toBeUsedInNothing(): never public function toBeUsedInNothing(): void
{ {
throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']); throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']);
} }
@ -392,8 +642,6 @@ final class OppositeExpectation
/** /**
* Asserts that the given expectation target not to have the given attribute. * Asserts that the given expectation target not to have the given attribute.
*
* @param class-string<Attribute> $attribute
*/ */
public function toHaveAttribute(string $attribute): ArchExpectation public function toHaveAttribute(string $attribute): ArchExpectation
{ {

View File

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Contracts\AddsAnnotations;
use Pest\Factories\Covers\CoversNothing as CoversNothingFactory;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
final class CoversNothing implements AddsAnnotations
{
/**
* {@inheritdoc}
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{
if (($method->covers[0] ?? null) instanceof CoversNothingFactory) {
$annotations[] = '@coversNothing';
}
return $annotations;
}
}

View File

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Contracts\AddsAnnotations;
use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Str;
/**
* @internal
*/
final class Depends implements AddsAnnotations
{
/**
* {@inheritdoc}
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{
foreach ($method->depends as $depend) {
$depend = Str::evaluable($method->describing !== null ? Str::describe($method->describing, $depend) : $depend);
$annotations[] = "@depends $depend";
}
return $annotations;
}
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Contracts\AddsAnnotations;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
final class Groups implements AddsAnnotations
{
/**
* {@inheritdoc}
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{
foreach ($method->groups as $group) {
$annotations[] = "@group $group";
}
return $annotations;
}
}

View File

@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Contracts\AddsAnnotations;
use Pest\Factories\TestCaseMethodFactory;
final class TestDox implements AddsAnnotations
{
/**
* {@inheritdoc}
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{
/*
* Escapes docblock according to
* https://manual.phpdoc.org/HTMLframesConverter/default/phpDocumentor/tutorial_phpDocumentor.howto.pkg.html#basics.desc
*
* Note: '@' escaping is not needed as it cannot be the first character of the line (it always starts with @testdox).
*/
assert($method->description !== null);
$methodDescription = str_replace('*/', '{@*}', $method->description);
$annotations[] = "@testdox $methodDescription";
return $annotations;
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Factories;
/**
* @internal
*/
final class Attribute
{
/**
* @param iterable<int, string> $arguments
*/
public function __construct(public string $name, public iterable $arguments)
{
//
}
}

View File

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Attributes;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
abstract class Attribute
{
/**
* Determine if the attribute should be placed above the class instead of above the method.
*/
public static bool $above = false;
/**
* @param array<int, string> $attributes
* @return array<int, string>
*/
public function __invoke(TestCaseMethodFactory $method, array $attributes): array // @phpstan-ignore-line
{
return $attributes;
}
}

View File

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Attributes;
use Pest\Factories\Covers\CoversClass;
use Pest\Factories\Covers\CoversFunction;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
final class Covers extends Attribute
{
/**
* Determine if the attribute should be placed above the class instead of above the method.
*/
public static bool $above = true;
/**
* Adds attributes regarding the "covers" feature.
*
* @param array<int, string> $attributes
* @return array<int, string>
*/
public function __invoke(TestCaseMethodFactory $method, array $attributes): array
{
foreach ($method->covers as $covering) {
if ($covering instanceof CoversClass) {
// Prepend a backslash for FQN classes
if (str_contains($covering->class, '\\')) {
$covering->class = '\\'.$covering->class;
}
$attributes[] = "#[\PHPUnit\Framework\Attributes\CoversClass({$covering->class}::class)]";
} elseif ($covering instanceof CoversFunction) {
$attributes[] = "#[\PHPUnit\Framework\Attributes\CoversFunction('{$covering->function}')]";
}
}
return $attributes;
}
}

View File

@ -6,8 +6,8 @@ namespace Pest\Factories;
use ParseError; use ParseError;
use Pest\Concerns; use Pest\Concerns;
use Pest\Contracts\AddsAnnotations;
use Pest\Contracts\HasPrintableTestCaseName; use Pest\Contracts\HasPrintableTestCaseName;
use Pest\Evaluators\Attributes;
use Pest\Exceptions\DatasetMissing; use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
use Pest\Exceptions\TestAlreadyExist; use Pest\Exceptions\TestAlreadyExist;
@ -27,26 +27,12 @@ final class TestCaseFactory
{ {
use HigherOrderable; use HigherOrderable;
/**
* The list of annotations.
*
* @var array<int, class-string<AddsAnnotations>>
*/
private const ANNOTATIONS = [
Annotations\Depends::class,
Annotations\Groups::class,
Annotations\CoversNothing::class,
Annotations\TestDox::class,
];
/** /**
* The list of attributes. * The list of attributes.
* *
* @var array<int, class-string<\Pest\Factories\Attributes\Attribute>> * @var array<int, Attribute>
*/ */
private const ATTRIBUTES = [ public array $attributes = [];
Attributes\Covers::class,
];
/** /**
* The FQN of the Test Case class. * The FQN of the Test Case class.
@ -147,32 +133,21 @@ final class TestCaseFactory
$className = 'InvalidTestName'.Str::random(); $className = 'InvalidTestName'.Str::random();
} }
$classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => $attribute::$above); $this->attributes = [
$methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => ! $attribute::$above); new Attribute(
\PHPUnit\Framework\Attributes\TestDox::class,
[$this->filename],
),
...$this->attributes,
];
$classAttributes = []; $attributesCode = Attributes::code($this->attributes);
foreach ($classAvailableAttributes as $attribute) {
$classAttributes = array_reduce(
$methods,
fn (array $carry, TestCaseMethodFactory $methodFactory): array => (new $attribute)->__invoke($methodFactory, $carry),
$classAttributes
);
}
$methodsCode = implode('', array_map( $methodsCode = implode('', array_map(
fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation( fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(),
self::ANNOTATIONS,
$methodAvailableAttributes
),
$methods $methods
)); ));
$classAttributesCode = implode('', array_map(
static fn (string $attribute): string => sprintf("\n%s", $attribute),
array_unique($classAttributes),
));
try { try {
$classCode = <<<PHP $classCode = <<<PHP
namespace $namespace; namespace $namespace;
@ -180,10 +155,7 @@ final class TestCaseFactory
use Pest\Repositories\DatasetsRepository as __PestDatasets; use Pest\Repositories\DatasetsRepository as __PestDatasets;
use Pest\TestSuite as __PestTestSuite; use Pest\TestSuite as __PestTestSuite;
/** $attributesCode
* @testdox $filename
*/
$classAttributesCode
#[\AllowDynamicProperties] #[\AllowDynamicProperties]
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN { final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode $traitsCode

View File

@ -5,13 +5,14 @@ declare(strict_types=1);
namespace Pest\Factories; namespace Pest\Factories;
use Closure; use Closure;
use Pest\Contracts\AddsAnnotations; use Pest\Evaluators\Attributes;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
use Pest\Factories\Concerns\HigherOrderable; use Pest\Factories\Concerns\HigherOrderable;
use Pest\Repositories\DatasetsRepository; use Pest\Repositories\DatasetsRepository;
use Pest\Support\Str; use Pest\Support\Str;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\Assert; use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
@ -21,11 +22,23 @@ final class TestCaseMethodFactory
{ {
use HigherOrderable; use HigherOrderable;
/**
* The list of attributes.
*
* @var array<int, Attribute>
*/
public array $attributes = [];
/** /**
* The test's describing, if any. * The test's describing, if any.
*/ */
public ?string $describing = null; public ?string $describing = null;
/**
* The test's description, if any.
*/
public ?string $description = null;
/** /**
* The test's number of repetitions. * The test's number of repetitions.
*/ */
@ -36,6 +49,34 @@ final class TestCaseMethodFactory
*/ */
public bool $todo = false; public bool $todo = false;
/**
* The associated issue numbers.
*
* @var array<int, int>
*/
public array $issues = [];
/**
* The test assignees.
*
* @var array<int, string>
*/
public array $assignees = [];
/**
* The associated PRs numbers.
*
* @var array<int, int>
*/
public array $prs = [];
/**
* The test's notes.
*
* @var array<int, string>
*/
public array $notes = [];
/** /**
* The test's datasets. * The test's datasets.
* *
@ -58,18 +99,15 @@ final class TestCaseMethodFactory
public array $groups = []; public array $groups = [];
/** /**
* The covered classes and functions. * @see This property is not actually used in the codebase, it's only here to make Rector happy.
*
* @var array<int, \Pest\Factories\Covers\CoversClass|\Pest\Factories\Covers\CoversFunction|\Pest\Factories\Covers\CoversNothing>
*/ */
public array $covers = []; public bool $__ran = false;
/** /**
* Creates a new test case method factory instance. * Creates a new test case method factory instance.
*/ */
public function __construct( public function __construct(
public string $filename, public string $filename,
public ?string $description,
public ?Closure $closure, public ?Closure $closure,
) { ) {
$this->closure ??= function (): void { $this->closure ??= function (): void {
@ -80,9 +118,9 @@ final class TestCaseMethodFactory
} }
/** /**
* Creates the test's closure. * Sets the test's hooks, and runs any proxy to the test case.
*/ */
public function getClosure(TestCase $concrete): Closure public function setUp(TestCase $concrete): void
{ {
$concrete::flush(); // @phpstan-ignore-line $concrete::flush(); // @phpstan-ignore-line
@ -90,16 +128,32 @@ final class TestCaseMethodFactory
throw ShouldNotHappen::fromMessage('Description can not be empty.'); throw ShouldNotHappen::fromMessage('Description can not be empty.');
} }
$closure = $this->closure;
$testCase = TestSuite::getInstance()->tests->get($this->filename); $testCase = TestSuite::getInstance()->tests->get($this->filename);
assert($testCase instanceof TestCaseFactory);
$testCase->factoryProxies->proxy($concrete); $testCase->factoryProxies->proxy($concrete);
$this->factoryProxies->proxy($concrete); $this->factoryProxies->proxy($concrete);
}
/**
* Flushes the test case.
*/
public function tearDown(TestCase $concrete): void
{
$concrete::flush(); // @phpstan-ignore-line
}
/**
* Creates the test's closure.
*/
public function getClosure(): Closure
{
$closure = $this->closure;
$testCase = TestSuite::getInstance()->tests->get($this->filename);
assert($testCase instanceof TestCaseFactory);
$method = $this; $method = $this;
return function () use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line return function (...$arguments) use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line
/* @var TestCase $this */ /* @var TestCase $this */
$testCase->proxies->proxy($this); $testCase->proxies->proxy($this);
$method->proxies->proxy($this); $method->proxies->proxy($this);
@ -107,7 +161,9 @@ final class TestCaseMethodFactory
$testCase->chains->chain($this); $testCase->chains->chain($this);
$method->chains->chain($this); $method->chains->chain($this);
return \Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args()); $this->__ran = true;
return \Pest\Support\Closure::bind($closure, $this, self::class)(...$arguments);
}; };
} }
@ -116,16 +172,13 @@ final class TestCaseMethodFactory
*/ */
public function receivesArguments(): bool public function receivesArguments(): bool
{ {
return $this->datasets !== [] || $this->depends !== []; return $this->datasets !== [] || $this->depends !== [] || $this->repetitions > 1;
} }
/** /**
* Creates a PHPUnit method as a string ready for evaluation. * Creates a PHPUnit method as a string ready for evaluation.
*
* @param array<int, class-string<AddsAnnotations>> $annotationsToUse
* @param array<int, class-string<\Pest\Factories\Attributes\Attribute>> $attributesToUse
*/ */
public function buildForEvaluation(array $annotationsToUse, array $attributesToUse): string public function buildForEvaluation(): string
{ {
if ($this->description === null) { if ($this->description === null) {
throw ShouldNotHappen::fromMessage('The test description may not be empty.'); throw ShouldNotHappen::fromMessage('The test description may not be empty.');
@ -134,46 +187,49 @@ final class TestCaseMethodFactory
$methodName = Str::evaluable($this->description); $methodName = Str::evaluable($this->description);
$datasetsCode = ''; $datasetsCode = '';
$annotations = ['@test'];
$attributes = [];
foreach ($annotationsToUse as $annotation) { $this->attributes = [
$annotations = (new $annotation)->__invoke($this, $annotations); new Attribute(
} \PHPUnit\Framework\Attributes\Test::class,
[],
),
new Attribute(
\PHPUnit\Framework\Attributes\TestDox::class,
[str_replace('*/', '{@*}', $this->description)],
),
...$this->attributes,
];
foreach ($attributesToUse as $attribute) { foreach ($this->depends as $depend) {
$attributes = (new $attribute)->__invoke($this, $attributes); $depend = Str::evaluable($this->describing !== null ? Str::describe($this->describing, $depend) : $depend);
$this->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\Depends::class,
[$depend],
);
} }
if ($this->datasets !== [] || $this->repetitions > 1) { if ($this->datasets !== [] || $this->repetitions > 1) {
$dataProviderName = $methodName.'_dataset'; $dataProviderName = $methodName.'_dataset';
$annotations[] = "@dataProvider $dataProviderName"; $this->attributes[] = new Attribute(
DataProvider::class,
[$dataProviderName],
);
$datasetsCode = $this->buildDatasetForEvaluation($methodName, $dataProviderName); $datasetsCode = $this->buildDatasetForEvaluation($methodName, $dataProviderName);
} }
$annotations = implode('', array_map( $attributesCode = Attributes::code($this->attributes);
static fn (string $annotation): string => sprintf("\n * %s", $annotation), $annotations,
));
$attributes = implode('', array_map(
static fn (string $attribute): string => sprintf("\n %s", $attribute), $attributes,
));
return <<<PHP return <<<PHP
$attributesCode
/**$annotations public function $methodName(...\$arguments)
*/
$attributes
public function $methodName()
{ {
\$test = \Pest\TestSuite::getInstance()->tests->get(self::\$__filename)->getMethod(\$this->name())->getClosure(\$this);
return \$this->__runTest( return \$this->__runTest(
\$test, \$this->__test,
...func_get_args(), ...\$arguments,
); );
} }
$datasetsCode $datasetsCode
PHP; PHP;
} }

View File

@ -3,9 +3,12 @@
declare(strict_types=1); declare(strict_types=1);
use Pest\Concerns\Expectable; use Pest\Concerns\Expectable;
use Pest\Configuration;
use Pest\Exceptions\AfterAllWithinDescribe; use Pest\Exceptions\AfterAllWithinDescribe;
use Pest\Exceptions\BeforeAllWithinDescribe; use Pest\Exceptions\BeforeAllWithinDescribe;
use Pest\Expectation; use Pest\Expectation;
use Pest\Mutate\Contracts\MutationTestRunner;
use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\AfterEachCall; use Pest\PendingCalls\AfterEachCall;
use Pest\PendingCalls\BeforeEachCall; use Pest\PendingCalls\BeforeEachCall;
use Pest\PendingCalls\DescribeCall; use Pest\PendingCalls\DescribeCall;
@ -13,6 +16,7 @@ use Pest\PendingCalls\TestCall;
use Pest\PendingCalls\UsesCall; use Pest\PendingCalls\UsesCall;
use Pest\Repositories\DatasetsRepository; use Pest\Repositories\DatasetsRepository;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\Container;
use Pest\Support\DatasetInfo; use Pest\Support\DatasetInfo;
use Pest\Support\HigherOrderTapProxy; use Pest\Support\HigherOrderTapProxy;
use Pest\TestSuite; use Pest\TestSuite;
@ -53,6 +57,8 @@ if (! function_exists('beforeEach')) {
/** /**
* Runs the given closure before each test in the current file. * Runs the given closure before each test in the current file.
* *
* @param-closure-this TestCase $closure
*
* @return HigherOrderTapProxy<Expectable|TestCall|TestCase>|Expectable|TestCall|TestCase|mixed * @return HigherOrderTapProxy<Expectable|TestCall|TestCase>|Expectable|TestCall|TestCase|mixed
*/ */
function beforeEach(?Closure $closure = null): BeforeEachCall function beforeEach(?Closure $closure = null): BeforeEachCall
@ -108,12 +114,24 @@ if (! function_exists('uses')) {
} }
} }
if (! function_exists('pest')) {
/**
* Creates a new Pest configuration instance.
*/
function pest(): Configuration
{
return new Configuration(Backtrace::file());
}
}
if (! function_exists('test')) { if (! function_exists('test')) {
/** /**
* Adds the given closure as a test. The first argument * Adds the given closure as a test. The first argument
* is the test description; the second argument is * is the test description; the second argument is
* a closure that contains the test expectations. * a closure that contains the test expectations.
* *
* @param-closure-this TestCase $closure
*
* @return Expectable|TestCall|TestCase|mixed * @return Expectable|TestCall|TestCase|mixed
*/ */
function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall
@ -134,6 +152,8 @@ if (! function_exists('it')) {
* is the test description; the second argument is * is the test description; the second argument is
* a closure that contains the test expectations. * a closure that contains the test expectations.
* *
* @param-closure-this TestCase $closure
*
* @return Expectable|TestCall|TestCase|mixed * @return Expectable|TestCall|TestCase|mixed
*/ */
function it(string $description, ?Closure $closure = null): TestCall function it(string $description, ?Closure $closure = null): TestCall
@ -149,9 +169,7 @@ if (! function_exists('it')) {
if (! function_exists('todo')) { if (! function_exists('todo')) {
/** /**
* Adds the given todo test. Internally, this test * Creates a new test that is marked as "todo".
* is marked as incomplete. Yet, Collision, Pest's
* printer, will display it as a "todo" test.
* *
* @return Expectable|TestCall|TestCase|mixed * @return Expectable|TestCall|TestCase|mixed
*/ */
@ -169,6 +187,8 @@ if (! function_exists('afterEach')) {
/** /**
* Runs the given closure after each test in the current file. * Runs the given closure after each test in the current file.
* *
* @param-closure-this TestCase $closure
*
* @return Expectable|HigherOrderTapProxy<Expectable|TestCall|TestCase>|TestCall|mixed * @return Expectable|HigherOrderTapProxy<Expectable|TestCall|TestCase>|TestCall|mixed
*/ */
function afterEach(?Closure $closure = null): AfterEachCall function afterEach(?Closure $closure = null): AfterEachCall
@ -194,3 +214,67 @@ if (! function_exists('afterAll')) {
TestSuite::getInstance()->afterAll->set($closure); TestSuite::getInstance()->afterAll->set($closure);
} }
} }
if (! function_exists('covers')) {
/**
* Specifies which classes, or functions, a test case covers.
*
* @param array<int, string>|string $classesOrFunctions
*/
function covers(array|string ...$classesOrFunctions): void
{
$filename = Backtrace::file();
$beforeEachCall = (new BeforeEachCall(TestSuite::getInstance(), $filename));
$beforeEachCall->covers(...$classesOrFunctions);
$beforeEachCall->group('__pest_mutate_only');
/** @var MutationTestRunner $runner */
$runner = Container::getInstance()->get(MutationTestRunner::class);
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if ($runner->isEnabled() && ! $everything && ! is_array($classes) && ! is_array($paths)) {
$beforeEachCall->only('__pest_mutate_only');
}
}
}
if (! function_exists('mutates')) {
/**
* Specifies which classes, enums, or traits a test case mutates.
*
* @param array<int, string>|string $targets
*/
function mutates(array|string ...$targets): void
{
$filename = Backtrace::file();
$beforeEachCall = (new BeforeEachCall(TestSuite::getInstance(), $filename));
$beforeEachCall->group('__pest_mutate_only');
/** @var MutationTestRunner $runner */
$runner = Container::getInstance()->get(MutationTestRunner::class);
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if ($runner->isEnabled() && ! $everything && ! is_array($classes) && ! is_array($paths)) {
$beforeEachCall->only('__pest_mutate_only');
}
/** @var ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if (! is_array($paths)) {
$configurationRepository->globalConfiguration('default')->class(...$targets); // @phpstan-ignore-line
}
}
}

View File

@ -27,7 +27,7 @@ use Whoops\Exception\Inspector;
/** /**
* @internal * @internal
*/ */
final class Kernel final readonly class Kernel
{ {
/** /**
* The Kernel bootstrappers. * The Kernel bootstrappers.
@ -47,8 +47,8 @@ final class Kernel
* Creates a new Kernel instance. * Creates a new Kernel instance.
*/ */
public function __construct( public function __construct(
private readonly Application $application, private Application $application,
private readonly OutputInterface $output, private OutputInterface $output,
) { ) {
// //
} }

View File

@ -18,23 +18,30 @@ use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestSuite\TestSuite; use PHPUnit\Event\TestSuite\TestSuite;
use PHPUnit\Event\TestSuite\TestSuiteForTestMethodWithDataProvider;
use PHPUnit\Framework\Exception as FrameworkException; use PHPUnit\Framework\Exception as FrameworkException;
use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult; use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult;
/** /**
* @internal * @internal
*/ */
final class Converter final readonly class Converter
{ {
/**
* The prefix for the test suite name.
*/
private const PREFIX = 'P\\'; private const PREFIX = 'P\\';
private readonly StateGenerator $stateGenerator; /**
* The state generator.
*/
private StateGenerator $stateGenerator;
/** /**
* Creates a new instance of the Converter. * Creates a new instance of the Converter.
*/ */
public function __construct( public function __construct(
private readonly string $rootPath, private string $rootPath,
) { ) {
$this->stateGenerator = new StateGenerator; $this->stateGenerator = new StateGenerator;
} }
@ -129,7 +136,7 @@ final class Converter
// Format stacktrace as `at <path>` // Format stacktrace as `at <path>`
$frames = array_map( $frames = array_map(
fn (string $frame) => "at $frame", fn (string $frame): string => "at $frame",
$frames $frames
); );
@ -141,6 +148,13 @@ final class Converter
*/ */
public function getTestSuiteName(TestSuite $testSuite): string public function getTestSuiteName(TestSuite $testSuite): string
{ {
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
$firstTest = $this->getFirstTest($testSuite);
if ($firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
return $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
}
}
$name = $testSuite->name(); $name = $testSuite->name();
if (! str_starts_with($name, self::PREFIX)) { if (! str_starts_with($name, self::PREFIX)) {
@ -162,6 +176,35 @@ final class Converter
* Gets the test suite location. * Gets the test suite location.
*/ */
public function getTestSuiteLocation(TestSuite $testSuite): ?string public function getTestSuiteLocation(TestSuite $testSuite): ?string
{
$firstTest = $this->getFirstTest($testSuite);
if (! $firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
return null;
}
$path = $firstTest->testDox()->prettifiedClassName();
$classRelativePath = $this->toRelativePath($path);
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
$methodName = $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
return "$classRelativePath::$methodName";
}
return $classRelativePath;
}
/**
* Gets the prettified test method name without dataset-related suffix.
*/
private function getTestMethodNameWithoutDatasetSuffix(TestMethod $testMethod): string
{
return Str::beforeLast($testMethod->testDox()->prettifiedMethodName(), ' with data set ');
}
/**
* Gets the first test from the test suite.
*/
private function getFirstTest(TestSuite $testSuite): ?TestMethod
{ {
$tests = $testSuite->tests()->asArray(); $tests = $testSuite->tests()->asArray();
@ -175,9 +218,7 @@ final class Converter
throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); throw ShouldNotHappen::fromMessage('Not an instance of TestMethod');
} }
$path = $firstTest->testDox()->prettifiedClassName(); return $firstTest;
return $this->toRelativePath($path);
} }
/** /**

View File

@ -9,6 +9,9 @@ namespace Pest\Logging\TeamCity;
*/ */
final class ServiceMessage final class ServiceMessage
{ {
/**
* The flow ID.
*/
private static ?int $flowId = null; private static ?int $flowId = null;
/** /**
@ -35,7 +38,7 @@ final class ServiceMessage
{ {
return new self('testSuiteStarted', [ return new self('testSuiteStarted', [
'name' => $name, 'name' => $name,
'locationHint' => $location === null ? null : "file://$location", 'locationHint' => $location === null ? null : "pest_qn://$location",
]); ]);
} }

View File

@ -9,7 +9,7 @@ use Pest\Logging\TeamCity\TeamCityLogger;
/** /**
* @internal * @internal
*/ */
abstract class Subscriber abstract class Subscriber // @pest-arch-ignore-line
{ {
/** /**
* Creates a new Subscriber instance. * Creates a new Subscriber instance.
@ -19,7 +19,7 @@ abstract class Subscriber
/** /**
* Creates a new TeamCityLogger instance. * Creates a new TeamCityLogger instance.
*/ */
final protected function logger(): TeamCityLogger final protected function logger(): TeamCityLogger // @pest-arch-ignore-line
{ {
return $this->logger; return $this->logger;
} }

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\MarkedIncompleteSubscriber;
/**
* @internal
*/
final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber
{
public function notify(MarkedIncomplete $event): void
{
$this->logger()->testMarkedIncomplete($event);
}
}

View File

@ -12,7 +12,6 @@ use Pest\Logging\TeamCity\Subscriber\TestErroredSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestExecutionFinishedSubscriber; use Pest\Logging\TeamCity\Subscriber\TestExecutionFinishedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestFailedSubscriber; use Pest\Logging\TeamCity\Subscriber\TestFailedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestFinishedSubscriber; use Pest\Logging\TeamCity\Subscriber\TestFinishedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestMarkedIncompleteSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestPreparedSubscriber; use Pest\Logging\TeamCity\Subscriber\TestPreparedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestSkippedSubscriber; use Pest\Logging\TeamCity\Subscriber\TestSkippedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestSuiteFinishedSubscriber; use Pest\Logging\TeamCity\Subscriber\TestSuiteFinishedSubscriber;
@ -28,7 +27,6 @@ use PHPUnit\Event\Test\ConsideredRisky;
use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestRunner\ExecutionFinished; use PHPUnit\Event\TestRunner\ExecutionFinished;
@ -45,8 +43,14 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final class TeamCityLogger final class TeamCityLogger
{ {
/**
* The current time.
*/
private ?HRTime $time = null; private ?HRTime $time = null;
/**
* Indicates if the summary test count has been printed.
*/
private bool $isSummaryTestCountPrinted = false; private bool $isSummaryTestCountPrinted = false;
/** /**
@ -108,7 +112,7 @@ final class TeamCityLogger
$this->time = $event->telemetryInfo()->time(); $this->time = $event->telemetryInfo()->time();
} }
public function testMarkedIncomplete(MarkedIncomplete $event): never public function testMarkedIncomplete(): never
{ {
throw ShouldNotHappen::fromMessage('testMarkedIncomplete not implemented.'); throw ShouldNotHappen::fromMessage('testMarkedIncomplete not implemented.');
} }
@ -262,7 +266,6 @@ final class TeamCityLogger
new TestFinishedSubscriber($this), new TestFinishedSubscriber($this),
new TestErroredSubscriber($this), new TestErroredSubscriber($this),
new TestFailedSubscriber($this), new TestFailedSubscriber($this),
new TestMarkedIncompleteSubscriber($this),
new TestSkippedSubscriber($this), new TestSkippedSubscriber($this),
new TestConsideredRiskySubscriber($this), new TestConsideredRiskySubscriber($this),
new TestExecutionFinishedSubscriber($this), new TestExecutionFinishedSubscriber($this),

View File

@ -344,36 +344,6 @@ final class Expectation
return $this; return $this;
} }
/**
* Asserts that the value has the method $name.
*
* @return self<TValue>
*/
public function toHaveMethod(string $name, string $message = ''): self
{
$this->toBeObject();
// @phpstan-ignore-next-line
Assert::assertTrue(method_exists($this->value, $name), $message);
return $this;
}
/**
* Asserts that the value has the provided methods $names.
*
* @param iterable<array-key, string> $names
* @return self<TValue>
*/
public function toHaveMethods(iterable $names, string $message = ''): self
{
foreach ($names as $name) {
$this->toHaveMethod($name, message: $message);
}
return $this;
}
/** /**
* Asserts that two variables have the same value. * Asserts that two variables have the same value.
* *

View File

@ -5,19 +5,20 @@ declare(strict_types=1);
namespace Pest; namespace Pest;
use NunoMaduro\Collision\Writer; use NunoMaduro\Collision\Writer;
use Pest\Exceptions\TestDescriptionMissing;
use Pest\Support\Container; use Pest\Support\Container;
use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Throwable; use Throwable;
use Whoops\Exception\Inspector; use Whoops\Exception\Inspector;
final class Panic final readonly class Panic
{ {
/** /**
* Creates a new Panic instance. * Creates a new Panic instance.
*/ */
private function __construct( private function __construct(
private readonly Throwable $throwable private Throwable $throwable
) { ) {
// ... // ...
} }
@ -27,6 +28,10 @@ final class Panic
*/ */
public static function with(Throwable $throwable): never public static function with(Throwable $throwable): never
{ {
if ($throwable instanceof TestDescriptionMissing && ! is_null($previous = $throwable->getPrevious())) {
$throwable = $previous;
}
$panic = new self($throwable); $panic = new self($throwable);
$panic->handle(); $panic->handle();

View File

@ -54,7 +54,7 @@ final class AfterEachCall
$proxies = $this->proxies; $proxies = $this->proxies;
$afterEachTestCase = ChainableClosure::boundWhen( $afterEachTestCase = ChainableClosure::boundWhen(
fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line fn (): bool => is_null($describing) || $this->__describing === $describing,
ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line
)->bindTo($this, self::class); )->bindTo($this, self::class);
@ -65,7 +65,6 @@ final class AfterEachCall
$this, $this,
$afterEachTestCase, $afterEachTestCase,
); );
} }
/** /**

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Pest\PendingCalls; namespace Pest\PendingCalls;
use Closure; use Closure;
use Pest\Exceptions\AfterBeforeTestFunction;
use Pest\PendingCalls\Concerns\Describable; use Pest\PendingCalls\Concerns\Describable;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\ChainableClosure; use Pest\Support\ChainableClosure;
@ -14,6 +15,8 @@ use Pest\TestSuite;
/** /**
* @internal * @internal
*
* @mixin TestCall
*/ */
final class BeforeEachCall final class BeforeEachCall
{ {
@ -59,17 +62,22 @@ final class BeforeEachCall
$testCaseProxies = $this->testCaseProxies; $testCaseProxies = $this->testCaseProxies;
$beforeEachTestCall = function (TestCall $testCall) use ($describing): void { $beforeEachTestCall = function (TestCall $testCall) use ($describing): void {
if ($describing !== $this->describing) {
return; if ($this->describing !== null) {
} if ($describing !== $this->describing) {
if ($describing !== $testCall->describing) { return;
return; }
if ($describing !== $testCall->describing) {
return;
}
} }
$this->testCallProxies->chain($testCall); $this->testCallProxies->chain($testCall);
}; };
$beforeEachTestCase = ChainableClosure::boundWhen( $beforeEachTestCase = ChainableClosure::boundWhen(
fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line fn (): bool => is_null($describing) || $this->__describing === $describing,
ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line
)->bindTo($this, self::class); )->bindTo($this, self::class);
@ -83,6 +91,18 @@ final class BeforeEachCall
); );
} }
/**
* Runs the given closure after the test.
*/
public function after(Closure $closure): self
{
if ($this->describing === null) {
throw new AfterBeforeTestFunction($this->filename);
}
return $this->__call('after', [$closure]);
}
/** /**
* Saves the calls to be used on the target. * Saves the calls to be used on the target.
* *
@ -91,7 +111,8 @@ final class BeforeEachCall
public function __call(string $name, array $arguments): self public function __call(string $name, array $arguments): self
{ {
if (method_exists(TestCall::class, $name)) { if (method_exists(TestCall::class, $name)) {
$this->testCallProxies->add(Backtrace::file(), Backtrace::line(), $name, $arguments); $this->testCallProxies
->add(Backtrace::file(), Backtrace::line(), $name, $arguments);
return $this; return $this;
} }

View File

@ -9,5 +9,13 @@ namespace Pest\PendingCalls\Concerns;
*/ */
trait Describable trait Describable
{ {
/**
* Note: this is property is not used; however, it gets added automatically by rector php.
*/
public string $__describing;
/**
* The describing of the test case.
*/
public ?string $describing = null; public ?string $describing = null;
} }

View File

@ -18,6 +18,11 @@ final class DescribeCall
*/ */
private static ?string $describing = null; private static ?string $describing = null;
/**
* The describe "before each" call.
*/
private ?BeforeEachCall $currentBeforeEachCall = null;
/** /**
* Creates a new Pending Call. * Creates a new Pending Call.
*/ */
@ -43,6 +48,8 @@ final class DescribeCall
*/ */
public function __destruct() public function __destruct()
{ {
unset($this->currentBeforeEachCall);
self::$describing = $this->description; self::$describing = $this->description;
try { try {
@ -57,14 +64,18 @@ final class DescribeCall
* *
* @param array<int, mixed> $arguments * @param array<int, mixed> $arguments
*/ */
public function __call(string $name, array $arguments): BeforeEachCall public function __call(string $name, array $arguments): self
{ {
$filename = Backtrace::file(); $filename = Backtrace::file();
$beforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename); if (! $this->currentBeforeEachCall instanceof \Pest\PendingCalls\BeforeEachCall) {
$this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename);
$beforeEachCall->describing = $this->description; $this->currentBeforeEachCall->describing = $this->description;
}
return $beforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line $this->currentBeforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line
return $this;
} }
} }

View File

@ -5,14 +5,16 @@ declare(strict_types=1);
namespace Pest\PendingCalls; namespace Pest\PendingCalls;
use Closure; use Closure;
use Pest\Concerns\Testable;
use Pest\Exceptions\InvalidArgumentException; use Pest\Exceptions\InvalidArgumentException;
use Pest\Factories\Covers\CoversClass; use Pest\Exceptions\TestDescriptionMissing;
use Pest\Factories\Covers\CoversFunction; use Pest\Factories\Attribute;
use Pest\Factories\Covers\CoversNothing;
use Pest\Factories\TestCaseMethodFactory; use Pest\Factories\TestCaseMethodFactory;
use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\Concerns\Describable; use Pest\PendingCalls\Concerns\Describable;
use Pest\Plugins\Only; use Pest\Plugins\Only;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\Container;
use Pest\Support\Exporter; use Pest\Support\Exporter;
use Pest\Support\HigherOrderCallables; use Pest\Support\HigherOrderCallables;
use Pest\Support\NullClosure; use Pest\Support\NullClosure;
@ -24,12 +26,19 @@ use PHPUnit\Framework\TestCase;
/** /**
* @internal * @internal
* *
* @mixin HigherOrderCallables|TestCase * @mixin HigherOrderCallables|TestCase|Testable
*/ */
final class TestCall final class TestCall // @phpstan-ignore-line
{ {
use Describable; use Describable;
/**
* The list of test case factory attributes.
*
* @var array<int, Attribute>
*/
private array $testCaseFactoryAttributes = [];
/** /**
* The Test Case Factory. * The Test Case Factory.
*/ */
@ -46,10 +55,10 @@ final class TestCall
public function __construct( public function __construct(
private readonly TestSuite $testSuite, private readonly TestSuite $testSuite,
private readonly string $filename, private readonly string $filename,
?string $description = null, private ?string $description = null,
?Closure $closure = null ?Closure $closure = null
) { ) {
$this->testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure); $this->testCaseMethod = new TestCaseMethodFactory($filename, $closure);
$this->descriptionLess = $description === null; $this->descriptionLess = $description === null;
@ -58,6 +67,42 @@ final class TestCall
$this->testSuite->beforeEach->get($this->filename)[0]($this); $this->testSuite->beforeEach->get($this->filename)[0]($this);
} }
/**
* Runs the given closure after the test.
*/
public function after(Closure $closure): self
{
if ($this->description === null) {
throw new TestDescriptionMissing($this->filename);
}
$description = is_null($this->describing)
? $this->description
: Str::describe($this->describing, $this->description);
$filename = $this->filename;
$when = function () use ($closure, $filename, $description): void {
if ($this::$__filename !== $filename) { // @phpstan-ignore-line
return;
}
if ($this->__description !== $description) { // @phpstan-ignore-line
return;
}
if ($this->__ran !== true) { // @phpstan-ignore-line
return;
}
$closure->call($this);
};
new AfterEachCall($this->testSuite, $this->filename, $when->bindTo(new \stdClass));
return $this;
}
/** /**
* Asserts that the test fails with the given message. * Asserts that the test fails with the given message.
*/ */
@ -165,7 +210,10 @@ final class TestCall
public function group(string ...$groups): self public function group(string ...$groups): self
{ {
foreach ($groups as $group) { foreach ($groups as $group) {
$this->testCaseMethod->groups[] = $group; $this->testCaseMethod->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\Group::class,
[$group],
);
} }
return $this; return $this;
@ -176,7 +224,7 @@ final class TestCall
*/ */
public function only(): self public function only(): self
{ {
Only::enable($this); Only::enable($this, ...func_get_args()); // @phpstan-ignore-line
return $this; return $this;
} }
@ -306,32 +354,186 @@ final class TestCall
} }
/** /**
* Sets the test as "todo". * Marks the test as "todo".
*/ */
public function todo(): self public function todo(// @phpstan-ignore-line
{ array|string|null $note = null,
array|string|null $assignee = null,
array|string|int|null $issue = null,
array|string|int|null $pr = null,
): self {
$this->skip('__TODO__'); $this->skip('__TODO__');
$this->testCaseMethod->todo = true; $this->testCaseMethod->todo = true;
if ($issue !== null) {
$this->issue($issue);
}
if ($pr !== null) {
$this->pr($pr);
}
if ($assignee !== null) {
$this->assignee($assignee);
}
if ($note !== null) {
$this->note($note);
}
return $this;
}
/**
* Sets the test as "work in progress".
*/
public function wip(// @phpstan-ignore-line
array|string|null $note = null,
array|string|null $assignee = null,
array|string|int|null $issue = null,
array|string|int|null $pr = null,
): self {
if ($issue !== null) {
$this->issue($issue);
}
if ($pr !== null) {
$this->pr($pr);
}
if ($assignee !== null) {
$this->assignee($assignee);
}
if ($note !== null) {
$this->note($note);
}
return $this;
}
/**
* Sets the test as "done".
*/
public function done(// @phpstan-ignore-line
array|string|null $note = null,
array|string|null $assignee = null,
array|string|int|null $issue = null,
array|string|int|null $pr = null,
): self {
if ($issue !== null) {
$this->issue($issue);
}
if ($pr !== null) {
$this->pr($pr);
}
if ($assignee !== null) {
$this->assignee($assignee);
}
if ($note !== null) {
$this->note($note);
}
return $this;
}
/**
* Associates the test with the given issue(s).
*
* @param array<int, string|int>|string|int $number
*/
public function issue(array|string|int $number): self
{
$number = is_array($number) ? $number : [$number];
$number = array_map(fn (string|int $number): int => (int) ltrim((string) $number, '#'), $number);
$this->testCaseMethod->issues = array_merge($this->testCaseMethod->issues, $number);
return $this;
}
/**
* Associates the test with the given ticket(s). (Alias for `issue`)
*
* @param array<int, string|int>|string|int $number
*/
public function ticket(array|string|int $number): self
{
return $this->issue($number);
}
/**
* Sets the test assignee(s).
*
* @param array<int, string>|string $assignee
*/
public function assignee(array|string $assignee): self
{
$assignees = is_array($assignee) ? $assignee : [$assignee];
$this->testCaseMethod->assignees = array_unique(array_merge($this->testCaseMethod->assignees, $assignees));
return $this;
}
/**
* Associates the test with the given pull request(s).
*
* @param array<int, string|int>|string|int $number
*/
public function pr(array|string|int $number): self
{
$number = is_array($number) ? $number : [$number];
$number = array_map(fn (string|int $number): int => (int) ltrim((string) $number, '#'), $number);
$this->testCaseMethod->prs = array_unique(array_merge($this->testCaseMethod->prs, $number));
return $this;
}
/**
* Adds a note to the test.
*
* @param array<int, string>|string $note
*/
public function note(array|string $note): self
{
$notes = is_array($note) ? $note : [$note];
$this->testCaseMethod->notes = array_unique(array_merge($this->testCaseMethod->notes, $notes));
return $this; return $this;
} }
/** /**
* Sets the covered classes or methods. * Sets the covered classes or methods.
*
* @param array<int, string>|string $classesOrFunctions
*/ */
public function covers(string ...$classesOrFunctions): self public function covers(array|string ...$classesOrFunctions): self
{ {
foreach ($classesOrFunctions as $classOrFunction) { /** @var array<int, string> $classesOrFunctions */
$isClass = class_exists($classOrFunction) || trait_exists($classOrFunction); $classesOrFunctions = array_reduce($classesOrFunctions, fn ($carry, $item): array => is_array($item) ? array_merge($carry, $item) : array_merge($carry, [$item]), []); // @pest-ignore-type
$isMethod = function_exists($classOrFunction);
if (! $isClass && ! $isMethod) { foreach ($classesOrFunctions as $classOrFunction) {
throw new InvalidArgumentException(sprintf('No class or method named "%s" has been found.', $classOrFunction)); $isClass = class_exists($classOrFunction) || interface_exists($classOrFunction) || enum_exists($classOrFunction);
$isTrait = trait_exists($classOrFunction);
$isFunction = function_exists($classOrFunction);
if (! $isClass && ! $isTrait && ! $isFunction) {
throw new InvalidArgumentException(sprintf('No class, trait or method named "%s" has been found.', $classOrFunction));
} }
if ($isClass) { if ($isClass) {
$this->coversClass($classOrFunction); $this->coversClass($classOrFunction);
} elseif ($isTrait) {
$this->coversTrait($classOrFunction);
} else { } else {
$this->coversFunction($classOrFunction); $this->coversFunction($classOrFunction);
} }
@ -346,7 +548,41 @@ final class TestCall
public function coversClass(string ...$classes): self public function coversClass(string ...$classes): self
{ {
foreach ($classes as $class) { foreach ($classes as $class) {
$this->testCaseMethod->covers[] = new CoversClass($class); $this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversClass::class,
[$class],
);
}
/** @var ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if (! is_array($paths)) {
$configurationRepository->globalConfiguration('default')->class(...$classes); // @phpstan-ignore-line
}
return $this;
}
/**
* Sets the covered classes.
*/
public function coversTrait(string ...$traits): self
{
foreach ($traits as $trait) {
$this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversTrait::class,
[$trait],
);
}
/** @var ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if (! is_array($paths)) {
$configurationRepository->globalConfiguration('default')->class(...$traits); // @phpstan-ignore-line
} }
return $this; return $this;
@ -358,7 +594,10 @@ final class TestCall
public function coversFunction(string ...$functions): self public function coversFunction(string ...$functions): self
{ {
foreach ($functions as $function) { foreach ($functions as $function) {
$this->testCaseMethod->covers[] = new CoversFunction($function); $this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversFunction::class,
[$function],
);
} }
return $this; return $this;
@ -369,7 +608,10 @@ final class TestCall
*/ */
public function coversNothing(): self public function coversNothing(): self
{ {
$this->testCaseMethod->covers = [new CoversNothing]; $this->testCaseMethod->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversNothing::class,
[],
);
return $this; return $this;
} }
@ -420,10 +662,11 @@ final class TestCall
if ($this->descriptionLess) { if ($this->descriptionLess) {
Exporter::default(); Exporter::default();
if ($this->testCaseMethod->description !== null) { if ($this->description !== null) {
$this->testCaseMethod->description .= ' → '; $this->description .= ' → ';
} }
$this->testCaseMethod->description .= $arguments === null
$this->description .= $arguments === null
? $name ? $name
: sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments)); : sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
} }
@ -436,11 +679,21 @@ final class TestCall
*/ */
public function __destruct() public function __destruct()
{ {
if ($this->description === null) {
throw new TestDescriptionMissing($this->filename);
}
if (! is_null($this->describing)) { if (! is_null($this->describing)) {
$this->testCaseMethod->describing = $this->describing; $this->testCaseMethod->describing = $this->describing;
$this->testCaseMethod->description = Str::describe($this->describing, $this->testCaseMethod->description); // @phpstan-ignore-line $this->testCaseMethod->description = Str::describe($this->describing, $this->description);
} else {
$this->testCaseMethod->description = $this->description;
} }
$this->testSuite->tests->set($this->testCaseMethod); $this->testSuite->tests->set($this->testCaseMethod);
if (! is_null($testCase = $this->testSuite->tests->get($this->filename))) {
$testCase->attributes = array_merge($testCase->attributes, $this->testCaseFactoryAttributes);
}
} }
} }

View File

@ -48,11 +48,14 @@ final class UsesCall
*/ */
public function __construct( public function __construct(
private readonly string $filename, private readonly string $filename,
private readonly array $classAndTraits private array $classAndTraits
) { ) {
$this->targets = [$filename]; $this->targets = [$filename];
} }
/**
* @deprecated Use `pest()->printer()->compact()` instead.
*/
public function compact(): self public function compact(): self
{ {
DefaultPrinter::compact(true); DefaultPrinter::compact(true);
@ -61,10 +64,29 @@ final class UsesCall
} }
/** /**
* The directories or file where the * Specifies the class or traits to use.
* class or traits should be used. *
* @alias extend
*/ */
public function in(string ...$targets): void public function use(string ...$classAndTraits): self
{
return $this->extend(...$classAndTraits);
}
/**
* Specifies the class or traits to use.
*/
public function extend(string ...$classAndTraits): self
{
$this->classAndTraits = array_merge($this->classAndTraits, array_values($classAndTraits));
return $this;
}
/**
* The directories or file where the class or traits should be used.
*/
public function in(string ...$targets): self
{ {
$targets = array_map(function (string $path): string { $targets = array_map(function (string $path): string {
$startChar = DIRECTORY_SEPARATOR; $startChar = DIRECTORY_SEPARATOR;
@ -78,7 +100,7 @@ final class UsesCall
return str_starts_with($path, $startChar) return str_starts_with($path, $startChar)
? $path ? $path
: implode(DIRECTORY_SEPARATOR, [ : implode(DIRECTORY_SEPARATOR, [
dirname($this->filename), is_dir($this->filename) ? $this->filename : dirname($this->filename),
$path, $path,
]); ]);
}, $targets); }, $targets);
@ -92,6 +114,8 @@ final class UsesCall
return $accumulator; return $accumulator;
}, []); }, []);
return $this;
} }
/** /**

View File

@ -6,7 +6,7 @@ namespace Pest;
function version(): string function version(): string
{ {
return '2.35.1'; return '3.3.1';
} }
function testDirectory(string $file = ''): string function testDirectory(string $file = ''): string

View File

@ -21,7 +21,7 @@ trait HandleArguments
return true; return true;
} }
if (str_starts_with($arg, "$argument=")) { if (str_starts_with((string) $arg, "$argument=")) { // @phpstan-ignore-line
return true; return true;
} }
} }

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins;
use DOMDocument;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Contracts\Plugins\Terminable;
use Pest\Plugins\Concerns\HandleArguments;
use PHPUnit\TextUI\CliArguments\Builder as CliConfigurationBuilder;
use PHPUnit\TextUI\CliArguments\XmlConfigurationFileFinder;
/**
* @internal
*/
final class Configuration implements HandlesArguments, Terminable
{
use HandleArguments;
/**
* The base PHPUnit file.
*/
public const BASE_PHPUNIT_FILE = __DIR__
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
.'resources/base-phpunit.xml';
/**
* Handles the arguments, adding the cache directory and the cache result arguments.
*/
public function handleArguments(array $arguments): array
{
if ($this->hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) {
return $arguments;
}
$arguments = $this->pushArgument('--configuration', $arguments);
return $this->pushArgument((string) realpath($this->fromGeneratedConfigurationFile()), $arguments);
}
/**
* Get the configuration file from the generated configuration file.
*/
private function fromGeneratedConfigurationFile(): string
{
$path = $this->getTempPhpunitXmlPath();
if (file_exists($path)) {
unlink($path);
}
$doc = new DOMDocument;
$doc->load(self::BASE_PHPUNIT_FILE);
$contents = $doc->saveXML();
assert(is_int(file_put_contents($path, $contents)));
return $path;
}
/**
* Check if the configuration file is custom.
*/
private function hasCustomConfigurationFile(): bool
{
$cliConfiguration = (new CliConfigurationBuilder)->fromParameters([]);
$configurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration);
return is_string($configurationFile);
}
/**
* Get the temporary phpunit.xml path.
*/
private function getTempPhpunitXmlPath(): string
{
return getcwd().'/.pest.xml';
}
/**
* Terminates the plugin.
*/
public function terminate(): void
{
$path = $this->getTempPhpunitXmlPath();
if (file_exists($path)) {
unlink($path);
}
}
}

View File

@ -114,6 +114,10 @@ final class Coverage implements AddsOutput, HandlesArguments
*/ */
public function addOutput(int $exitCode): int public function addOutput(int $exitCode): int
{ {
if (Parallel::isWorker()) {
return $exitCode;
}
if ($exitCode === 0 && $this->coverage) { if ($exitCode === 0 && $this->coverage) {
if (! \Pest\Support\Coverage::isAvailable()) { if (! \Pest\Support\Coverage::isAvailable()) {
$this->output->writeln( $this->output->writeln(
@ -130,7 +134,7 @@ final class Coverage implements AddsOutput, HandlesArguments
$this->output->writeln(sprintf( $this->output->writeln(sprintf(
"\n <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected <fg=white;options=bold> %s %%</>, currently <fg=red;options=bold> %s %%</>.", "\n <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected <fg=white;options=bold> %s %%</>, currently <fg=red;options=bold> %s %%</>.",
number_format($this->coverageMin, 1), number_format($this->coverageMin, 1),
number_format($coverage, 1) number_format(floor($coverage * 10) / 10, 1)
)); ));
} }

View File

@ -14,7 +14,7 @@ use function Pest\version;
/** /**
* @internal * @internal
*/ */
final class Help implements HandlesArguments final readonly class Help implements HandlesArguments
{ {
use Concerns\HandleArguments; use Concerns\HandleArguments;
@ -22,7 +22,7 @@ final class Help implements HandlesArguments
* Creates a new Plugin instance. * Creates a new Plugin instance.
*/ */
public function __construct( public function __construct(
private readonly OutputInterface $output private OutputInterface $output
) { ) {
// .. // ..
} }
@ -123,6 +123,21 @@ final class Help implements HandlesArguments
], [ ], [
'arg' => '--todos', 'arg' => '--todos',
'desc' => 'Output to standard output the list of todos', 'desc' => 'Output to standard output the list of todos',
], [
'arg' => '--notes',
'desc' => 'Output to standard output tests with notes',
], [
], [
'arg' => '--issue',
'desc' => 'Output to standard output tests with the given issue number',
], [
], [
'arg' => '--pr',
'desc' => 'Output to standard output tests with the given pull request number',
], [
], [
'arg' => '--pull-request',
'desc' => 'Output to standard output tests with the given pull request number (alias for --pr)',
], [ ], [
'arg' => '--retry', 'arg' => '--retry',
'desc' => 'Run non-passing tests first and stop execution upon first error or failure', 'desc' => 'Run non-passing tests first and stop execution upon first error or failure',
@ -143,6 +158,59 @@ final class Help implements HandlesArguments
'desc' => 'Set the minimum required coverage percentage, and fail if not met', 'desc' => 'Set the minimum required coverage percentage, and fail if not met',
], ...$content['Code Coverage']]; ], ...$content['Code Coverage']];
$content['Mutation Testing'] = [[
'arg' => '--mutate ',
'desc' => 'Runs mutation testing, to understand the quality of your tests',
], [
'arg' => '--mutate --parallel',
'desc' => 'Runs mutation testing in parallel',
], [
'arg' => '--mutate --min',
'desc' => 'Set the minimum required mutation score, and fail if not met',
], [
'arg' => '--mutate --id',
'desc' => 'Run only the mutation with the given ID. But E.g. --id=ecb35ab30ffd3491. Note, you need to provide the same options as the original run',
], [
'arg' => '--mutate --covered-only',
'desc' => 'Only generate mutations for classes that are covered by tests',
], [
'arg' => '--mutate --bail',
'desc' => 'Stop mutation testing execution upon first untested or uncovered mutation',
], [
'arg' => '--mutate --class',
'desc' => 'Generate mutations for the given class(es). E.g. --class=App\\\\Models',
], [
'arg' => '--mutate --ignore',
'desc' => 'Ignore the given class(es) when generating mutations. E.g. --ignore=App\\\\Http\\\\Requests',
], [
'arg' => '--mutate --clear-cache',
'desc' => 'Clear the mutation cache',
], [
'arg' => '--mutate --no-cache',
'desc' => 'Clear the mutation cache',
], [
'arg' => '--mutate --ignore-min-score-on-zero-mutations',
'desc' => 'Ignore the minimum score requirement when there are no mutations',
], [
'arg' => '--mutate --covered-only',
'desc' => 'Only generate mutations for classes that are covered by tests',
], [
'arg' => '--mutate --everything',
'desc' => 'Generate mutations for all classes, even if they are not covered by tests',
], [
'arg' => '--mutate --profile',
'desc' => 'Output to standard output the top ten slowest mutations',
], [
'arg' => '--mutate --retry',
'desc' => 'Run untested or uncovered mutations first and stop execution upon first error or failure',
], [
'arg' => '--mutate --stop-on-uncovered',
'desc' => 'Stop mutation testing execution upon first untested mutation',
], [
'arg' => '--mutate --stop-on-untested',
'desc' => 'Stop mutation testing execution upon first untested mutation',
]];
$content['Profiling'] = [ $content['Profiling'] = [
[ [
'arg' => '--profile ', 'arg' => '--profile ',

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Output\OutputInterface;
/** /**
* @internal * @internal
*/ */
final class Init implements HandlesArguments final readonly class Init implements HandlesArguments
{ {
/** /**
* The option the triggers the init job. * The option the triggers the init job.
@ -37,9 +37,9 @@ final class Init implements HandlesArguments
* Creates a new Plugin instance. * Creates a new Plugin instance.
*/ */
public function __construct( public function __construct(
private readonly TestSuite $testSuite, private TestSuite $testSuite,
private readonly InputInterface $input, private InputInterface $input,
private readonly OutputInterface $output private OutputInterface $output
) { ) {
// .. // ..
} }

View File

@ -28,6 +28,10 @@ final class Only implements Terminable
*/ */
public function terminate(): void public function terminate(): void
{ {
if (Parallel::isWorker()) {
return;
}
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (file_exists($lockFile)) { if (file_exists($lockFile)) {
@ -38,18 +42,26 @@ final class Only implements Terminable
/** /**
* Creates the lock file. * Creates the lock file.
*/ */
public static function enable(TestCall $testCall): void public static function enable(TestCall $testCall, string $group = '__pest_only'): void
{ {
if (Environment::name() == Environment::CI) { $testCall->group($group);
if (Environment::name() === Environment::CI || Parallel::isWorker()) {
return; return;
} }
$testCall->group('__pest_only');
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (file_exists($lockFile) && $group === '__pest_only') {
file_put_contents($lockFile, $group);
return;
}
if (! file_exists($lockFile)) { if (! file_exists($lockFile)) {
touch($lockFile); touch($lockFile);
file_put_contents($lockFile, $group);
} }
} }
@ -62,4 +74,18 @@ final class Only implements Terminable
return file_exists($lockFile); return file_exists($lockFile);
} }
/**
* Returns the group name.
*/
public static function group(): string
{
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (! file_exists($lockFile)) {
return '__pest_only';
}
return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line
}
} }

View File

@ -34,7 +34,7 @@ final class Parallel implements HandlesArguments
/** /**
* @var string[] * @var string[]
*/ */
private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry']; private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request'];
/** /**
* Whether the given command line arguments indicate that the test suite should be run in parallel. * Whether the given command line arguments indicate that the test suite should be run in parallel.
@ -42,6 +42,7 @@ final class Parallel implements HandlesArguments
public static function isEnabled(): bool public static function isEnabled(): bool
{ {
$argv = new ArgvInput; $argv = new ArgvInput;
if ($argv->hasParameterOption('--parallel')) { if ($argv->hasParameterOption('--parallel')) {
return true; return true;
} }

View File

@ -26,7 +26,7 @@ final class Laravel implements HandlesArguments
*/ */
public function handleArguments(array $arguments): array public function handleArguments(array $arguments): array
{ {
return self::whenUsingLaravel($arguments, function (array $arguments): array { return $this->whenUsingLaravel($arguments, function (array $arguments): array {
$this->ensureRunnerIsResolvable(); $this->ensureRunnerIsResolvable();
$arguments = $this->ensureEnvironmentVariables($arguments); $arguments = $this->ensureEnvironmentVariables($arguments);
@ -42,7 +42,7 @@ final class Laravel implements HandlesArguments
* @param CLosure(array<int, string>): array<int, string> $closure * @param CLosure(array<int, string>): array<int, string> $closure
* @return array<int, string> * @return array<int, string>
*/ */
private static function whenUsingLaravel(array $arguments, Closure $closure): array private function whenUsingLaravel(array $arguments, Closure $closure): array
{ {
$isLaravelApplication = InstalledVersions::isInstalled('laravel/framework', false); $isLaravelApplication = InstalledVersions::isInstalled('laravel/framework', false);
$isLaravelPackage = class_exists(\Orchestra\Testbench\TestCase::class); $isLaravelPackage = class_exists(\Orchestra\Testbench\TestCase::class);

View File

@ -11,7 +11,7 @@ final class CleanConsoleOutput extends ConsoleOutput
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected function doWrite(string $message, bool $newline): void protected function doWrite(string $message, bool $newline): void // @pest-arch-ignore-line
{ {
if ($this->isOpeningHeadline($message)) { if ($this->isOpeningHeadline($message)) {
return; return;

View File

@ -46,15 +46,27 @@ use function usleep;
*/ */
final class WrapperRunner implements RunnerInterface final class WrapperRunner implements RunnerInterface
{ {
/**
* The time to sleep between cycles.
*/
private const CYCLE_SLEEP = 10000; private const CYCLE_SLEEP = 10000;
/**
* The result printer.
*/
private readonly ResultPrinter $printer; private readonly ResultPrinter $printer;
/**
* The timer.
*/
private readonly Timer $timer; private readonly Timer $timer;
/** @var list<non-empty-string> */ /** @var list<non-empty-string> */
private array $pending = []; private array $pending = [];
/**
* The exit code.
*/
private int $exitcode = -1; private int $exitcode = -1;
/** @var array<positive-int,WrapperWorker> */ /** @var array<positive-int,WrapperWorker> */
@ -84,6 +96,9 @@ final class WrapperRunner implements RunnerInterface
/** @var non-empty-string[] */ /** @var non-empty-string[] */
private readonly array $parameters; private readonly array $parameters;
/**
* The code coverage filter registry.
*/
private CodeCoverageFilterRegistry $codeCoverageFilterRegistry; private CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
public function __construct( public function __construct(
@ -107,6 +122,9 @@ final class WrapperRunner implements RunnerInterface
$parameters = array_merge($parameters, $options->passthruPhp); $parameters = array_merge($parameters, $options->passthruPhp);
} }
/** @var array<int, non-empty-string> $parameters */
$parameters = $this->handleLaravelHerd($parameters);
$parameters[] = $wrapper; $parameters[] = $wrapper;
$this->parameters = $parameters; $this->parameters = $parameters;
@ -138,6 +156,21 @@ final class WrapperRunner implements RunnerInterface
return $this->complete($result); return $this->complete($result);
} }
/**
* Handles Laravel Herd's debug and coverage modes.
*
* @param array<string> $parameters
* @return array<string>
*/
private function handleLaravelHerd(array $parameters): array
{
if (isset($_ENV['HERD_DEBUG_INI'])) {
return array_merge($parameters, ['-c', $_ENV['HERD_DEBUG_INI']]);
}
return $parameters;
}
private function startWorkers(): void private function startWorkers(): void
{ {
for ($token = 1; $token <= $this->options->processes; $token++) { for ($token = 1; $token <= $this->options->processes; $token++) {
@ -390,6 +423,7 @@ final class WrapperRunner implements RunnerInterface
} }
$testSuite = (new LogMerger)->merge($this->junitFiles); $testSuite = (new LogMerger)->merge($this->junitFiles);
assert($testSuite instanceof \ParaTest\JUnit\TestSuite);
(new Writer)->write( (new Writer)->write(
$testSuite, $testSuite,
$this->options->configuration->logfileJunit(), $this->options->configuration->logfileJunit(),

156
src/Preset.php Normal file
View File

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Pest;
use Closure;
use Pest\Arch\Support\Composer;
use Pest\ArchPresets\AbstractPreset;
use Pest\ArchPresets\Custom;
use Pest\ArchPresets\Laravel;
use Pest\ArchPresets\Php;
use Pest\ArchPresets\Relaxed;
use Pest\ArchPresets\Security;
use Pest\ArchPresets\Strict;
use Pest\Exceptions\InvalidArgumentException;
use Pest\PendingCalls\TestCall;
use stdClass;
/**
* @internal
*/
final class Preset
{
/**
* The application / package base namespaces.
*
* @var ?array<int, string>
*/
private static ?array $baseNamespaces = null;
/**
* The custom presets.
*
* @var array<string, Closure>
*/
private static array $customPresets = [];
/**
* Creates a new preset instance.
*/
public function __construct()
{
//
}
/**
* Uses the Pest php preset and returns the test call instance.
*/
public function php(): Php
{
return $this->executePreset(new Php($this->baseNamespaces()));
}
/**
* Uses the Pest laravel preset and returns the test call instance.
*/
public function laravel(): Laravel
{
return $this->executePreset(new Laravel($this->baseNamespaces()));
}
/**
* Uses the Pest strict preset and returns the test call instance.
*/
public function strict(): Strict
{
return $this->executePreset(new Strict($this->baseNamespaces()));
}
/**
* Uses the Pest security preset and returns the test call instance.
*/
public function security(): AbstractPreset
{
return $this->executePreset(new Security($this->baseNamespaces()));
}
/**
* Uses the Pest relaxed preset and returns the test call instance.
*/
public function relaxed(): AbstractPreset
{
return $this->executePreset(new Relaxed($this->baseNamespaces()));
}
/**
* Uses the Pest custom preset and returns the test call instance.
*
* @internal
*/
public static function custom(string $name, Closure $execute): void
{
if (preg_match('/^[a-zA-Z]+$/', $name) === false) {
throw new InvalidArgumentException('The preset name must only contain words from a-z or A-Z.');
}
self::$customPresets[$name] = $execute;
}
/**
* Dynamically handle calls to the class.
*
* @param array<int, mixed> $arguments
*
* @throws InvalidArgumentException
*/
public function __call(string $name, array $arguments): AbstractPreset
{
if (! array_key_exists($name, self::$customPresets)) {
$availablePresets = [
...['php', 'laravel', 'strict', 'security', 'relaxed'],
...array_keys(self::$customPresets),
];
throw new InvalidArgumentException(sprintf('The preset [%s] does not exist. The available presets are [%s].', $name, implode(', ', $availablePresets)));
}
return $this->executePreset(new Custom($this->baseNamespaces(), $name, self::$customPresets[$name]));
}
/**
* Executes the given preset.
*
* @template TPreset of AbstractPreset
*
* @param TPreset $preset
* @return TPreset
*/
private function executePreset(AbstractPreset $preset): AbstractPreset
{
$this->baseNamespaces();
$preset->execute();
// $this->testCall->testCaseMethod->closure = (function () use ($preset): void {
// $preset->flush();
// })->bindTo(new stdClass);
return $preset;
}
/**
* Get the base namespaces for the application / package.
*
* @return array<int, string>
*/
private function baseNamespaces(): array
{
if (self::$baseNamespaces === null) {
self::$baseNamespaces = Composer::userNamespaces();
}
return self::$baseNamespaces;
}
}

View File

@ -9,9 +9,11 @@ use Pest\Contracts\TestCaseFilter;
use Pest\Contracts\TestCaseMethodFilter; use Pest\Contracts\TestCaseMethodFilter;
use Pest\Exceptions\TestCaseAlreadyInUse; use Pest\Exceptions\TestCaseAlreadyInUse;
use Pest\Exceptions\TestCaseClassOrTraitNotFound; use Pest\Exceptions\TestCaseClassOrTraitNotFound;
use Pest\Factories\Attribute;
use Pest\Factories\TestCaseFactory; use Pest\Factories\TestCaseFactory;
use Pest\Factories\TestCaseMethodFactory; use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Str; use Pest\Support\Str;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
@ -114,9 +116,9 @@ final class TestRepository
/** /**
* Gets the test case factory from the given filename. * Gets the test case factory from the given filename.
*/ */
public function get(string $filename): TestCaseFactory public function get(string $filename): ?TestCaseFactory
{ {
return $this->testCases[$filename]; return $this->testCases[$filename] ?? null;
} }
/** /**
@ -186,7 +188,10 @@ final class TestRepository
foreach ($testCase->methods as $method) { foreach ($testCase->methods as $method) {
foreach ($groups as $group) { foreach ($groups as $group) {
$method->groups[] = $group; $method->attributes[] = new Attribute(
Group::class,
[$group],
);
} }
} }

View File

@ -15,15 +15,15 @@ use Symfony\Component\Console\Output\OutputInterface;
/** /**
* @internal * @internal
*/ */
final class EnsureTeamCityEnabled implements ConfiguredSubscriber final readonly class EnsureTeamCityEnabled implements ConfiguredSubscriber
{ {
/** /**
* Creates a new Configured Subscriber instance. * Creates a new Configured Subscriber instance.
*/ */
public function __construct( public function __construct(
private readonly InputInterface $input, private InputInterface $input,
private readonly OutputInterface $output, private OutputInterface $output,
private readonly TestSuite $testSuite, private TestSuite $testSuite,
) {} ) {}
/** /**

View File

@ -17,13 +17,13 @@ final class ChainableClosure
*/ */
public static function boundWhen(Closure $condition, Closure $next): Closure public static function boundWhen(Closure $condition, Closure $next): Closure
{ {
return function () use ($condition, $next): void { return function (...$arguments) use ($condition, $next): void {
if (! is_object($this)) { // @phpstan-ignore-line if (! is_object($this)) { // @phpstan-ignore-line
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.'); throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
} }
if (\Pest\Support\Closure::bind($condition, $this, self::class)(...func_get_args())) { if (\Pest\Support\Closure::bind($condition, $this, self::class)(...$arguments)) {
\Pest\Support\Closure::bind($next, $this, self::class)(...func_get_args()); \Pest\Support\Closure::bind($next, $this, self::class)(...$arguments);
} }
}; };
} }
@ -33,13 +33,13 @@ final class ChainableClosure
*/ */
public static function bound(Closure $closure, Closure $next): Closure public static function bound(Closure $closure, Closure $next): Closure
{ {
return function () use ($closure, $next): void { return function (...$arguments) use ($closure, $next): void {
if (! is_object($this)) { // @phpstan-ignore-line if (! is_object($this)) { // @phpstan-ignore-line
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.'); throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
} }
\Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args()); \Pest\Support\Closure::bind($closure, $this, self::class)(...$arguments);
\Pest\Support\Closure::bind($next, $this, self::class)(...func_get_args()); \Pest\Support\Closure::bind($next, $this, self::class)(...$arguments);
}; };
} }
@ -48,9 +48,9 @@ final class ChainableClosure
*/ */
public static function unbound(Closure $closure, Closure $next): Closure public static function unbound(Closure $closure, Closure $next): Closure
{ {
return function () use ($closure, $next): void { return function (...$arguments) use ($closure, $next): void {
$closure(...func_get_args()); $closure(...$arguments);
$next(...func_get_args()); $next(...$arguments);
}; };
} }
@ -59,9 +59,9 @@ final class ChainableClosure
*/ */
public static function boundStatically(Closure $closure, Closure $next): Closure public static function boundStatically(Closure $closure, Closure $next): Closure
{ {
return static function () use ($closure, $next): void { return static function (...$arguments) use ($closure, $next): void {
\Pest\Support\Closure::bind($closure, null, self::class)(...func_get_args()); \Pest\Support\Closure::bind($closure, null, self::class)(...$arguments);
\Pest\Support\Closure::bind($next, null, self::class)(...func_get_args()); \Pest\Support\Closure::bind($next, null, self::class)(...$arguments);
}; };
} }
} }

View File

@ -20,13 +20,13 @@ final class Closure
*/ */
public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure
{ {
if ($closure == null) { if (! $closure instanceof \Closure) {
throw ShouldNotHappen::fromMessage('Could not bind null closure.'); throw ShouldNotHappen::fromMessage('Could not bind null closure.');
} }
$closure = BaseClosure::bind($closure, $newThis, $newScope); $closure = BaseClosure::bind($closure, $newThis, $newScope);
if ($closure == false) { if (! $closure instanceof \Closure) {
throw ShouldNotHappen::fromMessage('Could not bind closure.'); throw ShouldNotHappen::fromMessage('Could not bind closure.');
} }

View File

@ -13,6 +13,9 @@ use ReflectionParameter;
*/ */
final class Container final class Container
{ {
/**
* The instance of the container.
*/
private static ?Container $instance = null; private static ?Container $instance = null;
/** /**

View File

@ -138,7 +138,7 @@ final class Coverage
$totalCoverageAsString = $totalCoverage->asFloat() === 0.0 $totalCoverageAsString = $totalCoverage->asFloat() === 0.0
? '0.0' ? '0.0'
: number_format($totalCoverage->asFloat(), 1, '.', ''); : number_format(floor($totalCoverage->asFloat() * 10) / 10, 1, '.', '');
renderUsing($output); renderUsing($output);
render(<<<HTML render(<<<HTML
@ -197,7 +197,7 @@ final class Coverage
}; };
$array = []; $array = [];
foreach (array_filter($file->lineCoverageData(), 'is_array') as $line => $tests) { foreach (array_filter($file->lineCoverageData(), is_array(...)) as $line => $tests) {
$array = $eachLine($array, $tests, $line); $array = $eachLine($array, $tests, $line);
} }

View File

@ -31,7 +31,7 @@ final class ExceptionTrace
$message = str_replace(self::UNDEFINED_METHOD, 'Call to undefined method ', $message); $message = str_replace(self::UNDEFINED_METHOD, 'Call to undefined method ', $message);
if (class_exists((string) $class) && (is_countable(class_parents($class)) ? count(class_parents($class)) : 0) > 0 && array_values(class_parents($class))[0] === TestCase::class) { // @phpstan-ignore-line if (class_exists((string) $class) && (is_countable(class_parents($class)) ? count(class_parents($class)) : 0) > 0 && array_values(class_parents($class))[0] === TestCase::class) { // @phpstan-ignore-line
$message .= '. Did you forget to use the [uses()] function? Read more at: https://pestphp.com/docs/configuring-tests'; $message .= '. Did you forget to use the [pest()->extend()] function? Read more at: https://pestphp.com/docs/configuring-tests';
} }
Reflection::setPropertyValue($throwable, 'message', $message); Reflection::setPropertyValue($throwable, 'message', $message);

View File

@ -10,7 +10,7 @@ use SebastianBergmann\RecursionContext\Context;
/** /**
* @internal * @internal
*/ */
final class Exporter final readonly class Exporter
{ {
/** /**
* The maximum number of items in an array to export. * The maximum number of items in an array to export.
@ -21,7 +21,7 @@ final class Exporter
* Creates a new Exporter instance. * Creates a new Exporter instance.
*/ */
public function __construct( public function __construct(
private readonly BaseExporter $exporter, private BaseExporter $exporter,
) { ) {
// ... // ...
} }
@ -64,8 +64,6 @@ final class Exporter
continue; continue;
} }
assert(is_array($data));
$result[] = $context->contains($data[$key]) !== false $result[] = $context->contains($data[$key]) !== false
? '*RECURSION*' ? '*RECURSION*'
: sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context)); : sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context));

View File

@ -10,12 +10,12 @@ use Pest\Expectation;
/** /**
* @internal * @internal
*/ */
final class HigherOrderCallables final readonly class HigherOrderCallables
{ {
/** /**
* Creates a new Higher Order Callables instances. * Creates a new Higher Order Callables instances.
*/ */
public function __construct(private readonly object $target) public function __construct(private object $target)
{ {
// .. // ..
} }
@ -49,16 +49,6 @@ final class HigherOrderCallables
return $this->expect($value); return $this->expect($value);
} }
/**
* Execute the given callable after the test has executed the setup method.
*
* @deprecated This method is deprecated. Please use `defer` instead.
*/
public function tap(callable $callable): object
{
return $this->defer($callable);
}
/** /**
* Execute the given callable after the test has executed the setup method. * Execute the given callable after the test has executed the setup method.
*/ */

View File

@ -11,6 +11,7 @@ use Pest\TestSuite;
use ReflectionClass; use ReflectionClass;
use ReflectionException; use ReflectionException;
use ReflectionFunction; use ReflectionFunction;
use ReflectionMethod;
use ReflectionNamedType; use ReflectionNamedType;
use ReflectionParameter; use ReflectionParameter;
use ReflectionProperty; use ReflectionProperty;
@ -213,4 +214,74 @@ final class Reflection
{ {
return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null; return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null;
} }
/**
* Get the properties from the given reflection class.
*
* Used by `expect()->toHavePropertiesDocumented()`.
*
* @param ReflectionClass<object> $reflectionClass
* @return array<int, ReflectionProperty>
*/
public static function getPropertiesFromReflectionClass(ReflectionClass $reflectionClass): array
{
$getProperties = fn (ReflectionClass $reflectionClass): array => array_filter(
array_map(
fn (ReflectionProperty $property): \ReflectionProperty => $property,
$reflectionClass->getProperties(),
), fn (ReflectionProperty $property): bool => $property->getDeclaringClass()->getName() === $reflectionClass->getName(),
);
$propertiesFromTraits = [];
foreach ($reflectionClass->getTraits() as $trait) {
$propertiesFromTraits = array_merge($propertiesFromTraits, $getProperties($trait));
}
$propertiesFromTraits = array_map(
fn (ReflectionProperty $property): string => $property->getName(),
$propertiesFromTraits,
);
return array_values(
array_filter(
$getProperties($reflectionClass),
fn (ReflectionProperty $property): bool => ! in_array($property->getName(), $propertiesFromTraits, true),
),
);
}
/**
* Get the methods from the given reflection class.
*
* Used by `expect()->toHaveMethodsDocumented()`.
*
* @param ReflectionClass<object> $reflectionClass
* @return array<int, ReflectionMethod>
*/
public static function getMethodsFromReflectionClass(ReflectionClass $reflectionClass, int $filter = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED | ReflectionMethod::IS_PRIVATE): array
{
$getMethods = fn (ReflectionClass $reflectionClass): array => array_filter(
array_map(
fn (ReflectionMethod $method): \ReflectionMethod => $method,
$reflectionClass->getMethods($filter),
), fn (ReflectionMethod $method): bool => $method->getDeclaringClass()->getName() === $reflectionClass->getName(),
);
$methodsFromTraits = [];
foreach ($reflectionClass->getTraits() as $trait) {
$methodsFromTraits = array_merge($methodsFromTraits, $getMethods($trait));
}
$methodsFromTraits = array_map(
fn (ReflectionMethod $method): string => $method->getName(),
$methodsFromTraits,
);
return array_values(
array_filter(
$getMethods($reflectionClass),
fn (ReflectionMethod $method): bool => ! in_array($method->getName(), $methodsFromTraits, true),
),
);
}
} }

View File

@ -14,15 +14,21 @@ use Symfony\Component\Process\Process;
final class GitDirtyTestCaseFilter implements TestCaseFilter final class GitDirtyTestCaseFilter implements TestCaseFilter
{ {
/** /**
* @var string[]|null * @var array<int, string>|null
*/ */
private ?array $changedFiles = null; private ?array $changedFiles = null;
/**
* Creates a new instance of the filter.
*/
public function __construct(private readonly string $projectRoot) public function __construct(private readonly string $projectRoot)
{ {
// ... // ...
} }
/**
* {@inheritdoc}
*/
public function accept(string $testCaseFilename): bool public function accept(string $testCaseFilename): bool
{ {
if ($this->changedFiles === null) { if ($this->changedFiles === null) {
@ -41,6 +47,9 @@ final class GitDirtyTestCaseFilter implements TestCaseFilter
return in_array($relativePath, $this->changedFiles, true); return in_array($relativePath, $this->changedFiles, true);
} }
/**
* Loads the changed files.
*/
private function loadChangedFiles(): void private function loadChangedFiles(): void
{ {
$process = new Process(['git', 'status', '--short', '--', '*.php']); $process = new Process(['git', 'status', '--short', '--', '*.php']);

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Pest\TestCaseMethodFilters;
use Pest\Contracts\TestCaseMethodFilter;
use Pest\Factories\TestCaseMethodFactory;
final readonly class AssigneeTestCaseFilter implements TestCaseMethodFilter
{
/**
* Create a new filter instance.
*/
public function __construct(private string $assignee)
{
//
}
/**
* Filter the test case methods.
*/
public function accept(TestCaseMethodFactory $factory): bool
{
return array_filter($factory->assignees, fn (string $assignee): bool => str_starts_with($assignee, $this->assignee)) !== [];
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Pest\TestCaseMethodFilters;
use Pest\Contracts\TestCaseMethodFilter;
use Pest\Factories\TestCaseMethodFactory;
final readonly class IssueTestCaseFilter implements TestCaseMethodFilter
{
/**
* Create a new filter instance.
*/
public function __construct(private int $number)
{
//
}
/**
* Filter the test case methods.
*/
public function accept(TestCaseMethodFactory $factory): bool
{
return in_array($this->number, $factory->issues, true);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\TestCaseMethodFilters;
use Pest\Contracts\TestCaseMethodFilter;
use Pest\Factories\TestCaseMethodFactory;
final readonly class NotesTestCaseFilter implements TestCaseMethodFilter
{
/**
* Filter the test case methods.
*/
public function accept(TestCaseMethodFactory $factory): bool
{
return $factory->notes !== [];
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Pest\TestCaseMethodFilters;
use Pest\Contracts\TestCaseMethodFilter;
use Pest\Factories\TestCaseMethodFactory;
final readonly class PrTestCaseFilter implements TestCaseMethodFilter
{
/**
* Create a new filter instance.
*/
public function __construct(private int $number)
{
//
}
/**
* Filter the test case methods.
*/
public function accept(TestCaseMethodFactory $factory): bool
{
return in_array($this->number, $factory->prs, true);
}
}

View File

@ -7,8 +7,11 @@ namespace Pest\TestCaseMethodFilters;
use Pest\Contracts\TestCaseMethodFilter; use Pest\Contracts\TestCaseMethodFilter;
use Pest\Factories\TestCaseMethodFactory; use Pest\Factories\TestCaseMethodFactory;
final class TodoTestCaseFilter implements TestCaseMethodFilter final readonly class TodoTestCaseFilter implements TestCaseMethodFilter
{ {
/**
* Filter the test case methods.
*/
public function accept(TestCaseMethodFactory $factory): bool public function accept(TestCaseMethodFactory $factory): bool
{ {
return $factory->todo; return $factory->todo;

View File

@ -7,14 +7,13 @@
| |
| The closure you provide to your test functions is always bound to a specific PHPUnit test | The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may | case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "uses()" function to bind a different classes or traits. | need to change it using the "pest()" function to bind a different classes or traits.
| |
*/ */
uses( pest()->extend(Tests\TestCase::class)
Tests\TestCase::class, // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
// Illuminate\Foundation\Testing\RefreshDatabase::class, ->in('Feature');
)->in('Feature');
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -7,11 +7,11 @@
| |
| The closure you provide to your test functions is always bound to a specific PHPUnit test | The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may | case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "uses()" function to bind a different classes or traits. | need to change it using the "pest()" function to bind a different classes or traits.
| |
*/ */
// uses(Tests\TestCase::class)->in('Feature'); // pest()->extend(Tests\TestCase::class)->in('Feature');
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

Some files were not shown because too many files have changed in this diff Show More