Compare commits

...

394 Commits

Author SHA1 Message Date
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
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
d0ff2c8ec2 release: 2.35.0 2024-08-02 11:57:29 +01:00
5e41e546a0 chore: style changes 2024-08-02 11:53:24 +01:00
45cce6ce93 Style 2024-07-24 21:54:52 +01:00
6a8a4f3243 Merge pull request #1194 from dmason30/patch-1
Include method name in toHaveMethod error message
2024-07-20 18:29:37 +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
ef29b4f091 Include method name in toHaveMethod error message 2024-07-19 15:30:43 +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
ef120125e0 release: 2.34.9 2024-07-11 09:36:26 +01:00
8a9a416133 chore: bumps dependencies 2024-07-11 09:35:43 +01:00
4783334f15 chore: style 2024-07-11 09:35:38 +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
e8f122bf47 release: 2.34.8 2024-06-10 23:02:16 +01:00
9fc607a2b8 Fixes wrong stub 2024-06-10 22:52:21 +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
b33af71036 Merge pull request #1157 from ExeQue/2.x
[2.x] Added `toBeList` expectation
2024-05-27 13:08:05 +01:00
3c6c89a6ad Added test to toBeList 2024-05-21 08:15:32 +02:00
55f6b5696e Added toBeList expectation 2024-05-21 08:13:20 +02: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
303f4c0113 Adds sponsor 2024-04-19 20:47:46 +01:00
35a1fcd0cf chore: updates readme 2024-04-08 12:28:43 +01:00
adb2fb51df Always call parent teardown even if an exception is thrown 2024-04-08 10:03:55 +01:00
a7a3e4240e release: v2.34.7 2024-04-05 08:44:17 +01:00
e4af33867b chore: bumps dependencies 2024-04-05 08:44:07 +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
680111fb1e release: v2.34.6 2024-03-28 11:36:46 +00:00
aa6ff95ea4 chore: bumps dependencies 2024-03-28 11:36:36 +00:00
863a0cc837 release: v2.34.5 2024-03-22 08:44:19 +00:00
126a84a63e chore: bumps dependencies 2024-03-22 08:44:13 +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
d519e40b95 chore: adjusts workflow 2024-03-15 21:14:22 +00:00
6a1161ead8 release: v2.34.4 2024-03-14 19:44:18 +00:00
a1b3547dd6 chore: fixes paratest version 2024-03-14 19:42:03 +00:00
b9e3146a47 release: v2.34.3 2024-03-14 19:40:23 +00:00
ce1607cba9 chore: bumps phpunit 2024-03-14 19:40:16 +00:00
ac07bc1770 chore: override changes 2024-03-14 19:40:03 +00:00
521a41dd10 fix: no duplicate --no-output 2024-03-14 19:39:45 +00:00
1b68b340e8 chore: fixes static analysis 2024-03-14 19:39:31 +00:00
853f6efce6 release: v2.34.2 2024-03-11 18:05:47 +00:00
62a9a78ee2 chore: bumps dependencies 2024-03-11 18:05:37 +00:00
78d9fd31d0 release: v2.34.1 2024-02-28 15:15:55 +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
602b696348 release: v2.34.0 2024-02-17 10:06:53 +00:00
5b0f88c227 release: v2.33.6 2024-02-12 08:55:32 +00:00
f31a2c3220 chore: fixes paratest 2024-02-12 08:53:04 +00:00
e8fa98c810 release: 2.33.5 2024-02-12 08:44:52 +00:00
07e314fbf5 chore: bumps dependencies 2024-02-12 08:44:40 +00:00
4baf27911e release: 2.33.4 2024-02-02 16:54:54 +00:00
12e48a14d1 chore: fixes deps 2024-02-02 16:53:58 +00:00
1bc0f79508 release: 2.33.3 2024-02-02 16:51:42 +00:00
cb0f256791 release: 2.33.2 2024-02-02 16:50:40 +00:00
7b9bae0415 fix: usage of named arguments 2024-02-01 13:45:06 +00:00
3dffdf7cb8 Merge branch '2.x' into 3.x 2024-02-01 11:53:10 +00:00
923970a117 chore: bumps versioning 2024-02-01 11:51:11 +00:00
b3db7dfd4c chore: fixes type checking 2024-02-01 11:45:19 +00:00
b303f9f818 Merge pull request #1082 from nhaynes/fix-ci-flag
fix: updates Only plugin to check for CI environment
2024-01-30 14:17:03 +00:00
d29997d5b0 fix: updates Only plugin to check for CI environment 2024-01-29 19:03:06 -06:00
13f340a742 feat: improves badge coloring 2024-01-29 23:00:30 +00:00
eeade88ad2 Fixes kernel throwing all kind of errors 2024-01-29 12:50:00 +00:00
06280ef75d chore: updates snaphosts 2024-01-29 11:54:23 +00:00
aa46f73888 Merge pull request #1081 from nuernbergerA/track-vendor-changes
[2.x] Track vendor changes
2024-01-29 11:39:34 +00:00
3660865e5e update snapshot 2024-01-29 08:58:07 +01:00
13695d597b Merge branch '2.x' into track-vendor-changes 2024-01-29 08:56:41 +01:00
fab2de833f Merge pull request #1080 from nuernbergerA/test-junit
[2.x] Add test for junit implementation
2024-01-28 23:49:06 +00:00
5b630bcdff possible implementation 2024-01-28 09:48:37 +01:00
e70edbfa38 normalize path for windows 2024-01-28 09:11:06 +01:00
b1558ddde5 update snapshot 2024-01-28 09:04:42 +01:00
582529377b add test for junit output 2024-01-28 08:53:20 +01:00
88714598b6 Merge pull request #1076 from pestphp/fixing-version
[2.x] Fixing Version `2.33` for New Release
2024-01-27 13:01:06 +00:00
AJ
5136267bbe fixing version for new release 2024-01-26 23:43:52 -03:00
19e748f0d4 chore: adjusts snapshots 2024-01-26 01:58:03 +00:00
a53a9d03cf fix: exiting 2024-01-26 00:12:36 +00:00
edaa045283 feat: exists after kernel shutdown 2024-01-26 00:04:52 +00:00
c5ce355f3c feat: improves fatal exception handling 2024-01-25 21:47:31 +00:00
62d8459627 Merge pull request #1075 from luismgsantos/fix/docker-build-image
fix: build failing to run
2024-01-25 17:56:50 +00:00
a5bf6a3fcb fix: --cache-directory being used on phpunit file 2024-01-25 17:56:24 +00:00
7a46514df8 fix: removes process-isolation from --help output 2024-01-25 17:32:02 +00:00
cb1735f4d8 fix: removes process-isolation from --help output 2024-01-25 17:27:03 +00:00
607a4906ac Merge pull request #1006 from JonPurvis/to-be-backed-enum-expectation
[2.x] Add `toBeStringBackedEnum()` and `toBeIntBackedEnum()` Architecture Expectations
2024-01-25 17:09:23 +00:00
317ea0356e fix: build failing to run 2024-01-25 18:06:35 +01:00
1153531104 Merge pull request #1055 from mapon-com/feature/string-comparison-expectations
[2.x] Allow string type in greaterThan/lessThan expectations
2024-01-25 16:48:27 +00:00
cfb724cd77 Merge pull request #1060 from calebdw/bugfix-code_coverage
[2.x] fix: warn if no code coverage driver
2024-01-25 16:42:03 +00:00
0060b6f955 Merge pull request #1069 from davybaccaert/improve_coverage_message_on_failing_minimum_requirements
[2.x] Improve coverage output message on failing minimum requirements
2024-01-25 16:39:30 +00:00
95cd550524 fix: pipes not allowing to modify original value 2024-01-25 16:10:16 +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
887bed3d45 fix: adjusts backtrace for pest's internal test suite 2024-01-25 15:00:04 +00:00
79da02c500 Merge pull request #972 from Carnicero90/bugfix-backtrace-naming-conflicts
[2.x] Fixing Backtrace not found error if project dirname endswith pest
2024-01-25 14:54:58 +00:00
0aecd5d5d7 Merge pull request #974 from erikgaal/expect-to-contain-equals
[2.x] Add `toContainEquals` expectation
2024-01-25 14:38:55 +00:00
e95c4ee636 feat(toContainEqual): adds method name 2024-01-25 14:38:44 +00:00
2e7fec6be5 Merge pull request #961 from bastien-phi/allow_multiple_hook_per_directory
[2.x] Allow define multiple hooks per directory
2024-01-25 14:31:29 +00:00
4be7082de5 chore: updates snapshots 2024-01-25 14:31:17 +00:00
fb90f778b9 Update snapshots 2024-01-25 14:28:37 +00:00
9d58e1a77e Add ability to define multiple hooks for the same directory in Pest.php 2024-01-25 14:23:41 +00:00
9c077ed352 refacto: moves function to being used on internal test suite only 2024-01-25 14:13:18 +00:00
2562d36518 feat: clarfies that high order testing does not support bound datasets 2024-01-25 14:12:01 +00:00
1d2fe2de2d fix: doNotThrowsExceptions being marked as incomplete 2024-01-25 14:12:01 +00:00
2d82ee2837 chore: fixes types 2024-01-25 14:12:01 +00:00
1eee9df679 Merge pull request #981 from salehhashemi1992/refactor/remove-ansi-sequences
[2.x] Refactor: Extract ANSI Escape Sequence Removal to a Function
2024-01-25 14:11:51 +00:00
8c57cc1731 fix: --watch plugin access to original arguments 2024-01-25 12:33:20 +00:00
4febd8a11b Merge pull request #1073 from nuernbergerA/fix-junit-parallel
Fix junit parallel
2024-01-25 10:17:36 +00:00
880b003bee apply cs 2024-01-24 21:50:52 +01:00
e0f9d0bccf just override the phpunit file 2024-01-24 21:33:40 +01:00
d4853feecd drop own implementation 2024-01-24 21:33:17 +01:00
86e812284d remove plugin to ensure argument reaches paratest 2024-01-24 21:32:49 +01:00
4e31973040 Merge branch '2.x' into 3.x 2024-01-23 18:15:06 +00:00
f75063c420 release: 2.32.2 2024-01-23 18:12:07 +00:00
6a48e9d44b Merge branch '2.x' into 3.x 2024-01-23 18:06:23 +00:00
1f8e6e4e9f fix: helper access 2024-01-23 17:40:37 +00:00
bb593846e5 release: 2.32.1 2024-01-23 17:04:48 +00:00
108d181a05 Improve coverage output message on failing minimum requirements 2024-01-20 15:29:35 +01:00
ac5d6c1f67 chore: fixes constrains no workflow 2024-01-20 13:48:00 +00:00
5aa3b91d56 chore: fixes windows builds 2024-01-20 13:36:31 +00:00
9a01504b76 chore: fixes workflow 2024-01-20 13:32:21 +00:00
0ab636e436 chore: fixes workflow 2024-01-20 13:28:43 +00:00
b9d2be87a2 fix: missing things on junit 2024-01-20 13:21:57 +00:00
fef02594db release: 2.32.0 2024-01-20 11:44:11 +00:00
e135e2671f style 2024-01-20 11:44:11 +00:00
6d74965727 chore: bump dependencies 2024-01-20 11:44:11 +00:00
146e141b2a Merge pull request #887 from nuernbergerA/fix-junit-output
[2.x] Junit support
2024-01-20 11:43:20 +00:00
6fed7545c0 Merge pull request #990 from rudashi/patch-1
[2.x] Fix typo in `toHaveProperties` PHPDoc block
2024-01-13 01:44:16 +00:00
be407ac904 fix: warn if no code coverage driver 2024-01-11 10:20:35 -06: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
5332858782 chore: fixes snapshots 2024-01-11 15:46:50 +00:00
2b094b4188 Merge branch '2.x' into 3.x 2024-01-11 15:37:31 +00:00
3457841a9b release: v2.31.0 2024-01-11 15:33:20 +00:00
5258e569c1 feat: adds skipOnPHP 2024-01-11 15:33:12 +00:00
abb416c2ff chore: bumps dependencies 2024-01-11 15:32:44 +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
b1c59ec2e6 feat: allow string type in gt/lt expectations 2024-01-05 16:21:02 +02: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
dc1e4f040d docs: adds sponsor 2024-01-04 18:26:20 +00:00
5e1e701ce5 Merge pull request #1051 from krencl/fix-cache-directory-config-override
Fix cache directory config override
2024-01-02 14:33:48 +00:00
f004591c5a fix: checking existing argument with equal sign 2024-01-02 15:03:46 +01:00
86a96dd157 fix: overriding cli argument --cache-directory 2024-01-02 15:01:13 +01:00
14fb992ef2 unify converter 2023-12-19 06:29:28 +01:00
4550a344d3 overwrite phpunit junit logging with noop 2023-12-19 06:29:28 +01:00
8efd25ef65 remove debug output 2023-12-19 06:29:28 +01:00
117694f210 cleanup 2023-12-19 06:29:28 +01:00
e5dc6f0ae2 junit support 2023-12-19 06:29:28 +01:00
8be46b57a0 Update toHaveProperties() $names param 2023-11-24 09:16:13 +01:00
2e01776272 add to be backed enum expectation 2023-11-18 03:31:35 +00:00
15cd7187e9 Update toContainEquals.php 2023-11-06 10:31:48 +01:00
2cbecd10e6 Fix typo in toHaveProperties() PHPDoc block 2023-10-23 11:23:53 +02:00
86c107ae5e Extract ANSI escape sequence to a function 2023-10-13 20:16:46 +03:30
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
79f5973e5a Add tests 2023-10-03 11:09:26 +02:00
37c40cb735 Add toContainEquals expectation 2023-10-03 10:55:57 +02:00
28ee2917f1 Fixing Backtrace not found error if project dirname endswith pest 2023-09-30 00:05:42 +02:00
260 changed files with 5752 additions and 1342 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

@ -3,8 +3,6 @@ name: Tests
on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'
jobs:
tests:
@ -15,12 +13,9 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
symfony: ['6.4.0', '7.0.1']
php: ['8.1', '8.2', '8.3']
symfony: ['7.0']
php: ['8.2', '8.3']
dependency_version: [prefer-lowest, prefer-stable]
exclude:
- php: '8.1'
symfony: '7.0.1'
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
@ -41,7 +36,7 @@ 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 }}"
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:~${{ matrix.symfony }}"
- name: Unit Tests
run: composer test:unit

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)**
@ -21,17 +24,20 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
### Platinum Sponsors
- **[LaraJobs](https://larajobs.com)**
- **[Brokerchooser](https://brokerchooser.com)**
- **[Forge](https://forge.laravel.com)**
- **[Spatie](https://spatie.be)**
- **[Worksome](https://www.worksome.com/)**
### Premium Sponsors
- [Akaunting](https://akaunting.com)
- [Codecourse](https://codecourse.com)
- [Laracasts](https://laracasts.com)
- [Laradir](https://laradir.com)
- [Localazy](https://localazy.com)
- [Zapiet](https://www.zapiet.com)
- [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"`

116
bin/pest
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;
@ -13,43 +17,107 @@ use Symfony\Component\Console\Output\ConsoleOutput;
// Ensures Collision's Printer is registered.
$_SERVER['COLLISION_PRINTER'] = 'DefaultPrinter';
$args = $_SERVER['argv'];
$arguments = $originalArguments = $_SERVER['argv'];
$dirty = false;
$todo = false;
$notes = false;
foreach ($arguments as $key => $value) {
foreach ($args as $key => $value) {
if ($value === '--compact') {
$_SERVER['COLLISION_PRINTER_COMPACT'] = 'true';
unset($args[$key]);
unset($arguments[$key]);
}
if ($value === '--profile') {
$_SERVER['COLLISION_PRINTER_PROFILE'] = 'true';
unset($args[$key]);
unset($arguments[$key]);
}
if (str_contains($value, '--test-directory')) {
unset($args[$key]);
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') {
$dirty = true;
unset($args[$key]);
unset($arguments[$key]);
}
if (in_array($value, ['--todo', '--todos'], true)) {
$todo = true;
unset($args[$key]);
unset($arguments[$key]);
}
if ($value === '--notes') {
$notes = true;
unset($arguments[$key]);
}
if (str_contains($value, '--assignee=')) {
unset($arguments[$key]);
} else if ($value === '--assignee') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--issue=')) {
unset($arguments[$key]);
} else if ($value === '--issue') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--ticket=')) {
unset($arguments[$key]);
} else if ($value === '--ticket') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--pr=')) {
unset($arguments[$key]);
} else if ($value === '--pr') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--pull-request=')) {
unset($arguments[$key]);
} else if ($value === '--pull-request') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
unset($arguments[$key + 1]);
}
}
if (str_contains($value, '--teamcity')) {
unset($args[$key]);
$args[] = '--no-output';
unset($arguments[$key]);
$arguments[] = '--no-output';
unset($_SERVER['COLLISION_PRINTER']);
}
}
// 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);
@ -88,9 +180,9 @@ use Symfony\Component\Console\Output\ConsoleOutput;
try {
$kernel = Kernel::boot($testSuite, $input, $output);
$result = $kernel->handle($args);
$result = $kernel->handle($originalArguments, $arguments);
$kernel->shutdown();
$kernel->terminate();
} catch (Throwable|Error $e) {
Panic::with($e);
}

View File

@ -12,7 +12,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
$bootPest = (static function (): void {
$workerArgv = new ArgvInput();
$workerArgv = new ArgvInput;
$rootPath = dirname(PHPUNIT_COMPOSER_INSTALL, 2);
$testSuite = TestSuite::getInstance($rootPath, $workerArgv->getParameterOption(
@ -20,7 +20,7 @@ $bootPest = (static function (): void {
'tests'
));
$input = new ArgvInput();
$input = new ArgvInput;
$output = new ConsoleOutput(OutputInterface::VERBOSITY_NORMAL, true);
@ -81,6 +81,7 @@ $bootPest = (static function (): void {
$getopt['teamcity-file'] ?? null,
$getopt['testdox-file'] ?? null,
isset($getopt['testdox-color']),
$getopt['testdox-columns'] ?? null,
);
while (true) {

View File

@ -17,17 +17,18 @@
}
],
"require": {
"php": "^8.1.0",
"brianium/paratest": "^7.3.1",
"nunomaduro/collision": "^7.10.0|^8.0.1",
"nunomaduro/termwind": "^1.15.1|^2.0.0",
"pestphp/pest-plugin": "^2.1.1",
"pestphp/pest-plugin-arch": "^2.5.0",
"phpunit/phpunit": "^10.5.5"
"php": "^8.2.0",
"brianium/paratest": "^7.5.4",
"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.3",
"phpunit/phpunit": "^11.3.6"
},
"conflict": {
"phpunit/phpunit": ">10.5.5",
"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.6.0",
"symfony/process": "^6.4.0|^7.0.0"
"pestphp/pest-dev-tools": "^3.0.0",
"pestphp/pest-plugin-type-coverage": "^3.0.0",
"symfony/process": "^7.1.3"
},
"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

@ -2,13 +2,11 @@ ARG PHP=8.1
FROM php:${PHP}-cli-alpine
RUN apk update && apk add \
zip libzip-dev icu-dev git \
zip libzip-dev icu-dev git
RUN docker-php-ext-configure zip intl
RUN docker-php-ext-install zip intl
RUN docker-php-ext-enable zip intl
RUN apk add --no-cache linux-headers
RUN apk add --no-cache linux-headers autoconf build-base
RUN pecl install xdebug
RUN docker-php-ext-enable xdebug
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

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

@ -0,0 +1,458 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Logging\JUnit;
use DOMDocument;
use DOMElement;
use PHPUnit\Event\Code\Test;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\EventFacadeIsSealedException;
use PHPUnit\Event\Facade;
use PHPUnit\Event\InvalidArgumentException;
use PHPUnit\Event\Telemetry\HRTime;
use PHPUnit\Event\Telemetry\Info;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\PreparationStarted;
use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestSuite\Started;
use PHPUnit\Event\UnknownSubscriberTypeException;
use PHPUnit\TextUI\Output\Printer;
use PHPUnit\Util\Xml;
use function assert;
use function basename;
use function is_int;
use function sprintf;
use function str_replace;
use function trim;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class JunitXmlLogger
{
private readonly Printer $printer;
private readonly \Pest\Logging\Converter $converter; // pest-added
private DOMDocument $document;
private DOMElement $root;
/**
* @var DOMElement[]
*/
private array $testSuites = [];
/**
* @psalm-var array<int,int>
*/
private array $testSuiteTests = [0];
/**
* @psalm-var array<int,int>
*/
private array $testSuiteAssertions = [0];
/**
* @psalm-var array<int,int>
*/
private array $testSuiteErrors = [0];
/**
* @psalm-var array<int,int>
*/
private array $testSuiteFailures = [0];
/**
* @psalm-var array<int,int>
*/
private array $testSuiteSkipped = [0];
/**
* @psalm-var array<int,int>
*/
private array $testSuiteTimes = [0];
private int $testSuiteLevel = 0;
private ?DOMElement $currentTestCase = null;
private ?HRTime $time = null;
private bool $prepared = false;
private bool $preparationFailed = false;
/**
* @throws EventFacadeIsSealedException
* @throws UnknownSubscriberTypeException
*/
public function __construct(Printer $printer, Facade $facade)
{
$this->printer = $printer;
$this->converter = new \Pest\Logging\Converter(\Pest\Support\Container::getInstance()->get(\Pest\TestSuite::class)->rootPath); // pest-added
$this->registerSubscribers($facade);
$this->createDocument();
}
public function flush(): void
{
$this->printer->print($this->document->saveXML());
$this->printer->flush();
}
public function testSuiteStarted(Started $event): void
{
$testSuite = $this->document->createElement('testsuite');
$testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite())); // pest-changed
if ($event->testSuite()->isForTestClass()) {
$testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? ''); // pest-changed
}
if ($this->testSuiteLevel > 0) {
$this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
} else {
$this->root->appendChild($testSuite);
}
$this->testSuiteLevel++;
$this->testSuites[$this->testSuiteLevel] = $testSuite;
$this->testSuiteTests[$this->testSuiteLevel] = 0;
$this->testSuiteAssertions[$this->testSuiteLevel] = 0;
$this->testSuiteErrors[$this->testSuiteLevel] = 0;
$this->testSuiteFailures[$this->testSuiteLevel] = 0;
$this->testSuiteSkipped[$this->testSuiteLevel] = 0;
$this->testSuiteTimes[$this->testSuiteLevel] = 0;
}
public function testSuiteFinished(): void
{
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'tests',
(string) $this->testSuiteTests[$this->testSuiteLevel],
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'assertions',
(string) $this->testSuiteAssertions[$this->testSuiteLevel],
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'errors',
(string) $this->testSuiteErrors[$this->testSuiteLevel],
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'failures',
(string) $this->testSuiteFailures[$this->testSuiteLevel],
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'skipped',
(string) $this->testSuiteSkipped[$this->testSuiteLevel],
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'time',
sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]),
);
if ($this->testSuiteLevel > 1) {
$this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel];
$this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel];
$this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel];
$this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel];
$this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel];
$this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel];
}
$this->testSuiteLevel--;
}
/**
* @throws InvalidArgumentException
*/
public function testPreparationStarted(PreparationStarted $event): void
{
$this->createTestCase($event);
}
/**
* @throws InvalidArgumentException
*/
public function testPreparationFailed(): void
{
$this->preparationFailed = true;
}
/**
* @throws InvalidArgumentException
*/
public function testPrepared(): void
{
$this->prepared = true;
}
/**
* @throws InvalidArgumentException
*/
public function testFinished(Finished $event): void
{
if ($this->preparationFailed) {
return;
}
$this->handleFinish($event->telemetryInfo(), $event->numberOfAssertionsPerformed());
}
/**
* @throws InvalidArgumentException
*/
public function testMarkedIncomplete(MarkedIncomplete $event): void
{
$this->handleIncompleteOrSkipped($event);
}
/**
* @throws InvalidArgumentException
*/
public function testSkipped(Skipped $event): void
{
$this->handleIncompleteOrSkipped($event);
}
/**
* @throws InvalidArgumentException
*/
public function testErrored(Errored $event): void
{
$this->handleFault($event, 'error');
$this->testSuiteErrors[$this->testSuiteLevel]++;
}
/**
* @throws InvalidArgumentException
*/
public function testFailed(Failed $event): void
{
$this->handleFault($event, 'failure');
$this->testSuiteFailures[$this->testSuiteLevel]++;
}
/**
* @throws InvalidArgumentException
*/
private function handleFinish(Info $telemetryInfo, int $numberOfAssertionsPerformed): void
{
assert($this->currentTestCase !== null);
assert($this->time !== null);
$time = $telemetryInfo->time()->duration($this->time)->asFloat();
$this->testSuiteAssertions[$this->testSuiteLevel] += $numberOfAssertionsPerformed;
$this->currentTestCase->setAttribute(
'assertions',
(string) $numberOfAssertionsPerformed,
);
$this->currentTestCase->setAttribute(
'time',
sprintf('%F', $time),
);
$this->testSuites[$this->testSuiteLevel]->appendChild(
$this->currentTestCase,
);
$this->testSuiteTests[$this->testSuiteLevel]++;
$this->testSuiteTimes[$this->testSuiteLevel] += $time;
$this->currentTestCase = null;
$this->time = null;
$this->prepared = false;
}
/**
* @throws EventFacadeIsSealedException
* @throws UnknownSubscriberTypeException
*/
private function registerSubscribers(Facade $facade): void
{
$facade->registerSubscribers(
new TestSuiteStartedSubscriber($this),
new TestSuiteFinishedSubscriber($this),
new TestPreparationStartedSubscriber($this),
new TestPreparationFailedSubscriber($this),
new TestPreparedSubscriber($this),
new TestFinishedSubscriber($this),
new TestErroredSubscriber($this),
new TestFailedSubscriber($this),
new TestSkippedSubscriber($this),
new TestRunnerExecutionFinishedSubscriber($this),
);
}
private function createDocument(): void
{
$this->document = new DOMDocument('1.0', 'UTF-8');
$this->document->formatOutput = true;
$this->root = $this->document->createElement('testsuites');
$this->document->appendChild($this->root);
}
/**
* @throws InvalidArgumentException
*/
private function handleFault(Errored|Failed $event, string $type): void
{
if (! $this->prepared) {
$this->createTestCase($event);
}
assert($this->currentTestCase !== null);
$buffer = $this->converter->getTestCaseMethodName($event->test()); // pest-changed
$throwable = $event->throwable();
$buffer .= trim(
$this->converter->getExceptionMessage($throwable).PHP_EOL. // pest-changed
$this->converter->getExceptionDetails($throwable), // pest-changed
);
$fault = $this->document->createElement(
$type,
Xml::prepareString($buffer),
);
$fault->setAttribute('type', $throwable->className());
$this->currentTestCase->appendChild($fault);
if (! $this->prepared) {
$this->handleFinish($event->telemetryInfo(), 0);
}
}
/**
* @throws InvalidArgumentException
*/
private function handleIncompleteOrSkipped(MarkedIncomplete|Skipped $event): void
{
if (! $this->prepared) {
$this->createTestCase($event);
}
assert($this->currentTestCase !== null);
$skipped = $this->document->createElement('skipped');
$this->currentTestCase->appendChild($skipped);
$this->testSuiteSkipped[$this->testSuiteLevel]++;
if (! $this->prepared) {
$this->handleFinish($event->telemetryInfo(), 0);
}
}
/**
* @throws InvalidArgumentException
*/
private function testAsString(Test $test): string
{
if ($test->isPhpt()) {
return basename($test->file());
}
assert($test instanceof TestMethod);
return sprintf(
'%s::%s%s',
$test->className(),
$this->name($test),
PHP_EOL,
);
}
/**
* @throws InvalidArgumentException
*/
private function name(Test $test): string
{
if ($test->isPhpt()) {
return basename($test->file());
}
assert($test instanceof TestMethod);
if (! $test->testData()->hasDataFromDataProvider()) {
return $test->methodName();
}
$dataSetName = $test->testData()->dataFromDataProvider()->dataSetName();
if (is_int($dataSetName)) {
return sprintf(
'%s with data set #%d',
$test->methodName(),
$dataSetName,
);
}
return sprintf(
'%s with data set "%s"',
$test->methodName(),
$dataSetName,
);
}
/**
* @throws InvalidArgumentException
*
* @psalm-assert !null $this->currentTestCase
*/
private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void
{
$testCase = $this->document->createElement('testcase');
$test = $event->test();
$file = $this->converter->getTestCaseLocation($test); // pest-added
$testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test)); // pest-changed
$testCase->setAttribute('file', $file); // pest-changed
if ($test->isTestMethod()) {
assert($test instanceof TestMethod);
//$testCase->setAttribute('line', (string) $test->line()); // pest-removed
$className = $this->converter->getTrimmedTestClassName($test); // pest-added
$testCase->setAttribute('class', $className); // pest-changed
$testCase->setAttribute('classname', str_replace('\\', '.', $className)); // pest-changed
}
$this->currentTestCase = $testCase;
$this->time = $event->telemetryInfo()->time();
}
}

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

@ -81,11 +81,6 @@ final class DefaultResultCache implements ResultCache
*/
private array $defects = [];
/**
* @psalm-var array<string, TestStatus>
*/
private array $currentDefects = [];
/**
* @psalm-var array<string, float>
*/
@ -102,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
@ -115,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;
}
@ -133,9 +125,15 @@ final class DefaultResultCache implements ResultCache
return;
}
$contents = file_get_contents($this->cacheFilename);
if ($contents === false) {
return;
}
$data = json_decode(
file_get_contents($this->cacheFilename),
true
$contents,
true,
);
if ($data === null) {
@ -183,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,73 +56,74 @@ use function array_map;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class TestSuiteFilterProcessor
final readonly class TestSuiteFilterProcessor
{
private Factory $filterFactory;
public function __construct(Factory $factory = new Factory)
{
$this->filterFactory = $factory;
}
/**
* @throws Event\RuntimeException
* @throws FilterNotConfiguredException
*/
public function process(Configuration $configuration, TestSuite $suite): void
{
$factory = new Factory;
if (! $configuration->hasFilter() &&
! $configuration->hasGroups() &&
! $configuration->hasExcludeGroups() &&
! $configuration->hasExcludeFilter() &&
! $configuration->hasTestsCovering() &&
! $configuration->hasTestsUsing() &&
! Only::isEnabled()
) {
! Only::isEnabled()) {
return;
}
if ($configuration->hasExcludeGroups()) {
$this->filterFactory->addExcludeGroupFilter(
$configuration->excludeGroups()
$factory->addExcludeGroupFilter(
$configuration->excludeGroups(),
);
}
if (Only::isEnabled()) {
$this->filterFactory->addIncludeGroupFilter(['__pest_only']);
$factory->addIncludeGroupFilter([Only::group()]);
} elseif ($configuration->hasGroups()) {
$this->filterFactory->addIncludeGroupFilter(
$configuration->groups()
$factory->addIncludeGroupFilter(
$configuration->groups(),
);
}
if ($configuration->hasTestsCovering()) {
$this->filterFactory->addIncludeGroupFilter(
$factory->addIncludeGroupFilter(
array_map(
static fn (string $name): string => '__phpunit_covers_'.$name,
$configuration->testsCovering()
)
$configuration->testsCovering(),
),
);
}
if ($configuration->hasTestsUsing()) {
$this->filterFactory->addIncludeGroupFilter(
$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()) {
$this->filterFactory->addNameFilter(
$configuration->filter()
$factory->addIncludeNameFilter(
$configuration->filter(),
);
}
$suite->injectFilter($this->filterFactory);
$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

@ -10,7 +10,7 @@
?>
<div class="my-1">
<span class="ml-2 px-1 bg-<?php echo $bgBadgeColor ?>-600 font-bold"><?php echo htmlspecialchars($bgBadgeText) ?></span>
<span class="ml-2 px-1 bg-<?php echo $bgBadgeColor ?> font-bold"><?php echo htmlspecialchars($bgBadgeText) ?></span>
<span class="ml-1">
<?php echo htmlspecialchars($content) ?>
</span>

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

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

@ -15,16 +15,17 @@ final class BootOverrides implements Bootstrapper
/**
* The list of files to be overridden.
*
* @var array<int, string>
* @var array<string, string>
*/
private const FILES = [
'Runner/Filter/NameFilterIterator.php',
'Runner/ResultCache/DefaultResultCache.php',
'Runner/TestSuiteLoader.php',
'TextUI/Command/WarmCodeCoverageCacheCommand.php',
'TextUI/Output/Default/ProgressPrinter/TestSkippedSubscriber.php',
'TextUI/TestSuiteFilterProcessor.php',
'Event/Value/ThrowableBuilder.php',
public const FILES = [
'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,9 +31,8 @@ final class BootSubscribers implements Bootstrapper
* Creates a new instance of the Boot Subscribers.
*/
public function __construct(
private readonly Container $container,
) {
}
private Container $container,
) {}
/**
* Boots the list of Subscribers.

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

@ -60,7 +60,7 @@ trait Pipeable
}
/**
* Get th list of pipes by the given name.
* Get the list of pipes by the given name.
*
* @return array<int, Closure>
*/

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,11 +356,11 @@ trait Testable
return $arguments;
}
if (! $arguments[0] instanceof Closure) {
if (! isset($arguments[0]) || ! $arguments[0] instanceof Closure) {
return $arguments;
}
if (in_array($testParameterTypes[0], [Closure::class, 'callable'])) {
if (isset($testParameterTypes[0]) && in_array($testParameterTypes[0], [Closure::class, 'callable'])) {
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
) {
// ..
}
@ -50,7 +50,7 @@ final class Thanks
$wantsToSupport = false;
if (getenv('PEST_NO_SUPPORT') !== 'true' && $this->input->isInteractive()) {
$wantsToSupport = (new SymfonyQuestionHelper())->ask(
$wantsToSupport = (new SymfonyQuestionHelper)->ask(
new ArrayInput([]),
$this->output,
new ConfirmationQuestion(
@ -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,18 @@
<?php
declare(strict_types=1);
namespace Pest\Contracts\Plugins;
/**
* @internal
*/
interface HandlesOriginalArguments
{
/**
* Adds original arguments before the Test Suite execution.
*
* @param array<int, string> $arguments
*/
public function handleOriginalArguments(array $arguments): void;
}

View File

@ -7,10 +7,10 @@ namespace Pest\Contracts\Plugins;
/**
* @internal
*/
interface Shutdownable
interface Terminable
{
/**
* Shutdowns the plugin.
* Terminates the plugin.
*/
public function shutdown(): void;
public function terminate(): void;
}

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,16 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use RuntimeException;
/**
* @internal
*/
final class FatalException extends RuntimeException implements RenderlessTrace
{
//
}

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,8 +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
@ -347,9 +351,15 @@ final class Expectation
return new HigherOrderExpectation($this, call_user_func_array($this->value->$method(...), $parameters));
}
ExpectationPipeline::for($this->getExpectationClosure($method))
$closure = $this->getExpectationClosure($method);
$reflectionClosure = new \ReflectionFunction($closure);
$expectation = $reflectionClosure->getClosureThis();
assert(is_object($expectation));
ExpectationPipeline::for($closure)
->send(...$parameters)
->through($this->pipes($method, $this, Expectation::class))
->through($this->pipes($method, $expectation, Expectation::class))
->run();
return $this;
@ -414,7 +424,7 @@ final class Expectation
*/
public function any(): Any
{
return new Any();
return new Any;
}
/**
@ -427,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.
*/
@ -434,7 +509,7 @@ 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')),
);
@ -502,17 +577,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),
'to have 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.
*/
@ -578,8 +715,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
{
@ -604,6 +739,39 @@ final class Expectation
);
}
/**
* Asserts that the given expectation target to use the given trait.
*/
public function toUseTrait(string $trait): ArchExpectation
{
return $this->toUseTraits($trait);
}
/**
* Asserts that the given expectation target to use the given traits.
*
* @param array<int, string>|string $traits
*/
public function toUseTraits(array|string $traits): ArchExpectation
{
$traits = is_array($traits) ? $traits : [$traits];
return Targeted::make(
$this,
function (ObjectDescription $object) use ($traits): bool {
foreach ($traits as $trait) {
if (! in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
return false;
}
}
return true;
},
"to use traits '".implode("', '", $traits)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target to not implement any interfaces.
*/
@ -620,7 +788,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
{
@ -664,7 +832,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
{
@ -704,7 +872,10 @@ final class Expectation
return ToUseNothing::make($this);
}
public function toBeUsed(): never
/**
* Not supported.
*/
public function toBeUsed(): void
{
throw InvalidExpectation::fromMethods(['toBeUsed']);
}
@ -848,8 +1019,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
{
@ -876,4 +1045,51 @@ final class Expectation
{
return $this->toHaveMethod('__destruct');
}
/**
* Asserts that the given expectation target is a backed enum of given type.
*/
private function toBeBackedEnum(string $backingType): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum()
&& (new ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
&& (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line
'to be '.$backingType.' backed enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation targets are string backed enums.
*/
public function toBeStringBackedEnums(): ArchExpectation
{
return $this->toBeStringBackedEnum();
}
/**
* Asserts that the given expectation targets are int backed enums.
*/
public function toBeIntBackedEnums(): ArchExpectation
{
return $this->toBeIntBackedEnum();
}
/**
* Asserts that the given expectation target is a string backed enum.
*/
public function toBeStringBackedEnum(): ArchExpectation
{
return $this->toBeBackedEnum('string');
}
/**
* Asserts that the given expectation target is an int backed enum.
*/
public function toBeIntBackedEnum(): ArchExpectation
{
return $this->toBeBackedEnum('int');
}
}

View File

@ -17,6 +17,9 @@ use function expect;
*/
final class EachExpectation
{
/**
* Indicates if the expectation is the opposite.
*/
private bool $opposite = false;
/**
@ -24,9 +27,7 @@ final class EachExpectation
*
* @param Expectation<TValue> $original
*/
public function __construct(private readonly Expectation $original)
{
}
public function __construct(private readonly Expectation $original) {}
/**
* Creates a new expectation.

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,16 +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.
@ -77,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.
*/
@ -84,7 +146,7 @@ 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')),
);
@ -152,17 +214,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.
*/
@ -228,8 +436,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
{
@ -254,10 +460,43 @@ final class OppositeExpectation
);
}
/**
* Asserts that the given expectation target not to use the given trait.
*/
public function toUseTrait(string $trait): ArchExpectation
{
return $this->toUseTraits($trait);
}
/**
* Asserts that the given expectation target not to use the given traits.
*
* @param array<int, string>|string $traits
*/
public function toUseTraits(array|string $traits): ArchExpectation
{
$traits = is_array($traits) ? $traits : [$traits];
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($traits): bool {
foreach ($traits as $trait) {
if (in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
return false;
}
}
return true;
},
"not to use traits '".implode("', '", $traits)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target not to implement the given interfaces.
*
* @param array<int, class-string>|string $interfaces
* @param array<int, string>|string $interfaces
*/
public function toImplement(array|string $interfaces): ArchExpectation
{
@ -294,10 +533,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']);
}
@ -330,10 +567,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']);
}
@ -341,7 +576,7 @@ final class OppositeExpectation
/**
* Not supported.
*/
public function toUseNothing(): never
public function toUseNothing(): void
{
throw InvalidExpectation::fromMethods(['not', 'toUseNothing']);
}
@ -366,7 +601,7 @@ final class OppositeExpectation
), is_string($targets) ? [$targets] : $targets));
}
public function toOnlyBeUsedIn(): never
public function toOnlyBeUsedIn(): void
{
throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedIn']);
}
@ -374,7 +609,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']);
}
@ -394,8 +629,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
{
@ -485,4 +718,51 @@ final class OppositeExpectation
{
return $this->toHaveMethod('__destruct');
}
/**
* Asserts that the given expectation target is not a backed enum of given type.
*/
private function toBeBackedEnum(string $backingType): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum()
|| ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
|| (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line
'not to be '.$backingType.' backed enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation targets are not string backed enums.
*/
public function toBeStringBackedEnums(): ArchExpectation
{
return $this->toBeStringBackedEnum();
}
/**
* Asserts that the given expectation targets are not int backed enums.
*/
public function toBeIntBackedEnums(): ArchExpectation
{
return $this->toBeIntBackedEnum();
}
/**
* Asserts that the given expectation target is not a string backed enum.
*/
public function toBeStringBackedEnum(): ArchExpectation
{
return $this->toBeBackedEnum('string');
}
/**
* Asserts that the given expectation target is not an int backed enum.
*/
public function toBeIntBackedEnum(): ArchExpectation
{
return $this->toBeBackedEnum('int');
}
}

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

@ -28,8 +28,8 @@ trait HigherOrderable
*/
private function bootHigherOrderable(): void
{
$this->chains = new HigherOrderMessageCollection();
$this->factoryProxies = new HigherOrderMessageCollection();
$this->proxies = new HigherOrderMessageCollection();
$this->chains = new HigherOrderMessageCollection;
$this->factoryProxies = new HigherOrderMessageCollection;
$this->proxies = new HigherOrderMessageCollection;
}
}

View File

@ -9,7 +9,5 @@ namespace Pest\Factories\Covers;
*/
final class CoversClass
{
public function __construct(public string $class)
{
}
public function __construct(public string $class) {}
}

View File

@ -9,7 +9,5 @@ namespace Pest\Factories\Covers;
*/
final class CoversFunction
{
public function __construct(public string $function)
{
}
public function __construct(public string $function) {}
}

View File

@ -7,6 +7,4 @@ namespace Pest\Factories\Covers;
/**
* @internal
*/
final class CoversNothing
{
}
final class CoversNothing {}

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,31 +99,28 @@ 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 {
Assert::getCount() > 0 ?: self::markTestIncomplete(); // @phpstan-ignore-line
(Assert::getCount() > 0 || $this->doesNotPerformAssertions()) ?: self::markTestIncomplete(); // @phpstan-ignore-line
};
$this->bootHigherOrderable();
}
/**
* Creates the test's closure.
* Sets the test's hooks, and runs any proxy to the test case.
*/
public function getClosure(TestCase $concrete): Closure
public function setUp(TestCase $concrete): void
{
$concrete::flush(); // @phpstan-ignore-line
@ -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,32 @@ if (! function_exists('afterAll')) {
TestSuite::getInstance()->afterAll->set($closure);
}
}
if (! function_exists('covers')) {
/**
* Specifies which classes, or functions, a test method covers.
*
* @param array<int, string>|string $classesOrFunctions
*/
function covers(array|string ...$classesOrFunctions): void
{
$filename = Backtrace::file();
$beforeEachCall = (new BeforeEachCall(TestSuite::getInstance(), $filename));
$beforeEachCall->covers(...$classesOrFunctions);
$beforeEachCall->group('__pest_mutate_only');
/** @var MutationTestRunner $runner */
$runner = Container::getInstance()->get(MutationTestRunner::class);
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if ($runner->isEnabled() && ! $everything && ! is_array($classes) && ! is_array($paths)) {
$beforeEachCall->only('__pest_mutate_only');
}
}
}

View File

@ -4,23 +4,30 @@ declare(strict_types=1);
namespace Pest;
use NunoMaduro\Collision\Writer;
use Pest\Contracts\Bootstrapper;
use Pest\Exceptions\FatalException;
use Pest\Exceptions\NoDirtyTestsFound;
use Pest\Plugins\Actions\CallsAddsOutput;
use Pest\Plugins\Actions\CallsBoot;
use Pest\Plugins\Actions\CallsHandleArguments;
use Pest\Plugins\Actions\CallsShutdown;
use Pest\Plugins\Actions\CallsHandleOriginalArguments;
use Pest\Plugins\Actions\CallsTerminable;
use Pest\Support\Container;
use Pest\Support\Reflection;
use Pest\Support\View;
use PHPUnit\TestRunner\TestResult\Facade;
use PHPUnit\TextUI\Application;
use PHPUnit\TextUI\Configuration\Registry;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use Whoops\Exception\Inspector;
/**
* @internal
*/
final class Kernel
final readonly class Kernel
{
/**
* The Kernel bootstrappers.
@ -40,10 +47,10 @@ 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,
) {
register_shutdown_function(fn () => $this->shutdown());
//
}
/**
@ -59,6 +66,13 @@ final class Kernel
->add(OutputInterface::class, $output)
->add(Container::class, $container);
$kernel = new self(
new Application,
$output,
);
register_shutdown_function(fn () => $kernel->shutdown());
foreach (self::BOOTSTRAPPERS as $bootstrapper) {
$bootstrapper = Container::getInstance()->get($bootstrapper);
assert($bootstrapper instanceof Bootstrapper);
@ -68,11 +82,6 @@ final class Kernel
CallsBoot::execute();
$kernel = new self(
new Application(),
$output,
);
Container::getInstance()->add(self::class, $kernel);
return $kernel;
@ -81,14 +90,17 @@ final class Kernel
/**
* Runs the application, and returns the exit code.
*
* @param array<int, string> $args
* @param array<int, string> $originalArguments
* @param array<int, string> $arguments
*/
public function handle(array $args): int
public function handle(array $originalArguments, array $arguments): int
{
$args = CallsHandleArguments::execute($args);
CallsHandleOriginalArguments::execute($originalArguments);
$arguments = CallsHandleArguments::execute($arguments);
try {
$this->application->run($args);
$this->application->run($arguments);
} catch (NoDirtyTestsFound) {
$this->output->writeln([
'',
@ -106,16 +118,54 @@ final class Kernel
}
/**
* Shutdown the Kernel.
* Terminate the Kernel.
*/
public function shutdown(): void
public function terminate(): void
{
$preBufferOutput = Container::getInstance()->get(KernelDump::class);
assert($preBufferOutput instanceof KernelDump);
$preBufferOutput->shutdown();
$preBufferOutput->terminate();
CallsShutdown::execute();
CallsTerminable::execute();
}
/**
* Shutdowns unexpectedly the Kernel.
*/
public function shutdown(): void
{
$this->terminate();
if (is_array($error = error_get_last())) {
if (! in_array($error['type'], [E_ERROR, E_CORE_ERROR], true)) {
return;
}
$message = $error['message'];
$file = $error['file'];
$line = $error['line'];
try {
$writer = new Writer(null, $this->output);
$throwable = new FatalException($message);
Reflection::setPropertyValue($throwable, 'line', $line);
Reflection::setPropertyValue($throwable, 'file', $file);
$inspector = new Inspector($throwable);
$writer->write($inspector);
} catch (Throwable) { // @phpstan-ignore-line
View::render('components.badge', [
'type' => 'ERROR',
'content' => sprintf('%s in %s:%d', $message, $file, $line),
]);
}
exit(1);
}
}
}

View File

@ -48,9 +48,9 @@ final class KernelDump
}
/**
* Shutdown the output buffering.
* Terminate the output buffering.
*/
public function shutdown(): void
public function terminate(): void
{
$this->disable();
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Pest\Logging\TeamCity;
namespace Pest\Logging;
use NunoMaduro\Collision\Adapters\Phpunit\State;
use Pest\Exceptions\ShouldNotHappen;
@ -18,25 +18,32 @@ 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();
$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)) {
@ -150,10 +164,47 @@ final class Converter
return Str::after($name, self::PREFIX);
}
/**
* Gets the trimmed test class name.
*/
public function getTrimmedTestClassName(TestMethod $test): string
{
return Str::after($test->className(), self::PREFIX);
}
/**
* Gets the test suite location.
*/
public function getTestSuiteLocation(TestSuite $testSuite): ?string
{
$firstTest = $this->getFirstTest($testSuite);
if ($firstTest == null) {
return null;
}
$path = $firstTest->testDox()->prettifiedClassName();
$classRelativePath = $this->toRelativePath($path);
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
$methodName = $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
return "$classRelativePath::$methodName";
}
return $classRelativePath;
}
/**
* Gets the prettified test method name without dataset-related suffix.
*/
private function getTestMethodNameWithoutDatasetSuffix(TestMethod $testMethod): string
{
return Str::beforeLast($testMethod->testDox()->prettifiedMethodName(), ' with data set ');
}
/**
* Gets the first test from the test suite.
*/
private function getFirstTest(TestSuite $testSuite): ?TestMethod
{
$tests = $testSuite->tests()->asArray();
@ -167,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;
/**
@ -17,8 +20,7 @@ final class ServiceMessage
public function __construct(
private readonly string $type,
private readonly array $parameters,
) {
}
) {}
public function toString(): string
{
@ -36,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",
]);
}
@ -63,7 +65,7 @@ final class ServiceMessage
}
/**
* @param int $duration in milliseconds
* @param int $duration in milliseconds
*/
public static function testFinished(string $name, int $duration): self
{

View File

@ -9,19 +9,17 @@ use Pest\Logging\TeamCity\TeamCityLogger;
/**
* @internal
*/
abstract class Subscriber
abstract class Subscriber // @pest-arch-ignore-line
{
/**
* Creates a new Subscriber instance.
*/
public function __construct(private readonly TeamCityLogger $logger)
{
}
public function __construct(private readonly TeamCityLogger $logger) {}
/**
* 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

@ -6,12 +6,12 @@ namespace Pest\Logging\TeamCity;
use NunoMaduro\Collision\Adapters\Phpunit\Style;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Logging\Converter;
use Pest\Logging\TeamCity\Subscriber\TestConsideredRiskySubscriber;
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;
@ -27,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;
@ -44,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;
/**
@ -107,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.');
}
@ -261,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

@ -7,6 +7,4 @@ namespace Pest\Matchers;
/**
* @internal
*/
final class Any
{
}
final class Any {}

View File

@ -131,7 +131,7 @@ final class Expectation
*
* @return self<TValue>
*/
public function toBeGreaterThan(int|float|DateTimeInterface $expected, string $message = ''): self
public function toBeGreaterThan(int|float|string|DateTimeInterface $expected, string $message = ''): self
{
Assert::assertGreaterThan($expected, $this->value, $message);
@ -143,7 +143,7 @@ final class Expectation
*
* @return self<TValue>
*/
public function toBeGreaterThanOrEqual(int|float|DateTimeInterface $expected, string $message = ''): self
public function toBeGreaterThanOrEqual(int|float|string|DateTimeInterface $expected, string $message = ''): self
{
Assert::assertGreaterThanOrEqual($expected, $this->value, $message);
@ -155,7 +155,7 @@ final class Expectation
*
* @return self<TValue>
*/
public function toBeLessThan(int|float|DateTimeInterface $expected, string $message = ''): self
public function toBeLessThan(int|float|string|DateTimeInterface $expected, string $message = ''): self
{
Assert::assertLessThan($expected, $this->value, $message);
@ -167,7 +167,7 @@ final class Expectation
*
* @return self<TValue>
*/
public function toBeLessThanOrEqual(int|float|DateTimeInterface $expected, string $message = ''): self
public function toBeLessThanOrEqual(int|float|string|DateTimeInterface $expected, string $message = ''): self
{
Assert::assertLessThanOrEqual($expected, $this->value, $message);
@ -196,6 +196,24 @@ final class Expectation
return $this;
}
/**
* Asserts that $needle equal an element of the value.
*
* @return self<TValue>
*/
public function toContainEqual(mixed ...$needles): self
{
if (! is_iterable($this->value)) {
InvalidExpectationValue::expected('iterable');
}
foreach ($needles as $needle) {
Assert::assertContainsEquals($needle, $this->value);
}
return $this;
}
/**
* Asserts that the value starts with $expected.
*
@ -296,7 +314,7 @@ final class Expectation
*
* @return self<TValue>
*/
public function toHaveProperty(string $name, mixed $value = new Any(), string $message = ''): self
public function toHaveProperty(string $name, mixed $value = new Any, string $message = ''): self
{
$this->toBeObject();
@ -314,43 +332,13 @@ final class Expectation
/**
* Asserts that the value contains the provided properties $names.
*
* @param iterable<array-key, string> $names
* @param iterable<string, mixed>|iterable<int, string> $names
* @return self<TValue>
*/
public function toHaveProperties(iterable $names, string $message = ''): self
{
foreach ($names as $name => $value) {
is_int($name) ? $this->toHaveProperty($value, message: $message) : $this->toHaveProperty($name, $value, $message);
}
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);
is_int($name) ? $this->toHaveProperty($value, message: $message) : $this->toHaveProperty($name, $value, $message); // @phpstan-ignore-line
}
return $this;
@ -449,6 +437,18 @@ final class Expectation
return $this;
}
/**
* Asserts that the value is a list.
*
* @return self<TValue>
*/
public function toBeList(string $message = ''): self
{
Assert::assertIsList($this->value, $message);
return $this;
}
/**
* Asserts that the value is of type bool.
*
@ -624,7 +624,7 @@ final class Expectation
*
* @return self<TValue>
*/
public function toHaveKey(string|int $key, mixed $value = new Any(), string $message = ''): self
public function toHaveKey(string|int $key, mixed $value = new Any, string $message = ''): self
{
if (is_object($this->value) && method_exists($this->value, 'toArray')) {
$array = $this->value->toArray();

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();
@ -42,7 +47,7 @@ final class Panic
try {
$output = Container::getInstance()->get(OutputInterface::class);
} catch (Throwable) { // @phpstan-ignore-line
$output = new ConsoleOutput();
$output = new ConsoleOutput;
}
assert($output instanceof OutputInterface);

View File

@ -39,7 +39,7 @@ final class AfterEachCall
) {
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
$this->proxies = new HigherOrderMessageCollection();
$this->proxies = new HigherOrderMessageCollection;
$this->describing = DescribeCall::describing();
}
@ -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
{
@ -44,8 +47,8 @@ final class BeforeEachCall
) {
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
$this->testCallProxies = new HigherOrderMessageCollection();
$this->testCaseProxies = new HigherOrderMessageCollection();
$this->testCallProxies = new HigherOrderMessageCollection;
$this->testCaseProxies = new HigherOrderMessageCollection;
$this->describing = DescribeCall::describing();
}
@ -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;
}
@ -208,12 +255,37 @@ final class TestCall
return $this;
}
/**
* Skips the current test on the given PHP version.
*/
public function skipOnPhp(string $version): self
{
if (mb_strlen($version) < 2) {
throw new InvalidArgumentException('The version must start with [<] or [>].');
}
if (str_starts_with($version, '>=') || str_starts_with($version, '<=')) {
$operator = substr($version, 0, 2);
$version = substr($version, 2);
} elseif (str_starts_with($version, '>') || str_starts_with($version, '<')) {
$operator = $version[0];
$version = substr($version, 1);
// ensure starts with number:
} elseif (is_numeric($version[0])) {
$operator = '==';
} else {
throw new InvalidArgumentException('The version must start with [<, >, <=, >=] or a number.');
}
return $this->skip(version_compare(PHP_VERSION, $version, $operator), sprintf('This test is skipped on PHP [%s%s].', $operator, $version));
}
/**
* Skips the current test if the given test is running on Windows.
*/
public function skipOnWindows(): self
{
return $this->skipOn('Windows', 'This test is skipped on [Windows].');
return $this->skipOnOs('Windows', 'This test is skipped on [Windows].');
}
/**
@ -221,7 +293,7 @@ final class TestCall
*/
public function skipOnMac(): self
{
return $this->skipOn('Darwin', 'This test is skipped on [Mac].');
return $this->skipOnOs('Darwin', 'This test is skipped on [Mac].');
}
/**
@ -229,13 +301,13 @@ final class TestCall
*/
public function skipOnLinux(): self
{
return $this->skipOn('Linux', 'This test is skipped on [Linux].');
return $this->skipOnOs('Linux', 'This test is skipped on [Linux].');
}
/**
* Skips the current test if the given test is running on the given operating systems.
*/
private function skipOn(string $osFamily, string $message): self
private function skipOnOs(string $osFamily, string $message): self
{
return $osFamily === PHP_OS_FAMILY
? $this->skip($message)
@ -281,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|null $issue = null,
array|string|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|null $issue = null,
array|string|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|null $issue = null,
array|string|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);
}
@ -321,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;
@ -333,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;
@ -344,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;
}
@ -395,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));
}
@ -411,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.30.0';
return '3.0.7';
}
function testDirectory(string $file = ''): string

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins\Actions;
use Pest\Contracts\Plugins;
use Pest\Plugin\Loader;
/**
* @internal
*/
final class CallsHandleOriginalArguments
{
/**
* Executes the Plugin action.
*
* Transform the input arguments by passing it to the relevant plugins.
*
* @param array<int, string> $argv
*/
public static function execute(array $argv): void
{
$plugins = Loader::getPlugins(Plugins\HandlesOriginalArguments::class);
/** @var Plugins\HandlesOriginalArguments $plugin */
foreach ($plugins as $plugin) {
$plugin->handleOriginalArguments($argv);
}
}
}

View File

@ -10,20 +10,20 @@ use Pest\Plugin\Loader;
/**
* @internal
*/
final class CallsShutdown
final class CallsTerminable
{
/**
* Executes the Plugin action.
*
* Provides an opportunity for any plugins to shutdown.
* Provides an opportunity for any plugins to terminate.
*/
public static function execute(): void
{
$plugins = Loader::getPlugins(Plugins\Shutdownable::class);
$plugins = Loader::getPlugins(Plugins\Terminable::class);
/** @var Plugins\Shutdownable $plugin */
/** @var Plugins\Terminable $plugin */
foreach ($plugins as $plugin) {
$plugin->shutdown();
$plugin->terminate();
}
}
}

View File

@ -6,6 +6,10 @@ namespace Pest\Plugins;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Plugins\Concerns\HandleArguments;
use PHPUnit\TextUI\CliArguments\Builder as CliConfigurationBuilder;
use PHPUnit\TextUI\CliArguments\XmlConfigurationFileFinder;
use PHPUnit\TextUI\XmlConfiguration\DefaultConfiguration;
use PHPUnit\TextUI\XmlConfiguration\Loader;
/**
* @internal
@ -30,10 +34,21 @@ final class Cache implements HandlesArguments
*/
public function handleArguments(array $arguments): array
{
$arguments = $this->pushArgument(
sprintf('--cache-directory=%s', realpath(self::TEMPORARY_FOLDER)),
$arguments
);
if (! $this->hasArgument('--cache-directory', $arguments)) {
$cliConfiguration = (new CliConfigurationBuilder)->fromParameters([]);
$configurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration);
$xmlConfiguration = DefaultConfiguration::create();
if (is_string($configurationFile)) {
$xmlConfiguration = (new Loader)->load($configurationFile);
}
if (! $xmlConfiguration->phpunit()->hasCacheDirectory()) {
$arguments = $this->pushArgument('--cache-directory', $arguments);
$arguments = $this->pushArgument((string) realpath(self::TEMPORARY_FOLDER), $arguments);
}
}
if (! $this->hasArgument('--parallel', $arguments)) {
return $this->pushArgument('--cache-result', $arguments);

View File

@ -16,7 +16,17 @@ trait HandleArguments
*/
public function hasArgument(string $argument, array $arguments): bool
{
return in_array($argument, $arguments, true);
foreach ($arguments as $arg) {
if ($arg === $argument) {
return true;
}
if (str_starts_with((string) $arg, "$argument=")) { // @phpstan-ignore-line
return true;
}
}
return false;
}
/**

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(
@ -128,9 +132,9 @@ final class Coverage implements AddsOutput, HandlesArguments
if ($exitCode === 1) {
$this->output->writeln(sprintf(
"\n <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected:<fg=red;options=bold> %s %%</>. Minimum:<fg=white;options=bold> %s %%</>.",
number_format($coverage, 1),
number_format($this->coverageMin, 1)
"\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(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
) {
// ..
}
@ -61,6 +61,10 @@ final class Help implements HandlesArguments
assert(is_string($argument));
if (trim($argument) === '--process-isolation') {
continue;
}
View::render('components.two-column-detail', [
'left' => $this->colorizeOptions($argument),
'right' => preg_replace(['/</', '/>/'], ['[', ']'], $description),
@ -93,10 +97,9 @@ final class Help implements HandlesArguments
*/
private function getContent(): array
{
$helpReflection = new \ReflectionClass(PHPUnitHelp::class);
$helpReflection = new PHPUnitHelp;
/** @var array<string, array<int, array{arg: string, desc: string}>> $content */
$content = $helpReflection->getConstant('HELP_TEXT');
$content = (fn (): array => $this->elements())->call($helpReflection);
$content['Configuration'] = [...[[
'arg' => '--init',
@ -120,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',
@ -140,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

@ -4,13 +4,13 @@ declare(strict_types=1);
namespace Pest\Plugins;
use Pest\Contracts\Plugins\Shutdownable;
use Pest\Contracts\Plugins\Terminable;
use Pest\PendingCalls\TestCall;
/**
* @internal
*/
final class Only implements Shutdownable
final class Only implements Terminable
{
/**
* The temporary folder.
@ -26,8 +26,12 @@ final class Only implements Shutdownable
/**
* {@inheritDoc}
*/
public function shutdown(): void
public function terminate(): void
{
if (Parallel::isWorker()) {
return;
}
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (file_exists($lockFile)) {
@ -38,14 +42,26 @@ final class Only implements Shutdownable
/**
* Creates the lock file.
*/
public static function enable(TestCall $testCall): void
public static function enable(TestCall $testCall, string $group = '__pest_only'): void
{
$testCall->group('__pest_only');
$testCall->group($group);
if (Environment::name() === Environment::CI || Parallel::isWorker()) {
return;
}
$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);
}
}
@ -58,4 +74,18 @@ final class Only implements Shutdownable
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,14 +34,15 @@ 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.
*/
public static function isEnabled(): bool
{
$argv = new ArgvInput();
$argv = new ArgvInput;
if ($argv->hasParameterOption('--parallel')) {
return true;
}
@ -126,7 +127,7 @@ final class Parallel implements HandlesArguments
$arguments
);
$exitCode = $this->paratestCommand()->run(new ArgvInput($filteredArguments), new CleanConsoleOutput());
$exitCode = $this->paratestCommand()->run(new ArgvInput($filteredArguments), new CleanConsoleOutput);
return CallsAddsOutput::execute($exitCode);
}
@ -173,7 +174,7 @@ final class Parallel implements HandlesArguments
*/
private function hasArgumentsThatWouldBeFasterWithoutParallel(): bool
{
$arguments = new ArgvInput();
$arguments = new ArgvInput;
foreach (self::UNSUPPORTED_ARGUMENTS as $unsupportedArgument) {
if ($arguments->hasParameterOption($unsupportedArgument)) {

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

@ -63,8 +63,7 @@ final class ResultPrinter
{
public function __construct(
private readonly OutputInterface $output,
) {
}
) {}
public function print(string $buffer): void
{
@ -79,9 +78,7 @@ final class ResultPrinter
$this->output->write(OutputFormatter::escape($buffer));
}
public function flush(): void
{
}
public function flush(): void {}
};
$this->compactPrinter = CompactPrinter::default();
@ -172,7 +169,7 @@ final class ResultPrinter
return;
}
$state = (new StateGenerator())->fromPhpUnitTestResult($this->passedTests, $testResult);
$state = (new StateGenerator)->fromPhpUnitTestResult($this->passedTests, $testResult);
$this->compactPrinter->errors($state);
$this->compactPrinter->recap($state, $testResult, $duration, $this->options);

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(
@ -91,13 +106,13 @@ final class WrapperRunner implements RunnerInterface
private readonly OutputInterface $output
) {
$this->printer = new ResultPrinter($output, $options);
$this->timer = new Timer();
$this->timer = new Timer;
$wrapper = realpath(
dirname(__DIR__, 4).DIRECTORY_SEPARATOR.'bin'.DIRECTORY_SEPARATOR.'worker.php',
);
assert($wrapper !== false);
$phpFinder = new PhpExecutableFinder();
$phpFinder = new PhpExecutableFinder;
$phpBin = $phpFinder->find(false);
assert($phpBin !== false);
$parameters = [$phpBin];
@ -107,10 +122,13 @@ 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;
$this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry();
$this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry;
}
public function run(): int
@ -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++) {
@ -357,12 +390,21 @@ final class WrapperRunner implements RunnerInterface
return;
}
$coverageManager = new CodeCoverage();
$coverageManager = new CodeCoverage;
$coverageManager->init(
$this->options->configuration,
$this->codeCoverageFilterRegistry,
false,
);
if (! $coverageManager->isActive()) {
$this->output->writeln([
'',
' <fg=black;bg=yellow;options=bold> WARN </> No code coverage driver is available.</>',
'',
]);
return;
}
$coverageMerger = new CoverageMerger($coverageManager->codeCoverage());
foreach ($this->coverageFiles as $coverageFile) {
$coverageMerger->addCoverageFromFile($coverageFile);
@ -380,8 +422,9 @@ final class WrapperRunner implements RunnerInterface
return;
}
$testSuite = (new LogMerger())->merge($this->junitFiles);
(new Writer())->write(
$testSuite = (new LogMerger)->merge($this->junitFiles);
assert($testSuite instanceof \ParaTest\JUnit\TestSuite);
(new Writer)->write(
$testSuite,
$this->options->configuration->logfileJunit(),
);

View File

@ -22,6 +22,10 @@ final class Printer implements HandlesArguments
return $arguments;
}
if (in_array('--no-output', $arguments, true)) {
return $arguments;
}
return $this->pushArgument('--no-output', $arguments);
}
}

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