Compare commits

...

277 Commits

Author SHA1 Message Date
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
9ceb0834ae chore: updates snapshots 2024-08-22 21:07:39 +01:00
86d2191cae chore: refactor TestClosureMustNotBeStatic 2024-08-22 20:59:42 +01:00
748beb17d5 chore: adjusts memory limit 2024-08-22 20:59:42 +01:00
7ba235f61a Merge pull request #1129 from tomb1n0/bugfix/alwaysCallTeardownRegardlessOfExceptions
[Bug] Always call parent teardown even if an exception is thrown
2024-08-22 20:50:59 +01:00
700bd517f4 Merge pull request #1117 from peterfox/bug/catch-static-closures
[Bug] provided explaination for static closures
2024-08-22 20:33:58 +01:00
cbcfa2c5e2 Merge pull request #1100 from faissaloux/fix-use-strict-types
Fix `toUseStrictTypes`
2024-08-22 20:32:21 +01:00
243e45a551 Merge pull request #1097 from arifszn/2.x
[2.x] Modify `Result::exitCode` logic to address warning handling with `--fail-on-warning`
2024-08-22 20:31:34 +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
823c3d4b17 chore: updates snapshots 2024-08-20 22:49:23 +01:00
39c9b15bc0 Update visual_snapshot_of_help_command_output.snap 2024-08-20 22:44:39 +01:00
b13acb630d release: 2.35.1 2024-08-20 22:41:50 +01:00
172d94c0ca chore: bumps depedencies 2024-08-20 22:41:07 +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
0697555dc2 chore: adjusts sponsors 2024-08-05 10:42:52 +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
adb2fb51df Always call parent teardown even if an exception is thrown 2024-04-08 10:03:55 +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
0ccbe5c8f0 Remove Laravel serialisable closure 2024-03-17 17:23:17 +00:00
a4f8ae1a12 Handles tests where a static closure is provided 2024-03-17 16:48:43 +00:00
6094682158 Add static closure check 2024-03-17 12:04:38 +00:00
2c3234fb3d fix bool type 2024-02-21 17:09:16 +01:00
1b64fef7ba fix toUseStrictTypes 2024-02-21 16:58:40 +01:00
a136231503 fix: modify Result::exitCode logic to address warning handling with --fail-on-warning 2024-02-20 18:24:37 +06: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
167 changed files with 4395 additions and 951 deletions

View File

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

View File

@ -13,12 +13,9 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
symfony: ['6.4', '7.0']
php: ['8.1', '8.2', '8.3']
symfony: ['7.1']
php: ['8.2', '8.3', '8.4']
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 }}
@ -39,7 +36,13 @@ jobs:
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- 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
run: composer test:unit
@ -48,4 +51,5 @@ jobs:
run: composer test:parallel
- name: Integration Tests
if: ${{ matrix.php != '8.4' }}
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:
```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.

View File

@ -1,5 +1,5 @@
<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">
<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>
@ -9,6 +9,9 @@
</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.
- Explore our docs at **[pestphp.com »](https://pestphp.com)**
@ -31,8 +34,10 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
- [Akaunting](https://akaunting.com/?ref=pestphp)
- [Codecourse](https://codecourse.com/?ref=pestphp)
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
- [Laracasts](https://laracasts.com/?ref=pestphp)
- [Localazy](https://localazy.com/?ref=pestphp)
- [Route4Me](https://www.route4me.com/?ref=pestphp)
- [Zapiet](https://www.zapiet.com/?ref=pestphp)
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.

View File

@ -2,10 +2,10 @@
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`
- 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)
- 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}...3.x](https://github.com/pestphp/pest/compare/{latest_version}...3.x)
- Update the version number in [src/Pest.php](src/Pest.php)
- Run the tests locally using: `composer test`
- Commit the Pest file with the message: `git commit -m "release: vX.X.X"`

View File

@ -4,6 +4,10 @@
use Pest\Kernel;
use Pest\Panic;
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\TestSuite;
use Symfony\Component\Console\Input\ArgvInput;
@ -17,8 +21,10 @@ use Symfony\Component\Console\Output\ConsoleOutput;
$dirty = false;
$todo = false;
$notes = false;
foreach ($arguments as $key => $value) {
if ($value === '--compact') {
$_SERVER['COLLISION_PRINTER_COMPACT'] = 'true';
unset($arguments[$key]);
@ -29,8 +35,14 @@ use Symfony\Component\Console\Output\ConsoleOutput;
unset($arguments[$key]);
}
if (str_contains($value, '--test-directory')) {
if (str_contains($value, '--test-directory=')) {
unset($arguments[$key]);
} else if ($value === '--test-directory') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if ($value === '--dirty') {
@ -43,6 +55,61 @@ use Symfony\Component\Console\Output\ConsoleOutput;
unset($arguments[$key]);
}
if ($value === '--notes') {
$notes = true;
unset($arguments[$key]);
}
if (str_contains($value, '--assignee=')) {
unset($arguments[$key]);
} else if ($value === '--assignee') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--issue=')) {
unset($arguments[$key]);
} else if ($value === '--issue') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--ticket=')) {
unset($arguments[$key]);
} else if ($value === '--ticket') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--pr=')) {
unset($arguments[$key]);
} else if ($value === '--pr') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--pull-request=')) {
unset($arguments[$key]);
} else if ($value === '--pull-request') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--teamcity')) {
unset($arguments[$key]);
$arguments[] = '--no-output';
@ -50,6 +117,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
}
}
// Used when Pest is required using composer.
$vendorPath = dirname(__DIR__, 4).'/vendor/autoload.php';
@ -81,6 +149,30 @@ use Symfony\Component\Console\Output\ConsoleOutput;
$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';
$output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated);

View File

@ -17,17 +17,18 @@
}
],
"require": {
"php": "^8.1.0",
"brianium/paratest": "^7.3.1",
"nunomaduro/collision": "^7.10.0|^8.3.0",
"nunomaduro/termwind": "^1.15.1|^2.0.1",
"pestphp/pest-plugin": "^2.1.1",
"pestphp/pest-plugin-arch": "^2.7.0",
"phpunit/phpunit": "^10.5.17"
"php": "^8.2.0",
"brianium/paratest": "^7.5.5",
"nunomaduro/collision": "^8.4.0",
"nunomaduro/termwind": "^2.1.0",
"pestphp/pest-plugin": "^3.0.0",
"pestphp/pest-plugin-arch": "^3.0.0",
"pestphp/pest-plugin-mutate": "^3.0.5",
"phpunit/phpunit": "^11.3.6"
},
"conflict": {
"phpunit/phpunit": ">10.5.17",
"sebastian/exporter": "<5.1.0",
"phpunit/phpunit": ">11.3.6",
"sebastian/exporter": "<6.0.0",
"webmozart/assert": "<1.11.0"
},
"autoload": {
@ -51,9 +52,9 @@
]
},
"require-dev": {
"pestphp/pest-dev-tools": "^2.16.0",
"pestphp/pest-plugin-type-coverage": "^2.8.5",
"symfony/process": "^6.4.0|^7.1.3"
"pestphp/pest-dev-tools": "^3.0.0",
"pestphp/pest-plugin-type-coverage": "^3.0.0",
"symfony/process": "^7.1.5"
},
"minimum-stability": "dev",
"prefer-stable": true,
@ -73,11 +74,11 @@
"test:refacto": "rector --dry-run",
"test:lint": "pint --test",
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
"test:type:coverage": "php bin/pest --type-coverage --min=100",
"test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100",
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
"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",
"test": [
"@test:refacto",
@ -92,6 +93,8 @@
"extra": {
"pest": {
"plugins": [
"Pest\\Mutate\\Plugins\\Mutate",
"Pest\\Plugins\\Configuration",
"Pest\\Plugins\\Bail",
"Pest\\Plugins\\Cache",
"Pest\\Plugins\\Coverage",

View File

@ -1,5 +1,37 @@
<?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);
/*
@ -22,7 +54,7 @@ use PHPUnit\Util\ThrowableToStringMapper;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class ThrowableBuilder
final readonly class ThrowableBuilder
{
/**
* @throws Exception

View File

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

View File

@ -32,19 +32,27 @@
* 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;
use Exception;
use Pest\Contracts\HasPrintableTestCaseName;
use PHPUnit\Framework\SelfDescribing;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\PhptTestCase;
use RecursiveFilterIterator;
use RecursiveIterator;
use function end;
use function implode;
use function preg_match;
use function sprintf;
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
*/
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)
{
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
@ -78,29 +94,38 @@ final class NameFilterIterator extends RecursiveFilterIterator
return true;
}
$tmp = $this->describe($test);
if ($test instanceof PhptTestCase) {
return false;
}
if ($tmp[0] !== '') {
$name = implode('::', $tmp);
if ($test instanceof HasPrintableTestCaseName) {
$name = $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName();
} 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);
$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) {
// Handles:
// * testAssertEqualsSucceeds#4
@ -108,17 +133,17 @@ final class NameFilterIterator extends RecursiveFilterIterator
if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) {
if (isset($matches[3]) && $matches[2] < $matches[3]) {
$filter = sprintf(
'%s.*with dataset #(\d+)$',
$matches[1]
'%s.*with data set #(\d+)$',
$matches[1],
);
$this->filterMin = (int) $matches[2];
$this->filterMax = (int) $matches[3];
$dataSetMinimum = (int) $matches[2];
$dataSetMaximum = (int) $matches[3];
} else {
$filter = sprintf(
'%s.*with dataset #%s$',
'%s.*with data set #%s$',
$matches[1],
$matches[2]
$matches[2],
);
}
} // Handles:
@ -126,9 +151,9 @@ final class NameFilterIterator extends RecursiveFilterIterator
// * testDetermineJsonError@JSON.*
elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) {
$filter = sprintf(
'%s.*with dataset "%s"$',
'%s.*with data set "%s"$',
$matches[1],
$matches[2]
$matches[2],
);
}
@ -139,34 +164,15 @@ final class NameFilterIterator extends RecursiveFilterIterator
str_replace(
'/',
'\\/',
$filter
)
$filter,
),
);
}
$this->filter = $filter;
}
/**
* @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];
return [
'regularExpression' => $filter,
'dataSetMinimum' => $dataSetMinimum,
'dataSetMaximum' => $dataSetMaximum,
];
}
}

View File

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

View File

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

View File

@ -45,6 +45,8 @@ declare(strict_types=1);
namespace PHPUnit\TextUI\Command;
use const PHP_EOL;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
use PHPUnit\TextUI\Configuration\Configuration;
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
*/
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)
{
@ -76,16 +78,16 @@ final class WarmCodeCoverageCacheCommand implements Command
if (! $this->configuration->hasCoverageCacheDirectory()) {
return Result::from(
'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()) {
return Result::from(
'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->disableCodeCoverageIgnore(),
$this->configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage(),
$this->codeCoverageFilterRegistry->get()
$this->codeCoverageFilterRegistry->get(),
);
return Result::from();

View File

@ -43,7 +43,7 @@ declare(strict_types=1);
* 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\SkippedSubscriber;
@ -51,21 +51,16 @@ use ReflectionClass;
/**
* @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
{
/**
* Notifies the printer that a test was skipped.
*/
public function notify(Skipped $event): void
{
if (str_contains($event->message(), '__TODO__')) {
$this->printTodoItem();
}
$this->printer()->testSkipped();
$this->logger()->testSkipped($event);
}
/**

View File

@ -33,7 +33,6 @@
*/
declare(strict_types=1);
/*
* 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
*/
final class TestSuiteFilterProcessor
final readonly class TestSuiteFilterProcessor
{
/**
* @throws Event\RuntimeException
@ -70,24 +69,24 @@ final class TestSuiteFilterProcessor
if (! $configuration->hasFilter() &&
! $configuration->hasGroups() &&
! $configuration->hasExcludeGroups() &&
! $configuration->hasExcludeFilter() &&
! $configuration->hasTestsCovering() &&
! $configuration->hasTestsUsing() &&
! Only::isEnabled()
) {
! Only::isEnabled()) {
return;
}
if ($configuration->hasExcludeGroups()) {
$factory->addExcludeGroupFilter(
$configuration->excludeGroups()
$configuration->excludeGroups(),
);
}
if (Only::isEnabled()) {
$factory->addIncludeGroupFilter(['__pest_only']);
$factory->addIncludeGroupFilter([Only::group()]);
} elseif ($configuration->hasGroups()) {
$factory->addIncludeGroupFilter(
$configuration->groups()
$configuration->groups(),
);
}
@ -95,8 +94,8 @@ final class TestSuiteFilterProcessor
$factory->addIncludeGroupFilter(
array_map(
static fn (string $name): string => '__phpunit_covers_'.$name,
$configuration->testsCovering()
)
$configuration->testsCovering(),
),
);
}
@ -104,21 +103,27 @@ final class TestSuiteFilterProcessor
$factory->addIncludeGroupFilter(
array_map(
static fn (string $name): string => '__phpunit_uses_'.$name,
$configuration->testsUsing()
)
$configuration->testsUsing(),
),
);
}
if ($configuration->hasExcludeFilter()) {
$factory->addExcludeNameFilter(
$configuration->excludeFilter(),
);
}
if ($configuration->hasFilter()) {
$factory->addNameFilter(
$configuration->filter()
$factory->addIncludeNameFilter(
$configuration->filter(),
);
}
$suite->injectFilter($factory);
Event\Facade::emitter()->testSuiteFiltered(
Event\TestSuite\TestSuiteBuilder::from($suite)
Event\TestSuite\TestSuiteBuilder::from($suite),
);
}
}

View File

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

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

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

93
src/ArchPresets/Php.php Normal file
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
*/
final class BootKernelDump implements Bootstrapper
final readonly class BootKernelDump implements Bootstrapper
{
/**
* Creates a new Boot Kernel Dump instance.
*/
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>
*/
public const FILES = [
'c7b9c8a96006dea314204a8f09a8764e51ce0b9b79aadd58da52e8c328db4870' => 'Runner/Filter/NameFilterIterator.php',
'c7c09ab7c9378710b27f761a4b2948196cbbdf2a73e4389bcdca1e7c94fa9c21' => 'Runner/ResultCache/DefaultResultCache.php',
'bc8718c89264f65800beabc23e51c6d3bcff87dfc764a12179ef5dbfde272c8b' => 'Runner/TestSuiteLoader.php',
'f41e48d6cb546772a7de4f8e66b6b7ce894a5318d063eb52e354d206e96c701c' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
'cb7519f2d82893640b694492cf7ec9528da80773cc1d259634181b5d393528b5' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
'2f06e4b1a9f3a24145bfc7ea25df4f124117f940a2cde30a04d04d5678006bff' => 'TextUI/TestSuiteFilterProcessor.php',
'ef64a657ed9c0067791483784944107827bf227c7e3200f212b6751876b99e25' => 'Event/Value/ThrowableBuilder.php',
'c78f96e34b98ed01dd8106539d59b8aa8d67f733274118b827c01c5c4111c033' => 'Logging/JUnit/JunitXmlLogger.php',
'c96b1cb57d7fc8e649f4c13a8abe418c2541bcfab194fb6702b99f777f52ee84' => 'Runner/Filter/NameFilterIterator.php',
'a4a43de01f641c6944ee83d963795a46d32b5206b5ab3bbc6cce76e67190acbf' => 'Runner/ResultCache/DefaultResultCache.php',
'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php',
'3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
'8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
'43883b7e5811886cf3731c8ed6304d5a77078d9731e1e505abc2da36bde19f3e' => 'TextUI/TestSuiteFilterProcessor.php',
'357d5cd7007f8559b26e1b8cdf43bb6fb15b51b79db981779da6f31b7ec39dad' => 'Event/Value/ThrowableBuilder.php',
'676273f1fe483877cf2d95c5aedbf9ae5d6a8e2f4c12d6ce716df6591e6db023' => 'Logging/JUnit/JunitXmlLogger.php',
];
/**

View File

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

View File

@ -11,13 +11,13 @@ use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class BootView implements Bootstrapper
final readonly class BootView implements Bootstrapper
{
/**
* Creates a new instance of the Boot View.
*/
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;
use Closure;
use Pest\Exceptions\DatasetArgsCountMismatch;
use Pest\Exceptions\DatasetArgumentsMismatch;
use Pest\Preset;
use Pest\Support\ChainableClosure;
use Pest\Support\ExceptionTrace;
use Pest\Support\Reflection;
use Pest\TestSuite;
use PHPUnit\Framework\Attributes\PostCondition;
use PHPUnit\Framework\TestCase;
use ReflectionException;
use ReflectionFunction;
use ReflectionParameter;
use Throwable;
/**
@ -32,11 +35,40 @@ trait Testable
*/
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.
*/
public ?string $__describing = null;
/**
* Whether the test has ran or not.
*/
public bool $__ran = false;
/**
* The test's test closure.
*/
@ -67,15 +99,6 @@ trait Testable
*/
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.
*/
@ -88,11 +111,36 @@ trait Testable
if ($test->hasMethod($name)) {
$method = $test->getMethod($name);
$this->__description = self::$__latestDescription = $method->description;
self::$__latestAssignees = $method->assignees;
self::$__latestNotes = $method->notes;
self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs;
$this->__describing = $method->describing;
$this->__test = $method->getClosure($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.
*/
@ -186,14 +234,22 @@ trait Testable
/**
* Gets executed before the Test Case.
*/
protected function setUp(): void
protected function setUp(...$arguments): void
{
TestSuite::getInstance()->test = $this;
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$description = $this->dataName() ? $method->description.' with '.$this->dataName() : $method->description;
$description = htmlspecialchars(html_entity_decode($description), ENT_NOQUOTES);
$method->setUp($this);
$description = $method->description;
if ($this->dataName()) {
$description = str_contains((string) $description, ':dataset')
? str_replace(':dataset', str_replace('dataset ', '', $this->dataName()), (string) $description)
: $description.' with '.$this->dataName();
}
$description = htmlspecialchars(html_entity_decode((string) $description), ENT_NOQUOTES);
if ($method->repetitions > 1) {
$matches = [];
@ -211,6 +267,10 @@ trait Testable
}
$this->__description = self::$__latestDescription = $description;
self::$__latestAssignees = $method->assignees;
self::$__latestNotes = $method->notes;
self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs;
parent::setUp();
@ -220,13 +280,13 @@ trait Testable
$beforeEach = ChainableClosure::bound($this->__beforeEach, $beforeEach);
}
$this->__callClosure($beforeEach, func_get_args());
$this->__callClosure($beforeEach, $arguments);
}
/**
* Gets executed after the Test Case.
*/
protected function tearDown(): void
protected function tearDown(...$arguments): void
{
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
@ -234,11 +294,16 @@ trait Testable
$afterEach = ChainableClosure::bound($this->__afterEach, $afterEach);
}
$this->__callClosure($afterEach, func_get_args());
try {
$this->__callClosure($afterEach, func_get_args());
} finally {
parent::tearDown();
parent::tearDown();
TestSuite::getInstance()->test = null;
TestSuite::getInstance()->test = null;
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$method->tearDown($this);
}
}
/**
@ -249,7 +314,7 @@ trait Testable
private function __runTest(Closure $closure, ...$args): mixed
{
$arguments = $this->__resolveTestArguments($args);
$this->__ensureDatasetArgumentNumberMatches($arguments);
$this->__ensureDatasetArgumentNameAndNumberMatches($arguments);
return $this->__callClosure($closure, $arguments);
}
@ -264,7 +329,12 @@ trait Testable
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
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');
@ -286,7 +356,7 @@ trait Testable
return $arguments;
}
if (! $arguments[0] instanceof Closure) {
if (! isset($arguments[0]) || ! $arguments[0] instanceof Closure) {
return $arguments;
}
@ -309,9 +379,9 @@ trait Testable
* Ensures dataset items count matches underlying test case required parameters
*
* @throws ReflectionException
* @throws DatasetArgsCountMismatch
* @throws DatasetArgumentsMismatch
*/
private function __ensureDatasetArgumentNumberMatches(array $arguments): void
private function __ensureDatasetArgumentNameAndNumberMatches(array $arguments): void
{
if ($arguments === []) {
return;
@ -322,11 +392,21 @@ trait Testable
$requiredParametersCount = $testReflection->getNumberOfRequiredParameters();
$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;
}
throw new DatasetArgsCountMismatch($requiredParametersCount, $suppliedParametersCount);
if (isset($testParameterNames[0]) && $suppliedParametersCount >= $requiredParametersCount) {
return;
}
throw new DatasetArgumentsMismatch($requiredParametersCount, $suppliedParametersCount);
}
/**
@ -337,7 +417,15 @@ trait Testable
return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
}
/** @postCondition */
/**
* Uses the given preset on the test.
*/
public function preset(): Preset
{
return new Preset;
}
#[PostCondition]
protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void
{
if (count($this->__snapshotChanges) === 0) {
@ -378,4 +466,17 @@ trait Testable
{
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
*/
final class Help
final readonly class Help
{
/**
* The Command messages.
@ -27,7 +27,7 @@ final class Help
/**
* 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
*/
final class Thanks
final readonly class Thanks
{
/**
* The support options.
@ -33,8 +33,8 @@ final class Thanks
* Creates a new Console Command instance.
*/
public function __construct(
private readonly InputInterface $input,
private readonly OutputInterface $output
private InputInterface $input,
private OutputInterface $output
) {
// ..
}
@ -72,13 +72,13 @@ final class Thanks
}
if ($wantsToSupport === true) {
if (PHP_OS_FAMILY == 'Darwin') {
if (PHP_OS_FAMILY === 'Darwin') {
exec('open https://github.com/pestphp/pest');
}
if (PHP_OS_FAMILY == 'Windows') {
if (PHP_OS_FAMILY === 'Windows') {
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');
}
}

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

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use InvalidArgumentException;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Pest\Factories\TestCaseMethodFactory;
use Symfony\Component\Console\Exception\ExceptionInterface;
/**
* @internal
*/
final class TestClosureMustNotBeStatic extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new Exception instance.
*/
public function __construct(TestCaseMethodFactory $method)
{
parent::__construct(
sprintf(
'Test closure must not be static. Please remove the `static` keyword from the `%s` method in `%s`.',
$method->description,
$method->filename
)
);
}
}

View File

@ -30,9 +30,12 @@ use Pest\Expectations\HigherOrderExpectation;
use Pest\Expectations\OppositeExpectation;
use Pest\Matchers\Any;
use Pest\Support\ExpectationPipeline;
use Pest\Support\Reflection;
use PHPUnit\Architecture\Elements\ObjectDescription;
use PHPUnit\Framework\ExpectationFailedException;
use ReflectionEnum;
use ReflectionMethod;
use ReflectionProperty;
/**
* @template TValue
@ -220,7 +223,7 @@ final class Expectation
throw new BadMethodCallException('Expectation value is not iterable.');
}
if (count($callbacks) == 0) {
if ($callbacks === []) {
throw new InvalidArgumentException('No sequence expectations defined.');
}
@ -434,6 +437,71 @@ final class Expectation
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.
*/
@ -441,12 +509,25 @@ final class Expectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'),
fn (ObjectDescription $object): bool => (bool) preg_match('/^<\?php\s+declare\(.*?strict_types\s?=\s?1.*?\);/', (string) file_get_contents($object->path)),
'to use strict types',
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), ' == '), // @pest-arch-ignore-line
'to use strict equality',
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' == ')),
);
}
/**
* 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.
*
* @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(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->hasMethod($method),
sprintf("to have method '%s'", $method),
fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => $object->reflectionClass->hasMethod($method))) === count($methods),
sprintf("to have method '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target has a specific methods.
*
* @param array<int, string> $methods
*/
public function toHaveMethods(array $methods): ArchExpectation
{
return $this->toHaveMethod($methods);
}
/**
* Not supported.
*/
public function toHavePublicMethodsBesides(): void
{
throw InvalidExpectation::fromMethods(['toHavePublicMethodsBesides']);
}
/**
* Not supported.
*/
public function toHavePublicMethods(): void
{
throw InvalidExpectation::fromMethods(['toHavePublicMethods']);
}
/**
* Not supported.
*/
public function toHaveProtectedMethodsBesides(): void
{
throw InvalidExpectation::fromMethods(['toHaveProtectedMethodsBesides']);
}
/**
* Not supported.
*/
public function toHaveProtectedMethods(): void
{
throw InvalidExpectation::fromMethods(['toHaveProtectedMethods']);
}
/**
* Not supported.
*/
public function toHavePrivateMethodsBesides(): void
{
throw InvalidExpectation::fromMethods(['toHavePrivateMethodsBesides']);
}
/**
* Not supported.
*/
public function toHavePrivateMethods(): void
{
throw InvalidExpectation::fromMethods(['toHavePrivateMethods']);
}
/**
* Asserts that the given expectation target is enum.
*/
@ -585,8 +728,6 @@ final class Expectation
/**
* Asserts that the given expectation target to be subclass of the given class.
*
* @param class-string $class
*/
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.
*/
@ -627,7 +801,7 @@ final class Expectation
/**
* 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
{
@ -671,7 +845,7 @@ final class Expectation
/**
* 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
{
@ -711,7 +885,10 @@ final class Expectation
return ToUseNothing::make($this);
}
public function toBeUsed(): never
/**
* Not supported.
*/
public function toBeUsed(): void
{
throw InvalidExpectation::fromMethods(['toBeUsed']);
}
@ -855,8 +1032,6 @@ final class Expectation
/**
* Asserts that the given expectation target to have the given attribute.
*
* @param class-string<Attribute> $attribute
*/
public function toHaveAttribute(string $attribute): ArchExpectation
{

View File

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

View File

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

View File

@ -18,9 +18,13 @@ use Pest\Exceptions\InvalidExpectation;
use Pest\Expectation;
use Pest\Support\Arr;
use Pest\Support\Exporter;
use Pest\Support\Reflection;
use PHPUnit\Architecture\Elements\ObjectDescription;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExpectationFailedException;
use ReflectionMethod;
use ReflectionProperty;
use stdClass;
/**
* @internal
@ -29,14 +33,14 @@ use PHPUnit\Framework\ExpectationFailedException;
*
* @mixin Expectation<TValue>
*/
final class OppositeExpectation
final readonly class OppositeExpectation
{
/**
* Creates a new opposite expectation.
*
* @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.
@ -75,6 +79,66 @@ final class OppositeExpectation
), 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.
*/
@ -82,12 +146,25 @@ final class OppositeExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'),
fn (ObjectDescription $object): bool => ! (bool) preg_match('/^<\?php\s+declare\(.*?strict_types\s?=\s?1.*?\);/', (string) file_get_contents($object->path)),
'not to use strict types',
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
);
}
/**
* Asserts that the given expectation target does not use the strict equality operator.
*/
public function toUseStrictEquality(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' === '),
'to use strict equality',
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' === ')),
);
}
/**
* 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.
*
* @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(
$this->original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod($method),
'to not have method',
fn (ObjectDescription $object): bool => array_filter(
$methods,
fn (string $method): bool => $object->reflectionClass->hasMethod($method),
) === [],
'to not have methods: '.implode(', ', $methods),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target does not have the given methods.
*
* @param array<int, string> $methods
*/
public function toHaveMethods(array $methods): ArchExpectation
{
return $this->toHaveMethod($methods);
}
/**
* Asserts that the given expectation target not to have the public methods besides the given methods.
*
* @param array<int, string>|string $methods
*/
public function toHavePublicMethodsBesides(array|string $methods): ArchExpectation
{
$methods = is_array($methods) ? $methods : [$methods];
$state = new stdClass;
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PUBLIC)
: [];
foreach ($reflectionMethods as $reflectionMethod) {
if (! in_array($reflectionMethod->name, $methods, true)) {
$state->contains = 'public function '.$reflectionMethod->name;
return false;
}
}
return true;
},
$methods === []
? 'not to have public methods'
: sprintf("not to have public methods besides '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
);
}
/**
* Asserts that the given expectation target not to have the public methods.
*/
public function toHavePublicMethods(): ArchExpectation
{
return $this->toHavePublicMethodsBesides([]);
}
/**
* Asserts that the given expectation target not to have the protected methods besides the given methods.
*
* @param array<int, string>|string $methods
*/
public function toHaveProtectedMethodsBesides(array|string $methods): ArchExpectation
{
$methods = is_array($methods) ? $methods : [$methods];
$state = new stdClass;
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PROTECTED)
: [];
foreach ($reflectionMethods as $reflectionMethod) {
if (! in_array($reflectionMethod->name, $methods, true)) {
$state->contains = 'protected function '.$reflectionMethod->name;
return false;
}
}
return true;
},
$methods === []
? 'not to have protected methods'
: sprintf("not to have protected methods besides '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
);
}
/**
* Asserts that the given expectation target not to have the protected methods.
*/
public function toHaveProtectedMethods(): ArchExpectation
{
return $this->toHaveProtectedMethodsBesides([]);
}
/**
* Asserts that the given expectation target not to have the private methods besides the given methods.
*
* @param array<int, string>|string $methods
*/
public function toHavePrivateMethodsBesides(array|string $methods): ArchExpectation
{
$methods = is_array($methods) ? $methods : [$methods];
$state = new stdClass;
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($methods, &$state): bool {
$reflectionMethods = isset($object->reflectionClass)
? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PRIVATE)
: [];
foreach ($reflectionMethods as $reflectionMethod) {
if (! in_array($reflectionMethod->name, $methods, true)) {
$state->contains = 'private function '.$reflectionMethod->name;
return false;
}
}
return true;
},
$methods === []
? 'not to have private methods'
: sprintf("not to have private methods besides '%s'", implode("', '", $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, $state->contains)),
);
}
/**
* Asserts that the given expectation target not to have the private methods.
*/
public function toHavePrivateMethods(): ArchExpectation
{
return $this->toHavePrivateMethodsBesides([]);
}
/**
* Asserts that the given expectation target is not enum.
*/
@ -226,8 +449,6 @@ final class OppositeExpectation
/**
* Asserts that the given expectation target to be not subclass of the given class.
*
* @param class-string $class
*/
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.
*
* @param array<int, class-string>|string $interfaces
* @param array<int, string>|string $interfaces
*/
public function toImplement(array|string $interfaces): ArchExpectation
{
@ -292,10 +546,8 @@ final class OppositeExpectation
/**
* 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']);
}
@ -328,10 +580,8 @@ final class OppositeExpectation
/**
* Not supported.
*
* @param array<int, string>|string $targets
*/
public function toOnlyUse(array|string $targets): never
public function toOnlyUse(): void
{
throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']);
}
@ -339,7 +589,7 @@ final class OppositeExpectation
/**
* Not supported.
*/
public function toUseNothing(): never
public function toUseNothing(): void
{
throw InvalidExpectation::fromMethods(['not', 'toUseNothing']);
}
@ -364,7 +614,7 @@ final class OppositeExpectation
), is_string($targets) ? [$targets] : $targets));
}
public function toOnlyBeUsedIn(): never
public function toOnlyBeUsedIn(): void
{
throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedIn']);
}
@ -372,7 +622,7 @@ final class OppositeExpectation
/**
* Asserts that the given expectation dependency is not used.
*/
public function toBeUsedInNothing(): never
public function toBeUsedInNothing(): void
{
throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']);
}
@ -392,8 +642,6 @@ final class OppositeExpectation
/**
* Asserts that the given expectation target not to have the given attribute.
*
* @param class-string<Attribute> $attribute
*/
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,11 +6,12 @@ namespace Pest\Factories;
use ParseError;
use Pest\Concerns;
use Pest\Contracts\AddsAnnotations;
use Pest\Contracts\HasPrintableTestCaseName;
use Pest\Evaluators\Attributes;
use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Exceptions\TestAlreadyExist;
use Pest\Exceptions\TestClosureMustNotBeStatic;
use Pest\Exceptions\TestDescriptionMissing;
use Pest\Factories\Concerns\HigherOrderable;
use Pest\Support\Reflection;
@ -26,26 +27,12 @@ final class TestCaseFactory
{
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.
*
* @var array<int, class-string<\Pest\Factories\Attributes\Attribute>>
* @var array<int, Attribute>
*/
private const ATTRIBUTES = [
Attributes\Covers::class,
];
public array $attributes = [];
/**
* The FQN of the Test Case class.
@ -146,32 +133,21 @@ final class TestCaseFactory
$className = 'InvalidTestName'.Str::random();
}
$classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => $attribute::$above);
$methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => ! $attribute::$above);
$this->attributes = [
new Attribute(
\PHPUnit\Framework\Attributes\TestDox::class,
[$this->filename],
),
...$this->attributes,
];
$classAttributes = [];
foreach ($classAvailableAttributes as $attribute) {
$classAttributes = array_reduce(
$methods,
fn (array $carry, TestCaseMethodFactory $methodFactory): array => (new $attribute)->__invoke($methodFactory, $carry),
$classAttributes
);
}
$attributesCode = Attributes::code($this->attributes);
$methodsCode = implode('', array_map(
fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(
self::ANNOTATIONS,
$methodAvailableAttributes
),
fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(),
$methods
));
$classAttributesCode = implode('', array_map(
static fn (string $attribute): string => sprintf("\n%s", $attribute),
array_unique($classAttributes),
));
try {
$classCode = <<<PHP
namespace $namespace;
@ -179,10 +155,7 @@ final class TestCaseFactory
use Pest\Repositories\DatasetsRepository as __PestDatasets;
use Pest\TestSuite as __PestTestSuite;
/**
* @testdox $filename
*/
$classAttributesCode
$attributesCode
#[\AllowDynamicProperties]
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode
@ -216,6 +189,14 @@ final class TestCaseFactory
throw new TestAlreadyExist($method->filename, $method->description);
}
if (
$method->closure instanceof \Closure &&
(new \ReflectionFunction($method->closure))->isStatic()
) {
throw new TestClosureMustNotBeStatic($method);
}
if (! $method->receivesArguments()) {
if (! $method->closure instanceof \Closure) {
throw ShouldNotHappen::fromMessage('The test closure may not be empty.');

View File

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

View File

@ -3,9 +3,12 @@
declare(strict_types=1);
use Pest\Concerns\Expectable;
use Pest\Configuration;
use Pest\Exceptions\AfterAllWithinDescribe;
use Pest\Exceptions\BeforeAllWithinDescribe;
use Pest\Expectation;
use Pest\Mutate\Contracts\MutationTestRunner;
use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\AfterEachCall;
use Pest\PendingCalls\BeforeEachCall;
use Pest\PendingCalls\DescribeCall;
@ -13,6 +16,7 @@ use Pest\PendingCalls\TestCall;
use Pest\PendingCalls\UsesCall;
use Pest\Repositories\DatasetsRepository;
use Pest\Support\Backtrace;
use Pest\Support\Container;
use Pest\Support\DatasetInfo;
use Pest\Support\HigherOrderTapProxy;
use Pest\TestSuite;
@ -53,6 +57,8 @@ if (! function_exists('beforeEach')) {
/**
* 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
*/
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')) {
/**
* Adds the given closure as a test. The first argument
* is the test description; the second argument is
* a closure that contains the test expectations.
*
* @param-closure-this TestCase $closure
*
* @return Expectable|TestCall|TestCase|mixed
*/
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
* a closure that contains the test expectations.
*
* @param-closure-this TestCase $closure
*
* @return Expectable|TestCall|TestCase|mixed
*/
function it(string $description, ?Closure $closure = null): TestCall
@ -149,9 +169,7 @@ if (! function_exists('it')) {
if (! function_exists('todo')) {
/**
* Adds the given todo test. Internally, this test
* is marked as incomplete. Yet, Collision, Pest's
* printer, will display it as a "todo" test.
* Creates a new test that is marked as "todo".
*
* @return Expectable|TestCall|TestCase|mixed
*/
@ -169,6 +187,8 @@ if (! function_exists('afterEach')) {
/**
* Runs the given closure after each test in the current file.
*
* @param-closure-this TestCase $closure
*
* @return Expectable|HigherOrderTapProxy<Expectable|TestCall|TestCase>|TestCall|mixed
*/
function afterEach(?Closure $closure = null): AfterEachCall
@ -194,3 +214,67 @@ if (! function_exists('afterAll')) {
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
*/
final class Kernel
final readonly class Kernel
{
/**
* The Kernel bootstrappers.
@ -47,8 +47,8 @@ final class Kernel
* Creates a new Kernel instance.
*/
public function __construct(
private readonly Application $application,
private readonly OutputInterface $output,
private Application $application,
private OutputInterface $output,
) {
//
}

View File

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

View File

@ -9,6 +9,9 @@ namespace Pest\Logging\TeamCity;
*/
final class ServiceMessage
{
/**
* The flow ID.
*/
private static ?int $flowId = null;
/**
@ -35,7 +38,7 @@ final class ServiceMessage
{
return new self('testSuiteStarted', [
'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
*/
abstract class Subscriber
abstract class Subscriber // @pest-arch-ignore-line
{
/**
* Creates a new Subscriber instance.
@ -19,7 +19,7 @@ abstract class Subscriber
/**
* Creates a new TeamCityLogger instance.
*/
final protected function logger(): TeamCityLogger
final protected function logger(): TeamCityLogger // @pest-arch-ignore-line
{
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\TestFailedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestFinishedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestMarkedIncompleteSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestPreparedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestSkippedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestSuiteFinishedSubscriber;
@ -28,7 +27,6 @@ use PHPUnit\Event\Test\ConsideredRisky;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestRunner\ExecutionFinished;
@ -45,8 +43,14 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
final class TeamCityLogger
{
/**
* The current time.
*/
private ?HRTime $time = null;
/**
* Indicates if the summary test count has been printed.
*/
private bool $isSummaryTestCountPrinted = false;
/**
@ -108,7 +112,7 @@ final class TeamCityLogger
$this->time = $event->telemetryInfo()->time();
}
public function testMarkedIncomplete(MarkedIncomplete $event): never
public function testMarkedIncomplete(): never
{
throw ShouldNotHappen::fromMessage('testMarkedIncomplete not implemented.');
}
@ -262,7 +266,6 @@ final class TeamCityLogger
new TestFinishedSubscriber($this),
new TestErroredSubscriber($this),
new TestFailedSubscriber($this),
new TestMarkedIncompleteSubscriber($this),
new TestSkippedSubscriber($this),
new TestConsideredRiskySubscriber($this),
new TestExecutionFinishedSubscriber($this),

View File

@ -344,36 +344,6 @@ final class Expectation
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.
*

View File

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

View File

@ -54,7 +54,7 @@ final class AfterEachCall
$proxies = $this->proxies;
$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
)->bindTo($this, self::class);
@ -65,7 +65,6 @@ final class AfterEachCall
$this,
$afterEachTestCase,
);
}
/**

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Pest\PendingCalls;
use Closure;
use Pest\Exceptions\AfterBeforeTestFunction;
use Pest\PendingCalls\Concerns\Describable;
use Pest\Support\Backtrace;
use Pest\Support\ChainableClosure;
@ -14,6 +15,8 @@ use Pest\TestSuite;
/**
* @internal
*
* @mixin TestCall
*/
final class BeforeEachCall
{
@ -59,17 +62,22 @@ final class BeforeEachCall
$testCaseProxies = $this->testCaseProxies;
$beforeEachTestCall = function (TestCall $testCall) use ($describing): void {
if ($describing !== $this->describing) {
return;
}
if ($describing !== $testCall->describing) {
return;
if ($this->describing !== null) {
if ($describing !== $this->describing) {
return;
}
if ($describing !== $testCall->describing) {
return;
}
}
$this->testCallProxies->chain($testCall);
};
$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
)->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.
*
@ -91,7 +111,8 @@ final class BeforeEachCall
public function __call(string $name, array $arguments): self
{
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;
}

View File

@ -9,5 +9,13 @@ namespace Pest\PendingCalls\Concerns;
*/
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;
}

View File

@ -18,6 +18,11 @@ final class DescribeCall
*/
private static ?string $describing = null;
/**
* The describe "before each" call.
*/
private ?BeforeEachCall $currentBeforeEachCall = null;
/**
* Creates a new Pending Call.
*/
@ -43,6 +48,8 @@ final class DescribeCall
*/
public function __destruct()
{
unset($this->currentBeforeEachCall);
self::$describing = $this->description;
try {
@ -57,14 +64,18 @@ final class DescribeCall
*
* @param array<int, mixed> $arguments
*/
public function __call(string $name, array $arguments): BeforeEachCall
public function __call(string $name, array $arguments): self
{
$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

@ -6,13 +6,14 @@ namespace Pest\PendingCalls;
use Closure;
use Pest\Exceptions\InvalidArgumentException;
use Pest\Factories\Covers\CoversClass;
use Pest\Factories\Covers\CoversFunction;
use Pest\Factories\Covers\CoversNothing;
use Pest\Exceptions\TestDescriptionMissing;
use Pest\Factories\Attribute;
use Pest\Factories\TestCaseMethodFactory;
use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\Concerns\Describable;
use Pest\Plugins\Only;
use Pest\Support\Backtrace;
use Pest\Support\Container;
use Pest\Support\Exporter;
use Pest\Support\HigherOrderCallables;
use Pest\Support\NullClosure;
@ -30,6 +31,13 @@ final class TestCall
{
use Describable;
/**
* The list of test case factory attributes.
*
* @var array<int, Attribute>
*/
private array $testCaseFactoryAttributes = [];
/**
* The Test Case Factory.
*/
@ -46,10 +54,10 @@ final class TestCall
public function __construct(
private readonly TestSuite $testSuite,
private readonly string $filename,
?string $description = null,
private ?string $description = null,
?Closure $closure = null
) {
$this->testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure);
$this->testCaseMethod = new TestCaseMethodFactory($filename, $closure);
$this->descriptionLess = $description === null;
@ -58,6 +66,42 @@ final class TestCall
$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.
*/
@ -165,7 +209,10 @@ final class TestCall
public function group(string ...$groups): self
{
foreach ($groups as $group) {
$this->testCaseMethod->groups[] = $group;
$this->testCaseMethod->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\Group::class,
[$group],
);
}
return $this;
@ -176,7 +223,7 @@ final class TestCall
*/
public function only(): self
{
Only::enable($this);
Only::enable($this, ...func_get_args()); // @phpstan-ignore-line
return $this;
}
@ -306,32 +353,186 @@ final class TestCall
}
/**
* Sets the test as "todo".
* Marks the test as "todo".
*/
public function todo(): self
{
public function todo(// @phpstan-ignore-line
array|string|null $note = null,
array|string|null $assignee = null,
array|string|int|null $issue = null,
array|string|int|null $pr = null,
): self {
$this->skip('__TODO__');
$this->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;
}
/**
* 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) {
$isClass = class_exists($classOrFunction) || trait_exists($classOrFunction);
$isMethod = function_exists($classOrFunction);
/** @var array<int, string> $classesOrFunctions */
$classesOrFunctions = array_reduce($classesOrFunctions, fn ($carry, $item): array => is_array($item) ? array_merge($carry, $item) : array_merge($carry, [$item]), []); // @pest-ignore-type
if (! $isClass && ! $isMethod) {
throw new InvalidArgumentException(sprintf('No class or method named "%s" has been found.', $classOrFunction));
foreach ($classesOrFunctions as $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) {
$this->coversClass($classOrFunction);
} elseif ($isTrait) {
$this->coversTrait($classOrFunction);
} else {
$this->coversFunction($classOrFunction);
}
@ -346,7 +547,41 @@ final class TestCall
public function coversClass(string ...$classes): self
{
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;
@ -358,7 +593,10 @@ final class TestCall
public function coversFunction(string ...$functions): self
{
foreach ($functions as $function) {
$this->testCaseMethod->covers[] = new CoversFunction($function);
$this->testCaseFactoryAttributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversFunction::class,
[$function],
);
}
return $this;
@ -369,7 +607,10 @@ final class TestCall
*/
public function coversNothing(): self
{
$this->testCaseMethod->covers = [new CoversNothing];
$this->testCaseMethod->attributes[] = new Attribute(
\PHPUnit\Framework\Attributes\CoversNothing::class,
[],
);
return $this;
}
@ -420,10 +661,11 @@ final class TestCall
if ($this->descriptionLess) {
Exporter::default();
if ($this->testCaseMethod->description !== null) {
$this->testCaseMethod->description .= ' → ';
if ($this->description !== null) {
$this->description .= ' → ';
}
$this->testCaseMethod->description .= $arguments === null
$this->description .= $arguments === null
? $name
: sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
}
@ -436,11 +678,21 @@ final class TestCall
*/
public function __destruct()
{
if ($this->description === null) {
throw new TestDescriptionMissing($this->filename);
}
if (! is_null($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);
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(
private readonly string $filename,
private readonly array $classAndTraits
private array $classAndTraits
) {
$this->targets = [$filename];
}
/**
* @deprecated Use `pest()->printer()->compact()` instead.
*/
public function compact(): self
{
DefaultPrinter::compact(true);
@ -61,10 +64,29 @@ final class UsesCall
}
/**
* The directories or file where the
* class or traits should be used.
* Specifies the class or traits to use.
*
* @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 {
$startChar = DIRECTORY_SEPARATOR;
@ -78,7 +100,7 @@ final class UsesCall
return str_starts_with($path, $startChar)
? $path
: implode(DIRECTORY_SEPARATOR, [
dirname($this->filename),
is_dir($this->filename) ? $this->filename : dirname($this->filename),
$path,
]);
}, $targets);
@ -92,6 +114,8 @@ final class UsesCall
return $accumulator;
}, []);
return $this;
}
/**

View File

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

View File

@ -21,7 +21,7 @@ trait HandleArguments
return true;
}
if (str_starts_with($arg, "$argument=")) {
if (str_starts_with((string) $arg, "$argument=")) { // @phpstan-ignore-line
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
{
if (Parallel::isWorker()) {
return $exitCode;
}
if ($exitCode === 0 && $this->coverage) {
if (! \Pest\Support\Coverage::isAvailable()) {
$this->output->writeln(
@ -130,7 +134,7 @@ final class Coverage implements AddsOutput, HandlesArguments
$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 %%</>.",
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
*/
final class Help implements HandlesArguments
final readonly class Help implements HandlesArguments
{
use Concerns\HandleArguments;
@ -22,7 +22,7 @@ final class Help implements HandlesArguments
* Creates a new Plugin instance.
*/
public function __construct(
private readonly OutputInterface $output
private OutputInterface $output
) {
// ..
}
@ -123,6 +123,21 @@ final class Help implements HandlesArguments
], [
'arg' => '--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',
'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',
], ...$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'] = [
[
'arg' => '--profile ',

View File

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

View File

@ -28,6 +28,10 @@ final class Only implements Terminable
*/
public function terminate(): void
{
if (Parallel::isWorker()) {
return;
}
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (file_exists($lockFile)) {
@ -38,18 +42,26 @@ final class Only implements Terminable
/**
* 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;
}
$testCall->group('__pest_only');
$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)) {
touch($lockFile);
file_put_contents($lockFile, $group);
}
}
@ -62,4 +74,18 @@ final class Only implements Terminable
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[]
*/
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.
@ -42,6 +42,7 @@ final class Parallel implements HandlesArguments
public static function isEnabled(): bool
{
$argv = new ArgvInput;
if ($argv->hasParameterOption('--parallel')) {
return true;
}

View File

@ -26,7 +26,7 @@ final class Laravel implements HandlesArguments
*/
public function handleArguments(array $arguments): array
{
return self::whenUsingLaravel($arguments, function (array $arguments): array {
return $this->whenUsingLaravel($arguments, function (array $arguments): array {
$this->ensureRunnerIsResolvable();
$arguments = $this->ensureEnvironmentVariables($arguments);
@ -42,7 +42,7 @@ final class Laravel implements HandlesArguments
* @param CLosure(array<int, string>): array<int, string> $closure
* @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);
$isLaravelPackage = class_exists(\Orchestra\Testbench\TestCase::class);

View File

@ -11,7 +11,7 @@ final class CleanConsoleOutput extends ConsoleOutput
/**
* {@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)) {
return;

View File

@ -46,15 +46,27 @@ use function usleep;
*/
final class WrapperRunner implements RunnerInterface
{
/**
* The time to sleep between cycles.
*/
private const CYCLE_SLEEP = 10000;
/**
* The result printer.
*/
private readonly ResultPrinter $printer;
/**
* The timer.
*/
private readonly Timer $timer;
/** @var list<non-empty-string> */
private array $pending = [];
/**
* The exit code.
*/
private int $exitcode = -1;
/** @var array<positive-int,WrapperWorker> */
@ -84,6 +96,9 @@ final class WrapperRunner implements RunnerInterface
/** @var non-empty-string[] */
private readonly array $parameters;
/**
* The code coverage filter registry.
*/
private CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
public function __construct(
@ -107,6 +122,9 @@ final class WrapperRunner implements RunnerInterface
$parameters = array_merge($parameters, $options->passthruPhp);
}
/** @var array<int, non-empty-string> $parameters */
$parameters = $this->handleLaravelHerd($parameters);
$parameters[] = $wrapper;
$this->parameters = $parameters;
@ -138,6 +156,21 @@ final class WrapperRunner implements RunnerInterface
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
{
for ($token = 1; $token <= $this->options->processes; $token++) {
@ -390,6 +423,7 @@ final class WrapperRunner implements RunnerInterface
}
$testSuite = (new LogMerger)->merge($this->junitFiles);
assert($testSuite instanceof \ParaTest\JUnit\TestSuite);
(new Writer)->write(
$testSuite,
$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\Exceptions\TestCaseAlreadyInUse;
use Pest\Exceptions\TestCaseClassOrTraitNotFound;
use Pest\Factories\Attribute;
use Pest\Factories\TestCaseFactory;
use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Str;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
/**
@ -114,9 +116,9 @@ final class TestRepository
/**
* 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 ($groups as $group) {
$method->groups[] = $group;
$method->attributes[] = new Attribute(
Group::class,
[$group],
);
}
}

View File

@ -40,9 +40,20 @@ final class Result
*/
public static function exitCode(Configuration $configuration, TestResult $result): int
{
if ($result->wasSuccessfulIgnoringPhpunitWarnings()
&& ! $result->hasTestTriggeredPhpunitWarningEvents()) {
return self::SUCCESS_EXIT;
if ($result->wasSuccessfulIgnoringPhpunitWarnings()) {
if ($configuration->failOnWarning()) {
$warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents()
+ count($result->warnings())
+ count($result->phpWarnings());
if ($warnings > 0) {
return self::FAILURE_EXIT;
}
}
if (! $result->hasTestTriggeredPhpunitWarningEvents()) {
return self::SUCCESS_EXIT;
}
}
if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) {
@ -54,14 +65,6 @@ final class Result
$returnCode = self::FAILURE_EXIT;
}
$warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents()
+ count($result->warnings())
+ count($result->phpWarnings());
if ($configuration->failOnWarning() && $warnings > 0) {
$returnCode = self::FAILURE_EXIT;
}
if ($configuration->failOnIncomplete() && $result->hasTestMarkedIncompleteEvents()) {
$returnCode = self::FAILURE_EXIT;
}

View File

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

View File

@ -17,13 +17,13 @@ final class ChainableClosure
*/
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
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
}
if (\Pest\Support\Closure::bind($condition, $this, self::class)(...func_get_args())) {
\Pest\Support\Closure::bind($next, $this, self::class)(...func_get_args());
if (\Pest\Support\Closure::bind($condition, $this, self::class)(...$arguments)) {
\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
{
return function () use ($closure, $next): void {
return function (...$arguments) use ($closure, $next): void {
if (! is_object($this)) { // @phpstan-ignore-line
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
}
\Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args());
\Pest\Support\Closure::bind($next, $this, self::class)(...func_get_args());
\Pest\Support\Closure::bind($closure, $this, self::class)(...$arguments);
\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
{
return function () use ($closure, $next): void {
$closure(...func_get_args());
$next(...func_get_args());
return function (...$arguments) use ($closure, $next): void {
$closure(...$arguments);
$next(...$arguments);
};
}
@ -59,9 +59,9 @@ final class ChainableClosure
*/
public static function boundStatically(Closure $closure, Closure $next): Closure
{
return static function () use ($closure, $next): void {
\Pest\Support\Closure::bind($closure, null, self::class)(...func_get_args());
\Pest\Support\Closure::bind($next, null, self::class)(...func_get_args());
return static function (...$arguments) use ($closure, $next): void {
\Pest\Support\Closure::bind($closure, null, self::class)(...$arguments);
\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
{
if ($closure == null) {
if (! $closure instanceof \Closure) {
throw ShouldNotHappen::fromMessage('Could not bind null closure.');
}
$closure = BaseClosure::bind($closure, $newThis, $newScope);
if ($closure == false) {
if ($closure === null) {
throw ShouldNotHappen::fromMessage('Could not bind closure.');
}

View File

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

View File

@ -138,7 +138,7 @@ final class Coverage
$totalCoverageAsString = $totalCoverage->asFloat() === 0.0
? '0.0'
: number_format($totalCoverage->asFloat(), 1, '.', '');
: number_format(floor($totalCoverage->asFloat() * 10) / 10, 1, '.', '');
renderUsing($output);
render(<<<HTML
@ -197,7 +197,7 @@ final class Coverage
};
$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);
}

View File

@ -31,7 +31,7 @@ final class ExceptionTrace
$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
$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);

View File

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

View File

@ -10,12 +10,12 @@ use Pest\Expectation;
/**
* @internal
*/
final class HigherOrderCallables
final readonly class HigherOrderCallables
{
/**
* 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);
}
/**
* 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.
*/

View File

@ -11,6 +11,7 @@ use Pest\TestSuite;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionProperty;
@ -213,4 +214,74 @@ final class Reflection
{
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
{
/**
* @var string[]|null
* @var array<int, string>|null
*/
private ?array $changedFiles = null;
/**
* Creates a new instance of the filter.
*/
public function __construct(private readonly string $projectRoot)
{
// ...
}
/**
* {@inheritdoc}
*/
public function accept(string $testCaseFilename): bool
{
if ($this->changedFiles === null) {
@ -41,6 +47,9 @@ final class GitDirtyTestCaseFilter implements TestCaseFilter
return in_array($relativePath, $this->changedFiles, true);
}
/**
* Loads the changed files.
*/
private function loadChangedFiles(): void
{
$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\Factories\TestCaseMethodFactory;
final class TodoTestCaseFilter implements TestCaseMethodFilter
final readonly class TodoTestCaseFilter implements TestCaseMethodFilter
{
/**
* Filter the test case methods.
*/
public function accept(TestCaseMethodFactory $factory): bool
{
return $factory->todo;

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