Compare commits

...

434 Commits

Author SHA1 Message Date
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
0697555dc2 chore: adjusts sponsors 2024-08-05 10:42:52 +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
6a8a4f3243 Merge pull request #1194 from dmason30/patch-1
Include method name in toHaveMethod error message
2024-07-20 18:29:37 +01:00
ef29b4f091 Include method name in toHaveMethod error message 2024-07-19 15:30:43 +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
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
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
303f4c0113 Adds sponsor 2024-04-19 20:47:46 +01:00
35a1fcd0cf chore: updates readme 2024-04-08 12:28:43 +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
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
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
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
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
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
f75063c420 release: 2.32.2 2024-01-23 18:12:07 +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
5332858782 chore: fixes snapshots 2024-01-11 15:46:50 +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
b1c59ec2e6 feat: allow string type in gt/lt expectations 2024-01-05 16:21:02 +02: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
97dc32f9d2 release: v2.30.0 2023-12-28 10:36:40 +00:00
a3ab065343 chore: coding style 2023-12-28 10:36:30 +00:00
c390721ac3 chore: update snapshots 2023-12-28 10:34:22 +00:00
f83d758d4b feat: adds fails 2023-12-28 10:31:39 +00:00
e00aba539a release: v2.29.1 2023-12-27 15:27:07 +00:00
7799500d06 release: v2.29.0 2023-12-27 11:12:01 +00:00
c099991cd9 Merge pull request #1044 from nhrrs/fix-typo
Fix typo in `toBeClass`
2023-12-23 02:03:57 +00:00
e27d2e7394 Fix typo in toBeClass 2023-12-23 00:36:41 +00: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
8f738f5d49 Revert "Merge pull request #919 from WendellAdriel/feature/coverage-errors-only-flag-2"
This reverts commit 1e2ca40c5b, reversing
changes made to 4522cb5dcb.
2023-12-17 22:03:15 +00:00
1e2ca40c5b Merge pull request #919 from WendellAdriel/feature/coverage-errors-only-flag-2
[2.x] Print only files below the min coverage
2023-12-17 21:56:14 +00:00
4522cb5dcb Merge pull request #1014 from mjsafarali/chore/docker-file-optimization
[2.x] Dockerfile Optimization
2023-12-17 21:39:38 +00:00
9ee4191020 release: v2.28.1 2023-12-15 11:42:34 +00:00
cc65009d0a chore: adds "phpunit/phpunit": "^10.5.3" support 2023-12-15 11:42:23 +00:00
453133d382 chore: code style changes 2023-12-15 11:42:09 +00:00
dd0dddffd4 docs: updates sponsors 2023-12-11 12:05:58 +00:00
9a8f6e6414 release: v2.28.0 2023-12-05 19:06:22 +00:00
4ece95a040 tests: uses arch function 2023-12-05 19:06:11 +00:00
0cc09380bc chore: bumps dependencies 2023-12-05 19:06:03 +00:00
809fb855de release: v2.27.0 2023-12-04 11:11:35 +00:00
aa14f2e200 chore: uses specific symfony versions 2023-12-04 11:08:41 +00:00
e319bdb6d3 chore: fixes missing caret on workflow 2023-12-04 11:04:08 +00:00
fb7340b556 chore: fixes exclude key and add fail-fast 2023-12-04 11:02:41 +00:00
0528fec083 chore: fixes duplicated key name on workflow 2023-12-04 10:59:58 +00:00
1cbaaf6e12 chore: allows symfony 7 on composer 2023-12-04 10:55:34 +00:00
dc862f60b2 chore: adjusts workflow 2023-12-04 10:54:11 +00:00
ff04d54247 chore: adjusts workflow name 2023-12-04 10:40:29 +00:00
330cf05177 chore: adjusts workflow 2023-12-04 10:38:37 +00:00
42b5fa914c Fixes integration tests 2023-12-04 10:15:55 +00:00
3b1026b7d7 chore: fixes workflow name 2023-12-04 10:14:51 +00:00
b6151e0d01 chore: tests against Symonfy 7 2023-12-04 10:10:36 +00:00
d6db2c13c1 Merge pull request #1025 from xiCO2k/fix/allow-todo-argument
[2.x] Allow `--todo` argument.
2023-11-30 10:47:10 +00:00
07b6ff6c04 Update bin/pest
Co-authored-by: Owen Voke <development@voke.dev>
2023-11-30 07:49:24 +00:00
ac5da9e3f7 feat: Allow --todo argument. 2023-11-30 00:32:23 +00:00
90fb8c602c release: v2.26.0 2023-11-29 09:09:09 +00:00
3974a65a18 Merge pull request #1017 from markhuot/patch-2
[2.x] Add `toSnapshot` early return
2023-11-29 08:50:28 +00:00
2a54b5819d #1017 adds early return toSnapshot test 2023-11-28 20:59:45 -05:00
8be46b57a0 Update toHaveProperties() $names param 2023-11-24 09:16:13 +01:00
7177791f1e Merge pull request #1020 from allanmcarvalho/2.x
Update Expectation.php
2023-11-23 17:42:51 +00:00
c743b10a87 Update Expectation.php
Removed @internal phpdoc
2023-11-23 13:15:50 -03:00
83f8de17c8 release: v2.25.0 2023-11-22 07:17:30 +00:00
da20a62e49 Add toSnapshot() early return
Sometimes objects need native toString() and toArray() methods that are different from what you want to snapshot.

This adds an explicit toSnapshot() method that will be called first (when set) allowing for better snapshot values than the generic methods offer.
2023-11-21 22:56:21 -05:00
c8d3e1a9fa Merge pull request #1012 from nahime0/2.x
[2.x] Added onlyOn* methods to run the test only on a specific OS
2023-11-21 01:01:24 +11:00
f7705fe1c1 feat: onlyOn* methods, removed private onlyOn, rely instead on skipOn* methods 2023-11-20 14:51:38 +01:00
4f35dbc607 chore: optimized version of the Dockerfile 2023-11-18 14:57:03 +03:30
2e01776272 add to be backed enum expectation 2023-11-18 03:31:35 +00:00
cf23dfa477 feat: onlyOn* methods now use the private onlyOn method 2023-11-17 16:16:48 +01:00
ab4787c667 feat: added onlyOn* methods to run the test only on a specific OS 2023-11-17 15:03:28 +01:00
bd6b166a62 Merge pull request #1002 from faissaloux/remove-double-plus
Remove double plus
2023-11-08 10:01:57 +00:00
17340947b3 remove double plus 2023-11-08 10:52:33 +01:00
f235d84d95 release: v2.24.3 2023-11-08 09:47:14 +00:00
3c0d780696 Merge pull request #1001 from faissaloux/fix-html-in-descriptions-or-datasets
Fix html in descriptions or datasets
2023-11-08 09:40:48 +00:00
16768fca9f update snapshots/paralell test 2023-11-07 17:46:00 +01:00
95ec0a82b2 fix html in tests descriptions and datasets 2023-11-07 17:35:42 +01:00
15cd7187e9 Update toContainEquals.php 2023-11-06 10:31:48 +01:00
0a680dd06e release: v2.24.2 2023-11-01 19:10:11 -04:00
152892cc38 chore: bumps paratest 2023-11-01 19:06:05 -04:00
9aad417fb2 Merge pull request #996 from CalebDW/phpstan
Create PHPStan extension and add `HigherOrderTapProxy` to `universalObjectCratesClasses`
2023-10-30 20:49:55 -04:00
b58e0cba66 Add Expectation to universalObjectCratesClasses 2023-10-30 14:48:23 -05:00
74864c60e1 Create phpstan extension 2023-10-30 11:55:26 -05:00
fd4f161edd release: v2.24.1 2023-10-26 11:02:35 -04:00
e0939e3e99 chore: adds phpunit 10.4.2 support 2023-10-26 11:02:26 -04:00
2cbecd10e6 Fix typo in toHaveProperties() PHPDoc block 2023-10-23 11:23:53 +02:00
2cdd5e3ba0 fix: infer generic type from expectation 2023-10-21 11:06:26 +01:00
811ef27ee4 release: v2.24.0 2023-10-17 10:07:18 +01:00
22a7fd0656 chore: adjusts snapshots 2023-10-17 10:07:08 +01:00
698c276cbe chore: fixes style 2023-10-17 10:06:58 +01:00
6340656ece chore: bumps dependencies 2023-10-17 10:06:48 +01:00
2d5840f947 Merge pull request #933 from hungthai1401/throws_unless
[2.x] Add `throwsUnless`
2023-10-17 10:03:16 +01:00
b8bb3684a3 Merge pull request #983 from Muhammad-Sarfaraz/patch-1
Polishing Up "TestDox.php' PHPDoc Blocks for Clarity
2023-10-17 10:02:08 +01:00
b8cd563569 Update src/Factories/Annotations/TestDox.php
Co-authored-by: Owen Voke <development@voke.dev>
2023-10-16 16:23:09 +06:00
9fb64599de Polishing Up "TestDox.php' PHPDoc Blocks for Clarity
Added the missing parenthesis and period for proper punctuation and formatted the doc block to meet PHPDocumentor standards.
2023-10-16 10:56:06 +06:00
502f37d280 chore: updates links 2023-10-15 12:10:07 +01:00
29cfa8ec35 chore: updates sponsors 2023-10-14 11:34:00 +01:00
86c107ae5e Extract ANSI escape sequence to a function 2023-10-13 20:16:46 +03:30
a63cd2e4f5 Merge pull request #980 from salehhashemi1992/fix/lifecycle-hook-scope
Fix TestCycle hook scope
2023-10-13 15:27:37 +01:00
7249b59e52 fix lifecycle hook scope 2023-10-13 17:51:02 +03:30
5c94d9994e Merge pull request #979 from salehhashemi1992/ci/checkout-update
Update actions/checkout to v4
2023-10-13 00:36:11 +01:00
bb0a5d8323 update checkout to v4 2023-10-12 21:32:46 +03:30
b126e8e6e4 release: v2.23.2 2023-10-10 16:40:34 +01:00
677129d23d chore: uses paratest 7.3.0 2023-10-10 16:39:04 +01:00
cef5c36885 release: v2.23.1 2023-10-10 15:57:14 +01:00
a343ba4a29 chore: adds PHPUnit 10.4.1 support 2023-10-10 15:57:07 +01:00
21b30b22a7 release: v2.23.0 2023-10-10 15:41:56 +01:00
449c4b6c5e chore: adds collision v8 support 2023-10-10 15:37:25 +01:00
6513ad6ced release: v2.22.1 2023-10-10 14:59:16 +01:00
12421c846e chore: adds termwind v2 support 2023-10-10 14:55:43 +01:00
a312cecede release: v2.22.0 2023-10-10 08:45:41 +01:00
4be97ed314 Merge pull request #977 from JonPurvis/to-be-url-expectation
[2.x] Adds `toBeUrl()` Expectation
2023-10-09 20:06:52 +01:00
5101b9dce3 add to be url expectation 2023-10-09 20:02:11 +01:00
2ffafd445d release: v2.21.0 2023-10-06 13:33:39 +01:00
6068ef6150 feat: adds support for PHPUnit 10.4 2023-10-06 13:33:31 +01:00
8c0b933fcd chore: bumps dependencies 2023-10-05 18:32:07 +01:00
991e02649a chore: bumps paratest 2023-10-05 09:42:56 +01: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
a8b785f69e release: v2.20.0 2023-09-29 19:05:52 +01:00
56610d886d Merge pull request #968 from JonPurvis/add-to-be-between-expectation
[2.x] Add `toBeBetween` expectation
2023-09-29 19:01:05 +01:00
be0d9e964b add toBeBetween() expectation 2023-09-22 01:55:06 +01:00
6bc9da3fe1 chore: bumps collision 2023-09-19 11:48:16 +01:00
6f54462070 fix: sync wrapper runner with paratest 2023-09-19 11:27:09 +01:00
876629b744 release: v2.19.1 2023-09-19 11:01:29 +01:00
5e74e5a19d release: v2.19.0 2023-09-19 10:48:34 +01:00
0d114e21fd chore: updates snapshots 2023-09-19 10:48:23 +01:00
95b65fe72b Merge pull request #962 from JonPurvis/construct-destruct-expectations
add toHaveConstructor() and toHaveDestructor() expectations
2023-09-18 11:37:46 +01:00
bc08f2cb55 fix style issues 2023-09-18 01:13:51 +01:00
6c73a3d90b initial commit 2023-09-18 01:00:50 +01:00
c08f33638a chore: updates release 2023-09-13 23:16:44 +01:00
6c93390c9c chore: removes changelog.md 2023-09-13 23:15:56 +01:00
b53e396aac release: v2.18.2 2023-09-13 23:14:31 +01:00
8b327aa8b4 chore: adds phpunit 10.3.4 support 2023-09-13 23:14:22 +01:00
d0c6f9bc60 Merge pull request #957 from adevade/patch-1
Switch mixed indentation to spaces only in Laravel stub for `phpunit.xml.stub`
2023-09-12 16:15:39 +01:00
b5e066939b Whoops 2023-09-12 09:04:30 +02:00
7892237408 Update Laravel phpunit.xml.stub indentation 2023-09-12 09:03:41 +02:00
74df53c72b release: v2.18.1 2023-09-11 11:38:47 +01:00
ee26457705 Merge pull request #956 from Itemshopp/phpunit-xml-stub-update
[2.x] Update phpunit.xml stub file
2023-09-11 11:32:34 +01:00
MHO
09e6a0944a Removed self closing coverage tag from phpunit xml stub files 2023-09-11 11:03:25 +02:00
MHO
bdee46043a Reformatted php unit xml tag attributes in both init stubs files 2023-09-10 13:37:26 +02:00
MHO
3e25168777 Corrected incorrect indentation in laravel phpunit xml slug 2023-09-10 13:00:55 +02:00
MHO
21b8507252 Updated Laravel init phpunit.xml stub file 2023-09-08 16:40:25 +02:00
MHO
d8e283777e Updated phpunit.xml stub file 2023-09-08 15:39:44 +02:00
2b0aa4b9c9 release: v2.18.0 2023-09-07 19:00:46 +01:00
040eb8142d chore: phpunit 10.3.3 support 2023-09-07 19:00:26 +01:00
d1aeabc9da chore: style changes 2023-09-06 12:19:27 +01:00
e4ec2b3efa chore: updates snapshots 2023-09-06 11:58:48 +01:00
dedcc6b887 Merge pull request #950 from hungthai1401/wrong_comment
[2.x] Correct some  comment messages in `OppositeExpectation`
2023-09-06 11:55:17 +01:00
2b0ed2bc45 Merge pull request #948 from hungthai1401/to_be_uuid
[2.x] Add `toBeUuid` expectation
2023-09-06 11:54:05 +01:00
9c859ae7c4 Merge branch '2.x' into to_be_uuid 2023-09-06 11:53:58 +01:00
ae0a230046 chore: improves readability 2023-09-06 11:48:53 +01:00
644fade478 Merge pull request #949 from pestphp/fix-depends-with-describe
[2.x] Fix the Usage of `depends` With `describe`
2023-09-06 11:47:34 +01:00
c9e919dd40 fix: correct comment message in OppositeExpectation 2023-09-06 08:34:48 +07:00
42323e27b1 fix: correct method name 2023-09-06 08:21:42 +07:00
3927177b23 finishing the code 2023-09-05 20:36:18 -03:00
038fd80428 feat: toBeUUID expectation 2023-09-05 08:25:02 +07:00
cc6c5bf199 docs: updates changelog 2023-09-04 00:20:57 +01:00
b88d9e8ff2 tests: update snapshots 2023-09-03 23:24:52 +01:00
0fc232bbc7 Merge pull request #934 from hungthai1401/to_have_attribute_expectation
[2.x] Add `toHaveAttribute` expectation
2023-09-03 23:18:47 +01:00
7dcd42d113 chore: prepares release 2023-09-03 21:39:21 +01:00
e79ffc6bad tests: adjusts snapshots 2023-09-03 21:36:48 +01:00
8ea425b266 Merge pull request #947 from ludoguenet/2.x
[2.x] Add `toHaveMethod` arch expectation
2023-09-03 21:32:01 +01:00
3a0f6a1d09 chore: prepares release 2023-09-03 13:37:26 +01:00
b9b90295fa Update Expectation.php
Typo
2023-09-01 18:39:37 +02:00
9dabecacbf Add toHaveMethod arch expectation 2023-09-01 18:32:40 +02:00
04fa6b6372 Merge pull request #943 from fabio-ivona/datasets-in-pest-file
fix directory separator for windows
2023-08-29 10:36:58 +01:00
a0d2856f51 docs: update changelog 2023-08-29 10:36:06 +01:00
55b9266648 release: v2.16.1 2023-08-29 10:30:36 +01:00
4313a1ef20 chore: bump dependencies 2023-08-29 10:30:28 +01:00
005ef03845 chore: bumps dependencies 2023-08-29 10:17:07 +01:00
bbac28c9f4 fix directory separator for windows 2023-08-29 11:14:23 +02:00
eb56483ba2 Merge pull request #942 from fabio-ivona/datasets-in-pest-file
[fix] missing datasets when defined in Pest.php file
2023-08-29 09:54:09 +01:00
5d6b717c9a fix missing datasets when defined in Pest.php file 2023-08-29 10:49:17 +02:00
e888f3613b refactor: change falsy to false 2023-08-24 16:40:30 +07:00
e69899559d refactor: generic attribute 2023-08-24 15:23:13 +07:00
e6fe968d44 fix: pint 2023-08-24 14:45:11 +07:00
678898efe7 feat: toHaveAttribute expectation 2023-08-24 14:26:54 +07:00
6c3d8829ce feat: throwsUnless method 2023-08-24 09:28:47 +07:00
14859a4c89 Merge pull request #930 from pestphp/feature/same-size-arg
[2.x] chore: resolve `toHaveSameSize` parameter
2023-08-23 12:01:34 +01:00
8a44d3f136 chore: resolve toHaveSameSize parameter 2023-08-23 11:28:41 +01:00
be71d6918d chore: bump dependencies 2023-08-23 10:35:06 +01:00
afb3dd459a Merge pull request #924 from hungthai1401/to_have_same_size_expectation
[2.x] Add `toHaveSameSize` expectation
2023-08-23 10:14:17 +01:00
b6e3ffafa7 fix: phpstan 2023-08-23 08:14:27 +07:00
6c95f3d8cf Merge pull request #923 from hungthai1401/inconsistent_type_have_count_exception
[2.x] Inconsistent type have count exception
2023-08-22 10:37:59 +01:00
2192373bec test: toHaveSameSize 2023-08-22 11:10:38 +07:00
dfcdaa3f8e feat: toHaveSameSize expectation 2023-08-22 11:10:25 +07:00
79bc9e677f test: toHaveCount with invalid type 2023-08-22 10:36:10 +07:00
60b615ea6a fix: inconsistent type in InvalidExpectationValue exception at toHaveCount expectation 2023-08-22 10:35:07 +07:00
8787481e40 docs: updates changelog 2023-08-21 09:53:42 +01:00
c24406259f docs: updates changelog 2023-08-21 09:51:12 +01:00
cbd6a65057 release: v2.16.0 2023-08-21 09:42:07 +01:00
175004baf3 chore: adds testing on native functions 2023-08-21 09:40:04 +01:00
6d9c0483a6 chore: improves type checking 2023-08-21 09:39:55 +01:00
2dc413cba0 tests: update snapshots 2023-08-19 10:42:42 +01:00
206548af2b Merge pull request #895 from cerbero90/feature/traversable-sequence
[2.x] Add support for nested traversable in sequence
2023-08-19 09:38:46 +00:00
af6de422e9 Merge pull request #921 from leMaur/feat/string-case-expectations
feat: add string case expectations
2023-08-19 09:38:31 +00:00
1c7b254395 Merge branch '2.x' into feat/string-case-expectations 2023-08-19 10:28:48 +02:00
de1c721cd9 chore: improve error messages 2023-08-19 10:27:21 +02:00
f8dd286213 chore: skip array list 2023-08-19 10:27:02 +02:00
e11337df2d Merge branch '2.x' into feature/traversable-sequence 2023-08-19 00:34:30 +02:00
2f90d4ccd7 tests: update snapshots 2023-08-18 12:16:15 +01:00
2db15af24a Merge branch '2.x' into feature/traversable-sequence 2023-08-18 12:33:09 +02:00
8ea7b2b802 Add errors-only flag 2023-08-18 10:13:28 +01:00
c9e3932637 Merge pull request #911 from devajmeireles/feature/add-to-be-digits-expectation
[2.x] Introducing `toBeDigits` Expectation
2023-08-18 03:13:59 +00:00
d218afaf77 introducing new proposal of the PR template 2023-08-17 18:50:23 -03:00
19739ff814 Merge pull request #915 from pestphp/adapting-phpunit-xml-stubs
[2.x] Adapting `phpunit.xml` stubs with PhpUnit
2023-08-17 18:56:50 +00:00
478144fb35 feat: add toHaveStudlyCaseKeys 2023-08-17 20:51:26 +02:00
5d81cf0d4c feat: add toHaveCamelCaseKeys 2023-08-17 20:51:14 +02:00
0b115230f9 feat: add toHaveKebabCaseKeys 2023-08-17 20:50:51 +02:00
0b246f7a76 feat: add toHaveSnakeCaseKeys 2023-08-17 20:50:26 +02:00
7914224ff7 introducing https://schema.phpunit.de/10.3/phpunit.xsd 2023-08-17 15:50:15 -03:00
997b0e9368 feat: add toBeStudlyCase 2023-08-17 20:49:40 +02:00
a76414aeee feat: add toBeCamelCase 2023-08-17 20:49:21 +02:00
d2096df82a feat: add toBeKebabCase 2023-08-17 20:48:51 +02:00
4951b1b0f9 feat: add toBeSnakeCase 2023-08-17 20:48:18 +02:00
f2e31452f2 Merge pull request #912 from devajmeireles/issue-template
Introducing Issue Template
2023-08-17 11:04:27 -03:00
c2985ffb31 release: v2.15.0 2023-08-17 11:28:55 +01:00
492f797dd5 chore: style changes 2023-08-17 11:24:16 +01:00
0b261ef97b feat: adds php@8.3 support 2023-08-17 11:19:43 +01:00
f19692a72f chore: changes phpstan settings 2023-08-17 11:19:11 +01:00
0787b37f2c chore: style changes 2023-08-17 11:18:59 +01:00
f0223b50d0 introducing sample repository input 2023-08-16 15:50:40 -03:00
0263fcb2ac wip 2023-08-16 14:18:09 -03:00
c0a234317b introducing issue template 2023-08-16 14:16:18 -03:00
72100075d2 docs: updates changelog 2023-08-16 09:49:07 +01:00
a7aa923241 release: v2.14.1 2023-08-16 09:47:05 +01:00
e012517b16 chore: bumps phpunit 2023-08-16 09:46:51 +01:00
b1dd18af8a chore: style changes 2023-08-16 09:46:31 +01:00
398e3ff3b5 introducing toBeDigits 2023-08-14 17:10:58 -03:00
03648f580c docs: update changelog 2023-08-14 09:44:14 +01:00
df2212055b release: v2.14.0 2023-08-14 09:41:14 +01:00
b1a137c513 chore: updates snapshot tests 2023-08-14 09:41:05 +01:00
62267dfd3e Merge pull request #906 from JonPurvis/extra-expectations
add expectations for uppercase, lowercase, alpha and alphanumeric
2023-08-13 08:44:17 +00:00
f996a48dfa fix refacto check 2023-08-12 18:14:38 +01:00
54e00dd4dc add expectations for uppercase, lowercase, alpha and alphanumeric 2023-08-12 16:41:15 +01:00
f1414a0beb docs: changelog 2023-08-09 12:16:21 +01:00
47f2ae32c1 release: v2.13.0 2023-08-09 12:14:39 +01:00
306b7eb2a6 feat: adds ddWhen and ddUnless 2023-08-09 12:14:32 +01:00
02f72aabb2 Merge pull request #860 from devajmeireles/feature/add-dd-conditionally
Feature: Introducing The Ability to Dump Conditionally
2023-08-09 10:50:53 +00:00
e3a21384e6 release: v2.12.2 2023-08-07 10:29:25 +01:00
331381eed5 release: v2.12.1 2023-08-07 10:26:55 +01:00
75a7d77a80 Updates snapshots 2023-08-07 10:22:58 +01:00
cc242a50d1 chore: bump dependencies 2023-08-07 09:39:13 +01:00
704acbf6de Merge pull request #898 from dylanbr/allow_tests_to_be_extended
TestSuiteLoader will always consider classes from the current file
2023-08-06 22:59:52 +00:00
7baa48e068 TestSuiteLoader will always consider classes from the current file 2023-08-05 13:06:00 +02:00
3742e74adf feat: adds "phpunit/phpunit": "^10.3.1" support 2023-08-04 11:23:55 +01:00
cbcea04768 tests: update snapshots 2023-08-03 10:35:38 +01:00
f0581f87c6 Merge pull request #896 from fabio-ivona/windows-tests-fix
fix tests
2023-08-03 09:33:17 +00:00
60763f2223 fix tests 2023-08-03 11:27:01 +02:00
8a589022d9 release: v2.12.0 2023-08-02 23:04:35 +01:00
00109e9976 tests: adds more tests regarding snapshots multiple 2023-08-02 23:02:00 +01:00
43107c1743 chore: bumps phpunit 2023-08-02 23:01:36 +01:00
546253d591 Merge pull request #881 from fabio-ivona/snaphsots-cleanup
[2.x] Multiple snapshots
2023-08-02 21:43:56 +00:00
d94a6580f5 fix: type check 2023-08-02 20:49:27 +02:00
fb75b712d3 chore: update snapshot 2023-08-02 20:49:05 +02:00
6ead2a4e8b feat(sequence): Add support for nested traversable 2023-08-02 20:31:53 +02:00
9afe2296a6 fix line endings on Windows 2023-08-02 10:45:30 +02:00
0221c2a643 refactor 2023-08-01 17:18:55 +02:00
0518971d2f refactor 2023-08-01 17:16:50 +02:00
fe3747f850 lint 2023-08-01 17:15:44 +02:00
844d175981 refactor 2023-08-01 17:14:49 +02:00
4e719214c6 fix incomplete tests while updating snapshots 2023-08-01 17:13:15 +02:00
2f6b99885e Merge branch '2.x' into snaphsots-cleanup
# Conflicts:
#	src/Expectation.php
#	src/Expectations/OppositeExpectation.php
2023-08-01 17:04:11 +02:00
4b24da1a58 Merge pull request #892 from ash-jc-allen/comment-updates
Updated comments
2023-08-01 16:35:42 +02:00
72d482de28 docs: update changelog 2023-08-01 14:49:00 +01:00
049da041b2 release: v2.11.0 2023-08-01 14:43:50 +01:00
4d7aa2b98f Merge pull request #891 from ash-jc-allen/feature/invokable-arch-expectation
Add `toBeInvokable` arch expectation
2023-08-01 13:23:05 +00:00
2e352c0084 Updated comments. 2023-08-01 13:09:53 +01:00
3f854713e6 Style updates. 2023-08-01 12:12:41 +01:00
011bd3ba82 Added "toBeInvokable" arch expectation. 2023-08-01 12:09:18 +01:00
4de70da0a0 release: v2.10.1 2023-07-31 11:58:13 +01:00
6820d8b7aa Merge pull request #888 from pestphp/feat_opposite_suffix_prefix
feat(arch): Adds support for opposite expectations of `toHavePrefix` and `toHaveSuffix`
2023-07-31 10:48:58 +00:00
6886558ed1 feat(arch): Adds support for opposite expectations of toHavePrefix and toHaveSuffix. 2023-07-31 11:28:53 +01:00
b795a92840 docs: updates changelog 2023-07-31 00:11:24 +01:00
2e622f6fd4 chore: fixes type checkign 2023-07-31 00:06:36 +01:00
5f7a1663dd release: v2.10.0 2023-07-30 23:52:43 +01:00
f3f35a2ed1 feat: adds repeat 2023-07-30 23:49:20 +01:00
86a6b32715 fix: -v option 2023-07-30 23:49:11 +01:00
1efb9de043 multiple snapshots 2023-07-27 11:46:22 +02:00
b60d21dfe2 snapshots code cleanup 2023-07-27 11:16:27 +02:00
39e0d61dec phpstan fix 2023-07-27 11:10:42 +02:00
be41181b43 release: v2.9.5 2023-07-24 19:13:17 +01:00
632ffc2f8e fix: arch assertions counter 2023-07-24 19:13:09 +01:00
705f19dd87 release: v2.9.4 2023-07-22 09:42:37 -05:00
5637dfa75d fix: test description on beforeEach failure 2023-07-22 09:33:41 -05:00
cf5275293f fix: snapshots directory 2023-07-20 13:47:23 -05:00
81efe5953b release: v2.9.2 2023-07-20 13:35:03 -05:00
a37a3b9f99 fix: non-working beforeEach 2023-07-20 13:27:41 -05:00
9100913184 release: v2.9.1 2023-07-20 07:39:17 -05:00
8fdb0b3d32 chore: bump dependencies 2023-07-20 07:06:08 -05:00
8322ff0f5e release: v2.9.0 2023-07-19 10:28:49 -05:00
c8287567eb Merge branch 'develop' into 2.x 2023-07-19 10:23:58 -05:00
b00bc4d5ea applying enhancement to use single dd function 2023-07-17 19:11:06 -03:00
8abc0d1920 applying enhancement to use ddWhen inside ddUnless 2023-07-17 14:12:54 -03:00
ea967b439f Feature: Introducing The Ability to Dump Conditionally 2023-07-17 11:08:00 -03:00
23d7191990 Removes sponsor 2023-07-15 15:49:55 +01:00
c7e6df7c95 chore: coding style 2023-07-15 15:11:03 +01:00
21a04fefcf fix: same class on toExtend 2023-07-08 17:54:21 +01:00
aa4fb3bba2 chore: bumps arch plugin 2023-07-08 13:42:37 +01:00
4d0dffafd3 tests: adjusts snapshots 2023-07-01 10:42:57 +01:00
19e75d1070 chore: coding style 2023-07-01 10:31:26 +01:00
cee5b9feb9 fix: arguments keys 2023-07-01 10:31:26 +01:00
355a2349af feat: allows array formats 2023-07-01 10:31:26 +01:00
7e815cc985 tests: updates snapshots 2023-07-01 10:31:26 +01:00
fb443e0fa0 chore: fixes type checking 2023-07-01 10:31:26 +01:00
7f1135eeac chore: adjusts workflows 2023-07-01 10:31:26 +01:00
25e15e76e0 tests: updates snapshots 2023-07-01 10:31:26 +01:00
9426881cf6 fix: avoids usage of --update-snapshots in parallel 2023-07-01 10:31:26 +01:00
1f6970a5b3 fix: returns relative path from snapshot 2023-07-01 10:31:26 +01:00
e541ee86fc feat: adds "Illuminate\Testing\TestResponse" behavior to snapshot testing 2023-07-01 10:31:26 +01:00
b1c6f247e0 chore: uses snapshot testing in some visual testing 2023-07-01 10:31:26 +01:00
36b585835d feat: adds snapshot testing 2023-07-01 10:31:26 +01:00
17db4bd616 chore: missing properties 2023-07-01 10:31:26 +01:00
c98d8ca26a feat: more expectations 2023-07-01 10:31:26 +01:00
d5334f96a4 chore: increase deps 2023-07-01 10:31:26 +01:00
54f4ee57ad refacto: 100% type coverage 2023-07-01 10:31:26 +01:00
4f3796ed2e feat: improves VS Code auto-complete 2023-07-01 10:31:26 +01:00
ac13a288fb feat: improve grammar 2023-07-01 10:31:26 +01:00
e2ccc9deac chore: style changes 2023-07-01 10:31:26 +01:00
80129f2e23 chore: asserts style 2023-07-01 10:31:26 +01:00
5802bbc1dd feat: toHavePrefix, toHaveSuffix, toOnlyImplement, toImplementNothing 2023-07-01 10:31:26 +01:00
ee2f4eedbd feat: more reflection based expectations 2023-07-01 10:31:26 +01:00
0de1ce053a feat: toBeFinal 2023-07-01 10:31:26 +01:00
be9056f978 feat: toUseStrictTypes 2023-07-01 10:31:26 +01:00
26a6e7d712 More tests 2023-07-01 10:31:26 +01:00
a90b90ad29 docs: package description 2023-07-01 10:31:26 +01:00
bc951787d3 feat(describe): snapshots 2023-07-01 10:31:26 +01:00
0ae0887665 feat(describe): more refactors 2023-07-01 10:31:26 +01:00
551fa01415 feat(describe): more refactor 2023-07-01 10:31:26 +01:00
68ea2c7d7e feat(describe): refactor 2023-07-01 10:31:26 +01:00
3e8616ec64 feat(describe): continues work around hooks 2023-07-01 10:31:26 +01:00
465c65243d feat(describe): improves logic around hooks 2023-07-01 10:31:26 +01:00
9c0e5ddfc6 feat(describe): adds missing beforeAll exception class 2023-07-01 10:31:26 +01:00
8442b9a6e4 feat(describe): fixes missing beforeAll exception 2023-07-01 10:31:26 +01:00
297 changed files with 5539 additions and 1162 deletions

66
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,66 @@
name: Bug Report
description: Report an Issue or Bug with the Pest
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
We're sorry to hear you have a problem. Can you help us solve it by providing the following details.
- type: textarea
id: what-happened
attributes:
label: What Happened
description: What did you expect to happen?
placeholder: When I use expect()->toBeTrue() in my tests, I get an error
validations:
required: true
- type: textarea
id: how-to-reproduce
attributes:
label: How to Reproduce
description: How did this occur, please add any config values used and provide a set of reliable steps if possible.
placeholder: Install a fresh Laravel app, add Pest, add a test that uses expect()->toBeTrue()
validations:
required: true
- type: input
id: repository-sample
attributes:
label: Sample Repository
description: If possible, please provide a sample repository that reproduces the issue.
placeholder: https://github.com.br/your-username/your-repository
- type: input
id: pest-version
attributes:
label: Pest Version
description: What version of our Package are you running? Please be as specific as possible
placeholder: 2.14.1
validations:
required: true
- type: input
id: php-version
attributes:
label: PHP Version
description: What version of PHP are you running? Please be as specific as possible
placeholder: 8.1.20
validations:
required: true
- type: dropdown
id: operating-systems
attributes:
label: Operation System
description: On which operating systems does the problem occur? You can select more than one.
multiple: true
options:
- macOS
- Windows
- Linux
validations:
required: true
- type: textarea
id: notes
attributes:
label: Notes
description: Use this field to provide any other notes that you feel might be relevant to the issue.
validations:
required: false

View File

@ -1,10 +1,16 @@
| Q | A
| ------------- | ---
| Bug fix? | yes/no
| New feature? | yes/no
| Fixed tickets | #... <!-- #-prefixed issue number(s), if any -->
<!-- <!--
- Replace this comment by a description of what your PR is solving. - Fill in the form below correctly. This will help the Pest team to understand the PR and also work on it.
--> -->
### What:
- [ ] Bug Fix
- [ ] New Feature
### Description:
<!-- describe what your PR is solving -->
### Related:
<!-- link to the issue(s) your PR is solving. If it doesn't exist, remove the "Related" section. -->

View File

@ -1,42 +0,0 @@
name: Integration Tests
on:
push:
schedule:
- cron: '0 0 * * *'
jobs:
ci:
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
php: ['8.1', '8.2']
dependency-version: [prefer-lowest, prefer-stable]
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
coverage: none
- name: Setup Problem Matches
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install PHP dependencies
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
- name: Integration Tests
run: composer test:integration

View File

@ -13,12 +13,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true
matrix: matrix:
dependency-version: [prefer-lowest, prefer-stable] dependency-version: [prefer-lowest, prefer-stable]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
@ -30,8 +31,11 @@ jobs:
- name: Install Dependencies - name: Install Dependencies
run: composer update --prefer-stable --no-interaction --no-progress --ansi run: composer update --prefer-stable --no-interaction --no-progress --ansi
- name: Types - name: Type Check
run: composer test:types run: composer test:type:check
- name: Type Coverage
run: composer test:type:coverage
- name: Refacto - name: Refacto
run: composer test:refacto run: composer test:refacto

View File

@ -3,25 +3,28 @@ name: Tests
on: on:
push: push:
pull_request: pull_request:
schedule:
- cron: '0 0 * * *'
jobs: jobs:
ci: tests:
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest' if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: true
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
php: ['8.1', '8.2'] symfony: ['6.4', '7.0']
dependency-version: [prefer-lowest, prefer-stable] php: ['8.1', '8.2', '8.3']
dependency_version: [prefer-lowest, prefer-stable]
exclude:
- php: '8.1'
symfony: '7.0'
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
@ -36,11 +39,13 @@ jobs:
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install PHP dependencies - name: Install PHP dependencies
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:~${{ matrix.symfony }}"
- name: Unit Tests - name: Unit Tests
run: composer test:unit run: composer test:unit
- name: Unit Tests in Parallel - name: Parallel Tests
run: composer test:parallel run: composer test:parallel
- name: Integration Tests
run: composer test:integration

View File

@ -1,136 +0,0 @@
# Release Notes for 2.x
## Unreleased
## [v2.8.1 (2023-06-20)](https://github.com/pestphp/pest/compare/v2.8.0...v2.8.1)
### Fixed
- Fixes "Cannot find TestCase object on call stack" ([eb7bb34](https://github.com/pestphp/pest/commit/eb7bb348253f412e806a6ba6f0df46c0435d0dfe))
## [v2.8.0 (2023-06-19)](https://github.com/pestphp/pest/compare/v2.7.0...v2.8.0)
### Added
- Support for `globs` in `uses` ([#829](https://github.com/pestphp/pest/pull/829))
## [v2.7.0 (2023-06-15)](https://github.com/pestphp/pest/compare/v2.6.3...v2.7.0)
### Added
- Support for unexpected output on printer ([eb9f31e](https://github.com/pestphp/pest/commit/eb9f31edeb00a88c449874f3d48156128a00fff8))
### Chore
- Bumps PHPUnit to `^10.2.2` ([0e5470b](https://github.com/pestphp/pest/commit/0e5470b192b259ba2db7c02a50371216c98fc0a6))
## [v2.6.3 (2023-06-07)](https://github.com/pestphp/pest/compare/v2.6.2...v2.6.3)
### Chore
- Bumps PHPUnit to `^10.2.1` ([73a859e](https://github.com/pestphp/pest/commit/73a859ee563fe96944ba39b191dceca28ef703c2))
## [v2.6.2 (2023-06-02)](https://github.com/pestphp/pest/compare/v2.6.1...v2.6.2)
### Chore
- Bumps PHPUnit to `^10.2.0` ([a0041f1](https://github.com/pestphp/pest/commit/a0041f139cba94fe5d15318c38e275f2e2fb3350))
## [v2.6.1 (2023-04-12)](https://github.com/pestphp/pest/compare/v2.6.0...v2.6.1)
### Fixes
- PHPStorm issue output problem for tests throwing an exception before the first assertion ([#809](https://github.com/pestphp/pest/pull/809))
- Allow traits to be covered ([#804](https://github.com/pestphp/pest/pull/804))
### Chore
- Bumps PHPUnit to `^10.1.3` ([c993252](https://github.com/pestphp/pest/commit/c99325275acf1fd3759b487b93ec50473f706709))
## [v2.6.0 (2023-04-05)](https://github.com/pestphp/pest/compare/v2.5.2...v2.6.0)
### Adds
- Allows `toThrow` to be used against an exception instance ([#797](https://github.com/pestphp/pest/pull/797))
## [v2.5.2 (2023-04-19)](https://github.com/pestphp/pest/compare/v2.5.1...v2.5.2)
### Chore
- Removes `myclabs/php-enuma` dependency ([1a05df1](https://github.com/pestphp/pest/commit/1a05df14d0ce7d12583df26ff716807db6f81f13))
## [v2.5.1 (2023-04-18)](https://github.com/pestphp/pest/compare/v2.5.0...v2.5.1)
### Chore
- Bumps PHPUnit to `^10.1.1` ([ec6a817](https://github.com/pestphp/pest/commit/ec6a81735af19f5463d24545df97535d77697ec6))
## [v2.5.0 (2023-04-14)](https://github.com/pestphp/pest/compare/v2.4.0...v2.5.0)
### Chore
- Bumps PHPUnit to `^10.1.0` ([#780](https://github.com/pestphp/pest/pull/780))
## [v2.4.0 (2023-04-03)](https://github.com/pestphp/pest/compare/v2.3.0...v2.4.0)
### Added
- `skipOnWindows()`, `skipOnMac()`, and `skipOnLinux()` ([#757](https://github.com/pestphp/pest/pull/757))
- source architecture testing violation ([#1](https://github.com/pestphp/pest-plugin-arch/pull/1))([8e66263](https://github.com/pestphp/pest-plugin-arch/commit/8e66263104304e99e3d6ceda25c7ed679b27fb03))
- `toHaveProperties` may now also check values ([#760](https://github.com/pestphp/pest/pull/760))
### Fixed
- Tests on `tests/Helpers` directory not being executed ([#753](https://github.com/pestphp/pest/pull/753))
- Teamcity count ([#747](https://github.com/pestphp/pest/pull/747))
- Parallel execution when class extends class with same name ([#748](https://github.com/pestphp/pest/pull/748))
- Wording on `uses()` hint ([#745](https://github.com/pestphp/pest/pull/745/files))
## [v2.3.0 (2023-03-28)](https://github.com/pestphp/pest/compare/v2.2.3...v2.3.0)
### Added
- Better error handler about missing uses ([#743](https://github.com/pestphp/pest/pull/743))
### Fixed
- Inconsistent spelling of `dataset` ([#739](https://github.com/pestphp/pest/pull/739))
### Chore
- Bumps PHPUnit to `^10.0.19` ([3d7e621](https://github.com/pestphp/pest/commit/3d7e621b7dfc03f0b2d9dcf6eb06c26bc383f502))
## [v2.2.3 (2023-03-24)](https://github.com/pestphp/pest/compare/v2.2.2...v2.2.3)
### Fixed
- Unnecessary dataset on dataset arguments mismatch ([#736](https://github.com/pestphp/pest/pull/736))
- Parallel arguments on plugins order ([#703](https://github.com/pestphp/pest/pull/703))
- Arch plugin runtime exceptions on bad phpdocs ([2f2b51c](https://github.com/pestphp/pest/commit/2f2b51ce3d1b000be9d6add0e785fd0044931b3b))
## [v2.2.2 (2023-03-23)](https://github.com/pestphp/pest/compare/v2.2.1...v2.2.2)
### Fixed
- Edge case in parallel execution test description ([3ce6408](https://github.com/pestphp/pest/commit/3ce640819541ca6022b250e000f336d87c3e7889))
## [v2.2.1 (2023-03-22)](https://github.com/pestphp/pest/compare/v2.2.0...v2.2.1)
### Fixed
- Collision between tests names with underscores ([#724](https://github.com/pestphp/pest/pull/724))
### Chore
- Bumps PHPUnit to `^10.0.18` ([1408cff](https://github.com/pestphp/pest/commit/1408cffc028690057e44f00038f9390f776e6bfb))
## [v2.2.0 (2023-03-22)](https://github.com/pestphp/pest/compare/v2.1.0...v2.2.0)
### Added
- Improved error messages on dataset arguments mismatch ([#698](https://github.com/pestphp/pest/pull/698))
- Allows the usage of `DateTimeInterface` on multiple expectations ([#716](https://github.com/pestphp/pest/pull/716))
### Fixed
- `--dirty` option on Windows environments ([#721](https://github.com/pestphp/pest/pull/721))
- Parallel exit code when `phpunit.xml` is outdated ([14dd5cb](https://github.com/pestphp/pest/commit/14dd5cb57b9432300ac4e8095f069941cb43bdb5))
## [v2.1.0 (2023-03-21)](https://github.com/pestphp/pest/compare/v2.0.2...v2.1.0)
### Added
- `only` test case method ([bcd1503](https://github.com/pestphp/pest/commit/bcd1503cade938853a55c1283b02b6b820ea0b69))
### Fixed
- Issues with different characters on test names ([715](https://github.com/pestphp/pest/pull/715))
## [v2.0.2 (2023-03-20)](https://github.com/pestphp/pest/compare/v2.0.1...v2.0.2)
### Fixed
- `Pest.php` not being loaded in certain scenarios ([b887116](https://github.com/pestphp/pest/commit/b887116e5ce9a69403ad620cad20f0a029474eb5))
## [v2.0.1 (2023-03-20)](https://github.com/pestphp/pest/compare/v2.0.0...v2.0.1)
### Fixed
- Wrong `version` configuration key on `composer.json` ([8f91f40](https://github.com/pestphp/pest/commit/8f91f40e8ea8b35e04b7989bed6a8f9439e2a2d6))
## [v2.0.0 (2023-03-20)](https://github.com/pestphp/pest/compare/v1.22.6...v2.0.0)
Please consult the [upgrade guide](https://pestphp.com/docs/upgrade-guide) and [release notes](https://pestphp.com/docs/announcing-pest2) in the official Pest documentation.

View File

@ -21,20 +21,20 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
### Platinum Sponsors ### Platinum Sponsors
- **[LaraJobs](https://larajobs.com)**
- **[Brokerchooser](https://brokerchooser.com)**
- **[Forge](https://forge.laravel.com)** - **[Forge](https://forge.laravel.com)**
- **[LoadForge](https://loadforge.com)**
- **[Spatie](https://spatie.be)** - **[Spatie](https://spatie.be)**
- **[Worksome](https://www.worksome.com/)** - **[Worksome](https://www.worksome.com/)**
### Premium Sponsors ### Premium Sponsors
- [Akaunting](https://akaunting.com) - [Akaunting](https://akaunting.com/?ref=pestphp)
- [Codecourse](https://codecourse.com/) - [Codecourse](https://codecourse.com/?ref=pestphp)
- [Laracasts](https://laracasts.com/) - [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
- [Localazy](https://localazy.com) - [Laracasts](https://laracasts.com/?ref=pestphp)
- [Hyvor](https://hyvor.com/) - [Localazy](https://localazy.com/?ref=pestphp)
- [Fathom Analytics](https://usefathom.com/) - [Route4Me](https://www.route4me.com/?ref=pestphp)
- [Meema](https://meema.io) - [Zapiet](https://www.zapiet.com/?ref=pestphp)
- [Zapiet](https://www.zapiet.com)
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.

View File

@ -5,10 +5,10 @@ When releasing a new version of Pest there are some checks and updates that need
> **For Pest v1 you should use the `1.x` branch instead.** > **For Pest v1 you should use the `1.x` branch instead.**
- Clear your local repository with: `git add . && git reset --hard && git checkout 2.x` - Clear your local repository with: `git add . && git reset --hard && git checkout 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}...master) and update the [changelog](CHANGELOG.md) file with the main changes for this release - On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...2.x](https://github.com/pestphp/pest/compare/{latest_version}...2.x)
- Update the version number in [src/Pest.php](src/Pest.php) - Update the version number in [src/Pest.php](src/Pest.php)
- Run the tests locally using: `composer test` - Run the tests locally using: `composer test`
- Commit the CHANGELOG and Pest file with the message: `git commit -m "release: vX.X.X"` - Commit the Pest file with the message: `git commit -m "release: vX.X.X"`
- Push the changes to GitHub - Push the changes to GitHub
- Check that the CI is passing as expected: [github.com/pestphp/pest/actions](https://github.com/pestphp/pest/actions) - Check that the CI is passing as expected: [github.com/pestphp/pest/actions](https://github.com/pestphp/pest/actions)
- Tag and push the tag with `git tag vX.X.X && git push --tags` - Tag and push the tag with `git tag vX.X.X && git push --tags`

View File

@ -13,39 +13,39 @@ use Symfony\Component\Console\Output\ConsoleOutput;
// Ensures Collision's Printer is registered. // Ensures Collision's Printer is registered.
$_SERVER['COLLISION_PRINTER'] = 'DefaultPrinter'; $_SERVER['COLLISION_PRINTER'] = 'DefaultPrinter';
$args = $_SERVER['argv']; $arguments = $originalArguments = $_SERVER['argv'];
$dirty = false; $dirty = false;
$todo = false; $todo = false;
foreach ($args as $key => $value) { foreach ($arguments as $key => $value) {
if ($value === '--compact') { if ($value === '--compact') {
$_SERVER['COLLISION_PRINTER_COMPACT'] = 'true'; $_SERVER['COLLISION_PRINTER_COMPACT'] = 'true';
unset($args[$key]); unset($arguments[$key]);
} }
if ($value === '--profile') { if ($value === '--profile') {
$_SERVER['COLLISION_PRINTER_PROFILE'] = 'true'; $_SERVER['COLLISION_PRINTER_PROFILE'] = 'true';
unset($args[$key]); unset($arguments[$key]);
} }
if (str_contains($value, '--test-directory')) { if (str_contains($value, '--test-directory')) {
unset($args[$key]); unset($arguments[$key]);
} }
if ($value === '--dirty') { if ($value === '--dirty') {
$dirty = true; $dirty = true;
unset($args[$key]); unset($arguments[$key]);
} }
if ($value === '--todos') { if (in_array($value, ['--todo', '--todos'], true)) {
$todo = true; $todo = true;
unset($args[$key]); unset($arguments[$key]);
} }
if (str_contains($value, '--teamcity')) { if (str_contains($value, '--teamcity')) {
unset($args[$key]); unset($arguments[$key]);
$args[] = '--no-output'; $arguments[] = '--no-output';
unset($_SERVER['COLLISION_PRINTER']); unset($_SERVER['COLLISION_PRINTER']);
} }
} }
@ -88,9 +88,9 @@ use Symfony\Component\Console\Output\ConsoleOutput;
try { try {
$kernel = Kernel::boot($testSuite, $input, $output); $kernel = Kernel::boot($testSuite, $input, $output);
$result = $kernel->handle($args); $result = $kernel->handle($originalArguments, $arguments);
$kernel->shutdown(); $kernel->terminate();
} catch (Throwable|Error $e) { } catch (Throwable|Error $e) {
Panic::with($e); Panic::with($e);
} }

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "pestphp/pest", "name": "pestphp/pest",
"description": "An elegant PHP Testing Framework.", "description": "The elegant PHP Testing Framework.",
"keywords": [ "keywords": [
"php", "php",
"framework", "framework",
@ -18,16 +18,17 @@
], ],
"require": { "require": {
"php": "^8.1.0", "php": "^8.1.0",
"brianium/paratest": "^7.2.2", "brianium/paratest": "^7.3.1",
"nunomaduro/collision": "^7.7.0", "nunomaduro/collision": "^7.10.0|^8.4.0",
"nunomaduro/termwind": "^1.15.1", "nunomaduro/termwind": "^1.15.1|^2.0.1",
"pestphp/pest-plugin": "^2.0.1", "pestphp/pest-plugin": "^2.1.1",
"pestphp/pest-plugin-arch": "^2.2.2", "pestphp/pest-plugin-arch": "^2.7.0",
"phpunit/phpunit": "^10.2.3" "phpunit/phpunit": "^10.5.17"
}, },
"conflict": { "conflict": {
"webmozart/assert": "<1.11.0", "phpunit/phpunit": ">10.5.17",
"phpunit/phpunit": ">10.2.3" "sebastian/exporter": "<5.1.0",
"webmozart/assert": "<1.11.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -42,6 +43,7 @@
"psr-4": { "psr-4": {
"Tests\\Fixtures\\Covers\\": "tests/Fixtures/Covers", "Tests\\Fixtures\\Covers\\": "tests/Fixtures/Covers",
"Tests\\Fixtures\\Inheritance\\": "tests/Fixtures/Inheritance", "Tests\\Fixtures\\Inheritance\\": "tests/Fixtures/Inheritance",
"Tests\\Fixtures\\Arch\\": "tests/Fixtures/Arch",
"Tests\\": "tests/PHPUnit/" "Tests\\": "tests/PHPUnit/"
}, },
"files": [ "files": [
@ -49,10 +51,12 @@
] ]
}, },
"require-dev": { "require-dev": {
"pestphp/pest-dev-tools": "^2.12.0", "pestphp/pest-dev-tools": "^2.16.0",
"symfony/process": "^6.3.0" "pestphp/pest-plugin-type-coverage": "^2.8.5",
"symfony/process": "^6.4.0|^7.1.3"
}, },
"minimum-stability": "stable", "minimum-stability": "dev",
"prefer-stable": true,
"config": { "config": {
"sort-packages": true, "sort-packages": true,
"preferred-install": "dist", "preferred-install": "dist",
@ -68,16 +72,18 @@
"lint": "pint", "lint": "pint",
"test:refacto": "rector --dry-run", "test:refacto": "rector --dry-run",
"test:lint": "pint --test", "test:lint": "pint --test",
"test:types": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
"test:type:coverage": "php bin/pest --type-coverage --min=100",
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact", "test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml", "test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
"test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=10", "test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3",
"test:integration": "php bin/pest --colors=always --group=integration", "test:integration": "php bin/pest --colors=always --group=integration",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
"test": [ "test": [
"@test:refacto", "@test:refacto",
"@test:lint", "@test:lint",
"@test:types", "@test:type:check",
"@test:type:coverage",
"@test:unit", "@test:unit",
"@test:parallel", "@test:parallel",
"@test:integration" "@test:integration"
@ -98,9 +104,16 @@
"Pest\\Plugins\\ProcessIsolation", "Pest\\Plugins\\ProcessIsolation",
"Pest\\Plugins\\Profile", "Pest\\Plugins\\Profile",
"Pest\\Plugins\\Retry", "Pest\\Plugins\\Retry",
"Pest\\Plugins\\Snapshot",
"Pest\\Plugins\\Verbose",
"Pest\\Plugins\\Version", "Pest\\Plugins\\Version",
"Pest\\Plugins\\Parallel" "Pest\\Plugins\\Parallel"
] ]
},
"phpstan": {
"includes": [
"extension.neon"
]
} }
} }
} }

View File

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

4
extension.neon Normal file
View File

@ -0,0 +1,4 @@
parameters:
universalObjectCratesClasses:
- Pest\Support\HigherOrderTapProxy
- Pest\Expectation

View File

@ -0,0 +1,459 @@
<?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 TestMarkedIncompleteSubscriber($this),
new TestSkippedSubscriber($this),
new TestRunnerExecutionFinishedSubscriber($this),
);
}
private function createDocument(): void
{
$this->document = new DOMDocument('1.0', 'UTF-8');
$this->document->formatOutput = true;
$this->root = $this->document->createElement('testsuites');
$this->document->appendChild($this->root);
}
/**
* @throws InvalidArgumentException
*/
private function handleFault(Errored|Failed $event, string $type): void
{
if (! $this->prepared) {
$this->createTestCase($event);
}
assert($this->currentTestCase !== null);
$buffer = $this->converter->getTestCaseMethodName($event->test()); // 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

@ -34,17 +34,18 @@
namespace PHPUnit\Runner\Filter; namespace PHPUnit\Runner\Filter;
use function end;
use Exception; use Exception;
use function implode;
use Pest\Contracts\HasPrintableTestCaseName; use Pest\Contracts\HasPrintableTestCaseName;
use PHPUnit\Framework\SelfDescribing; use PHPUnit\Framework\SelfDescribing;
use PHPUnit\Framework\Test; use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\TestSuite;
use function preg_match;
use RecursiveFilterIterator; use RecursiveFilterIterator;
use RecursiveIterator; use RecursiveIterator;
use function end;
use function implode;
use function preg_match;
use function sprintf; use function sprintf;
use function str_replace; use function str_replace;

View File

@ -45,22 +45,23 @@ declare(strict_types=1);
namespace PHPUnit\Runner\ResultCache; namespace PHPUnit\Runner\ResultCache;
use const DIRECTORY_SEPARATOR;
use PHPUnit\Framework\TestStatus\TestStatus;
use PHPUnit\Runner\DirectoryCannotBeCreatedException;
use PHPUnit\Runner\Exception;
use PHPUnit\Util\Filesystem;
use function array_keys; use function array_keys;
use function assert; use function assert;
use const DIRECTORY_SEPARATOR;
use function dirname; use function dirname;
use function file_get_contents; use function file_get_contents;
use function file_put_contents; use function file_put_contents;
use function is_array; use function is_array;
use function is_dir; use function is_dir;
use function is_file;
use function json_decode; use function json_decode;
use function json_encode; use function json_encode;
use function Pest\version; use function Pest\version;
use PHPUnit\Framework\TestStatus\TestStatus;
use PHPUnit\Runner\DirectoryCannotBeCreatedException;
use PHPUnit\Runner\Exception;
use PHPUnit\Util\Filesystem;
/** /**
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
@ -89,7 +90,7 @@ final class DefaultResultCache implements ResultCache
*/ */
private array $times = []; private array $times = [];
public function __construct(string $filepath = null) public function __construct(?string $filepath = null)
{ {
if ($filepath !== null && is_dir($filepath)) { if ($filepath !== null && is_dir($filepath)) {
$filepath .= DIRECTORY_SEPARATOR.self::DEFAULT_RESULT_CACHE_FILENAME; $filepath .= DIRECTORY_SEPARATOR.self::DEFAULT_RESULT_CACHE_FILENAME;
@ -127,13 +128,15 @@ final class DefaultResultCache implements ResultCache
public function load(): void public function load(): void
{ {
if (! is_file($this->cacheFilename)) { $contents = @file_get_contents($this->cacheFilename);
if ($contents === false) {
return; return;
} }
$data = json_decode( $data = json_decode(
file_get_contents($this->cacheFilename), $contents,
true true,
); );
if ($data === null) { if ($data === null) {

View File

@ -36,18 +36,19 @@ declare(strict_types=1);
namespace PHPUnit\Runner; namespace PHPUnit\Runner;
use function array_diff;
use function array_values;
use function basename;
use function class_exists;
use Exception; use Exception;
use function get_declared_classes;
use Pest\Contracts\HasPrintableTestCaseName; use Pest\Contracts\HasPrintableTestCaseName;
use Pest\TestCases\IgnorableTestCase; use Pest\TestCases\IgnorableTestCase;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use ReflectionClass; use ReflectionClass;
use ReflectionException; use ReflectionException;
use function array_diff;
use function array_values;
use function basename;
use function class_exists;
use function get_declared_classes;
use function substr; use function substr;
/** /**
@ -60,6 +61,11 @@ final class TestSuiteLoader
*/ */
private static array $loadedClasses = []; private static array $loadedClasses = [];
/**
* @psalm-var array<string, array<class-string>>
*/
private static array $loadedClassesByFilename = [];
/** /**
* @psalm-var list<class-string> * @psalm-var list<class-string>
*/ */
@ -97,6 +103,17 @@ final class TestSuiteLoader
self::$loadedClasses = array_merge($loadedClasses, self::$loadedClasses); self::$loadedClasses = array_merge($loadedClasses, self::$loadedClasses);
foreach ($loadedClasses as $loadedClass) {
$reflection = new ReflectionClass($loadedClass);
$filename = $reflection->getFileName();
self::$loadedClassesByFilename[$filename] = [
$loadedClass,
...self::$loadedClassesByFilename[$filename] ?? [],
];
}
$loadedClasses = array_merge(self::$loadedClassesByFilename[$suiteClassFile] ?? [], $loadedClasses);
if (empty($loadedClasses)) { if (empty($loadedClasses)) {
return $this->exceptionFor($suiteClassName, $suiteClassFile); return $this->exceptionFor($suiteClassName, $suiteClassFile);
} }
@ -115,7 +132,7 @@ final class TestSuiteLoader
continue; continue;
} }
if ($class->isAbstract() || ($class->getFileName() !== $suiteClassFile)) { if ($class->isAbstract() || ($suiteClassFile !== $class->getFileName())) {
if (! str_contains($class->getFileName(), 'TestCaseFactory.php')) { if (! str_contains($class->getFileName(), 'TestCaseFactory.php')) {
continue; continue;
} }

View File

@ -45,7 +45,6 @@ declare(strict_types=1);
namespace PHPUnit\TextUI; namespace PHPUnit\TextUI;
use function array_map;
use Pest\Plugins\Only; use Pest\Plugins\Only;
use PHPUnit\Event; use PHPUnit\Event;
use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\TestSuite;
@ -53,24 +52,21 @@ use PHPUnit\Runner\Filter\Factory;
use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Configuration\Configuration;
use PHPUnit\TextUI\Configuration\FilterNotConfiguredException; use PHPUnit\TextUI\Configuration\FilterNotConfiguredException;
use function array_map;
/** /**
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
final class TestSuiteFilterProcessor final class TestSuiteFilterProcessor
{ {
private Factory $filterFactory;
public function __construct(Factory $factory = new Factory)
{
$this->filterFactory = $factory;
}
/** /**
* @throws Event\RuntimeException * @throws Event\RuntimeException
* @throws FilterNotConfiguredException * @throws FilterNotConfiguredException
*/ */
public function process(Configuration $configuration, TestSuite $suite): void public function process(Configuration $configuration, TestSuite $suite): void
{ {
$factory = new Factory;
if (! $configuration->hasFilter() && if (! $configuration->hasFilter() &&
! $configuration->hasGroups() && ! $configuration->hasGroups() &&
! $configuration->hasExcludeGroups() && ! $configuration->hasExcludeGroups() &&
@ -82,21 +78,21 @@ final class TestSuiteFilterProcessor
} }
if ($configuration->hasExcludeGroups()) { if ($configuration->hasExcludeGroups()) {
$this->filterFactory->addExcludeGroupFilter( $factory->addExcludeGroupFilter(
$configuration->excludeGroups() $configuration->excludeGroups()
); );
} }
if (Only::isEnabled()) { if (Only::isEnabled()) {
$this->filterFactory->addIncludeGroupFilter(['__pest_only']); $factory->addIncludeGroupFilter(['__pest_only']);
} elseif ($configuration->hasGroups()) { } elseif ($configuration->hasGroups()) {
$this->filterFactory->addIncludeGroupFilter( $factory->addIncludeGroupFilter(
$configuration->groups() $configuration->groups()
); );
} }
if ($configuration->hasTestsCovering()) { if ($configuration->hasTestsCovering()) {
$this->filterFactory->addIncludeGroupFilter( $factory->addIncludeGroupFilter(
array_map( array_map(
static fn (string $name): string => '__phpunit_covers_'.$name, static fn (string $name): string => '__phpunit_covers_'.$name,
$configuration->testsCovering() $configuration->testsCovering()
@ -105,7 +101,7 @@ final class TestSuiteFilterProcessor
} }
if ($configuration->hasTestsUsing()) { if ($configuration->hasTestsUsing()) {
$this->filterFactory->addIncludeGroupFilter( $factory->addIncludeGroupFilter(
array_map( array_map(
static fn (string $name): string => '__phpunit_uses_'.$name, static fn (string $name): string => '__phpunit_uses_'.$name,
$configuration->testsUsing() $configuration->testsUsing()
@ -114,12 +110,12 @@ final class TestSuiteFilterProcessor
} }
if ($configuration->hasFilter()) { if ($configuration->hasFilter()) {
$this->filterFactory->addNameFilter( $factory->addNameFilter(
$configuration->filter() $configuration->filter()
); );
} }
$suite->injectFilter($this->filterFactory); $suite->injectFilter($factory);
Event\Facade::emitter()->testSuiteFiltered( Event\Facade::emitter()->testSuiteFiltered(
Event\TestSuite\TestSuiteBuilder::from($suite) Event\TestSuite\TestSuiteBuilder::from($suite)

View File

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

View File

@ -7,12 +7,13 @@ namespace Pest\Bootstrappers;
use Pest\Contracts\Bootstrapper; use Pest\Contracts\Bootstrapper;
use Pest\Support\DatasetInfo; use Pest\Support\DatasetInfo;
use Pest\Support\Str; use Pest\Support\Str;
use function Pest\testDirectory;
use Pest\TestSuite; use Pest\TestSuite;
use RecursiveDirectoryIterator; use RecursiveDirectoryIterator;
use RecursiveIteratorIterator; use RecursiveIteratorIterator;
use SebastianBergmann\FileIterator\Facade as PhpUnitFileIterator; use SebastianBergmann\FileIterator\Facade as PhpUnitFileIterator;
use function Pest\testDirectory;
/** /**
* @internal * @internal
*/ */
@ -77,6 +78,8 @@ final class BootFiles implements Bootstrapper
private function bootDatasets(string $testsPath): void private function bootDatasets(string $testsPath): void
{ {
assert(strlen($testsPath) > 0);
$files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php'); $files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');
foreach ($files as $file) { foreach ($files as $file) {

View File

@ -15,16 +15,17 @@ final class BootOverrides implements Bootstrapper
/** /**
* The list of files to be overridden. * The list of files to be overridden.
* *
* @var array<int, string> * @var array<string, string>
*/ */
private const FILES = [ public const FILES = [
'Runner/Filter/NameFilterIterator.php', 'c7b9c8a96006dea314204a8f09a8764e51ce0b9b79aadd58da52e8c328db4870' => 'Runner/Filter/NameFilterIterator.php',
'Runner/ResultCache/DefaultResultCache.php', 'c7c09ab7c9378710b27f761a4b2948196cbbdf2a73e4389bcdca1e7c94fa9c21' => 'Runner/ResultCache/DefaultResultCache.php',
'Runner/TestSuiteLoader.php', 'bc8718c89264f65800beabc23e51c6d3bcff87dfc764a12179ef5dbfde272c8b' => 'Runner/TestSuiteLoader.php',
'TextUI/Command/WarmCodeCoverageCacheCommand.php', 'f41e48d6cb546772a7de4f8e66b6b7ce894a5318d063eb52e354d206e96c701c' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
'TextUI/Output/Default/ProgressPrinter/TestSkippedSubscriber.php', 'cb7519f2d82893640b694492cf7ec9528da80773cc1d259634181b5d393528b5' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
'TextUI/TestSuiteFilterProcessor.php', '2f06e4b1a9f3a24145bfc7ea25df4f124117f940a2cde30a04d04d5678006bff' => 'TextUI/TestSuiteFilterProcessor.php',
'Event/Value/ThrowableBuilder.php', 'ef64a657ed9c0067791483784944107827bf227c7e3200f212b6751876b99e25' => 'Event/Value/ThrowableBuilder.php',
'c78f96e34b98ed01dd8106539d59b8aa8d67f733274118b827c01c5c4111c033' => 'Logging/JUnit/JunitXmlLogger.php',
]; ];
/** /**

View File

@ -32,8 +32,7 @@ final class BootSubscribers implements Bootstrapper
*/ */
public function __construct( public function __construct(
private readonly Container $container, private readonly Container $container,
) { ) {}
}
/** /**
* Boots the list of Subscribers. * Boots the list of Subscribers.

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> * @return array<int, Closure>
*/ */

View File

@ -23,40 +23,50 @@ use Throwable;
trait Testable trait Testable
{ {
/** /**
* Test method description. * The test's description.
*/ */
private string $__description; private string $__description;
/** /**
* Test "latest" method description. * The test's latest description.
*/ */
private static string $__latestDescription; private static string $__latestDescription;
/** /**
* The Test Case "test" closure. * The test's describing, if any.
*/
public ?string $__describing = null;
/**
* The test's test closure.
*/ */
private Closure $__test; private Closure $__test;
/** /**
* The Test Case "setUp" closure. * The test's before each closure.
*/ */
private ?Closure $__beforeEach = null; private ?Closure $__beforeEach = null;
/** /**
* The Test Case "tearDown" closure. * The test's after each closure.
*/ */
private ?Closure $__afterEach = null; private ?Closure $__afterEach = null;
/** /**
* The Test Case "setUpBeforeClass" closure. * The test's before all closure.
*/ */
private static ?Closure $__beforeAll = null; private static ?Closure $__beforeAll = null;
/** /**
* The test "tearDownAfterClass" closure. * The test's after all closure.
*/ */
private static ?Closure $__afterAll = null; private static ?Closure $__afterAll = null;
/**
* The list of snapshot changes, if any.
*/
private array $__snapshotChanges = [];
/** /**
* Resets the test case static properties. * Resets the test case static properties.
*/ */
@ -78,6 +88,7 @@ trait Testable
if ($test->hasMethod($name)) { if ($test->hasMethod($name)) {
$method = $test->getMethod($name); $method = $test->getMethod($name);
$this->__description = self::$__latestDescription = $method->description; $this->__description = self::$__latestDescription = $method->description;
$this->__describing = $method->describing;
$this->__test = $method->getClosure($this); $this->__test = $method->getClosure($this);
} }
} }
@ -92,7 +103,7 @@ trait Testable
} }
self::$__beforeAll = (self::$__beforeAll instanceof Closure) self::$__beforeAll = (self::$__beforeAll instanceof Closure)
? ChainableClosure::fromStatic(self::$__beforeAll, $hook) ? ChainableClosure::boundStatically(self::$__beforeAll, $hook)
: $hook; : $hook;
} }
@ -106,7 +117,7 @@ trait Testable
} }
self::$__afterAll = (self::$__afterAll instanceof Closure) self::$__afterAll = (self::$__afterAll instanceof Closure)
? ChainableClosure::fromStatic(self::$__afterAll, $hook) ? ChainableClosure::boundStatically(self::$__afterAll, $hook)
: $hook; : $hook;
} }
@ -136,7 +147,7 @@ trait Testable
} }
$this->{$property} = ($this->{$property} instanceof Closure) $this->{$property} = ($this->{$property} instanceof Closure)
? ChainableClosure::from($this->{$property}, $hook) ? ChainableClosure::bound($this->{$property}, $hook)
: $hook; : $hook;
} }
@ -150,7 +161,7 @@ trait Testable
$beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename); $beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename);
if (self::$__beforeAll instanceof Closure) { if (self::$__beforeAll instanceof Closure) {
$beforeAll = ChainableClosure::fromStatic(self::$__beforeAll, $beforeAll); $beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll);
} }
call_user_func(Closure::bind($beforeAll, null, self::class)); call_user_func(Closure::bind($beforeAll, null, self::class));
@ -164,7 +175,7 @@ trait Testable
$afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename); $afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename);
if (self::$__afterAll instanceof Closure) { if (self::$__afterAll instanceof Closure) {
$afterAll = ChainableClosure::fromStatic(self::$__afterAll, $afterAll); $afterAll = ChainableClosure::boundStatically(self::$__afterAll, $afterAll);
} }
call_user_func(Closure::bind($afterAll, null, self::class)); call_user_func(Closure::bind($afterAll, null, self::class));
@ -179,12 +190,34 @@ trait Testable
{ {
TestSuite::getInstance()->test = $this; 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);
if ($method->repetitions > 1) {
$matches = [];
preg_match('/\((.*?)\)/', $description, $matches);
if (count($matches) > 1) {
if (str_contains($description, 'with '.$matches[0].' /')) {
$description = str_replace('with '.$matches[0].' /', '', $description);
} else {
$description = str_replace('with '.$matches[0], '', $description);
}
}
$description .= ' @ repetition '.($matches[1].' of '.$method->repetitions);
}
$this->__description = self::$__latestDescription = $description;
parent::setUp(); parent::setUp();
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename)[1]; $beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename)[1];
if ($this->__beforeEach instanceof Closure) { if ($this->__beforeEach instanceof Closure) {
$beforeEach = ChainableClosure::from($this->__beforeEach, $beforeEach); $beforeEach = ChainableClosure::bound($this->__beforeEach, $beforeEach);
} }
$this->__callClosure($beforeEach, func_get_args()); $this->__callClosure($beforeEach, func_get_args());
@ -198,7 +231,7 @@ trait Testable
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename); $afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
if ($this->__afterEach instanceof Closure) { if ($this->__afterEach instanceof Closure) {
$afterEach = ChainableClosure::from($this->__afterEach, $afterEach); $afterEach = ChainableClosure::bound($this->__afterEach, $afterEach);
} }
$this->__callClosure($afterEach, func_get_args()); $this->__callClosure($afterEach, func_get_args());
@ -230,7 +263,9 @@ trait Testable
{ {
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$this->__description = self::$__latestDescription = $this->dataName() ? $method->description.' with '.$this->dataName() : $method->description; if ($method->repetitions > 1) {
array_shift($arguments);
}
$underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure'); $underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure');
$testParameterTypes = array_values(Reflection::getFunctionArguments($underlyingTest)); $testParameterTypes = array_values(Reflection::getFunctionArguments($underlyingTest));
@ -255,7 +290,7 @@ trait Testable
return $arguments; return $arguments;
} }
if (in_array($testParameterTypes[0], [Closure::class, 'callable'])) { if (isset($testParameterTypes[0]) && in_array($testParameterTypes[0], [Closure::class, 'callable'])) {
return $arguments; return $arguments;
} }
@ -302,6 +337,24 @@ trait Testable
return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments)); return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
} }
/** @postCondition */
protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void
{
if (count($this->__snapshotChanges) === 0) {
return;
}
if (count($this->__snapshotChanges) === 1) {
$this->markTestIncomplete($this->__snapshotChanges[0]);
return;
}
$messages = implode(PHP_EOL, array_map(static fn (string $message): string => '- $message', $this->__snapshotChanges));
$this->markTestIncomplete($messages);
}
/** /**
* The printable test case name. * The printable test case name.
*/ */

View File

@ -23,9 +23,10 @@ final class Thanks
* @var array<string, string> * @var array<string, string>
*/ */
private const FUNDING_MESSAGES = [ private const FUNDING_MESSAGES = [
'Star the project on GitHub' => 'https://github.com/pestphp/pest', 'Star' => 'https://github.com/pestphp/pest',
'Tweet about the project' => 'https://twitter.com/pestphp', 'News' => 'https://twitter.com/pestphp',
'Sponsor the project' => 'https://github.com/sponsors/nunomaduro', 'Videos' => 'https://youtube.com/@nunomaduro',
'Sponsor' => 'https://github.com/sponsors/nunomaduro',
]; ];
/** /**
@ -49,7 +50,7 @@ final class Thanks
$wantsToSupport = false; $wantsToSupport = false;
if (getenv('PEST_NO_SUPPORT') !== 'true' && $this->input->isInteractive()) { if (getenv('PEST_NO_SUPPORT') !== 'true' && $this->input->isInteractive()) {
$wantsToSupport = (new SymfonyQuestionHelper())->ask( $wantsToSupport = (new SymfonyQuestionHelper)->ask(
new ArrayInput([]), new ArrayInput([]),
$this->output, $this->output,
new ConfirmationQuestion( new ConfirmationQuestion(

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 * @internal
*/ */
interface Shutdownable interface Terminable
{ {
/** /**
* Shutdowns the plugin. * Terminates the plugin.
*/ */
public function shutdown(): void; public function terminate(): void;
} }

View File

@ -12,13 +12,13 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
/** /**
* @internal * @internal
*/ */
final class BeforeEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class AfterAllWithinDescribe extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/** /**
* Creates a new Exception instance. * Creates a new Exception instance.
*/ */
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The beforeEach already exists in the filename `%s`.', $filename)); parent::__construct(sprintf('The afterAll method can not be used within describe functions. Filename `%s`.', $filename));
} }
} }

View File

@ -12,13 +12,13 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
/** /**
* @internal * @internal
*/ */
final class AfterEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class BeforeAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/** /**
* Creates a new Exception instance. * Creates a new Exception instance.
*/ */
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The afterEach already exists in the filename `%s`.', $filename)); parent::__construct(sprintf('The beforeAll already exists in the filename `%s`.', $filename));
} }
} }

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 BeforeAllWithinDescribe extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename)
{
parent::__construct(sprintf('The beforeAll method can not be used within describe functions. Filename `%s`.', $filename));
}
}

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,24 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use InvalidArgumentException as BaseInvalidArgumentException;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Symfony\Component\Console\Exception\ExceptionInterface;
/**
* @internal
*/
final class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new Exception instance.
*/
public function __construct(string $message)
{
parent::__construct($message, 1);
}
}

View File

@ -14,7 +14,7 @@ use Symfony\Component\Console\Output\OutputInterface;
/** /**
* @internal * @internal
*/ */
final class NoDirtyTestsFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace, Panicable final class NoDirtyTestsFound extends InvalidArgumentException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{ {
/** /**
* Renders the panic on the given output. * Renders the panic on the given output.

View File

@ -4,15 +4,21 @@ declare(strict_types=1);
namespace Pest; namespace Pest;
use Attribute;
use BadMethodCallException; use BadMethodCallException;
use Closure; use Closure;
use InvalidArgumentException;
use OutOfRangeException;
use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Contracts\ArchExpectation;
use Pest\Arch\Expectations\Targeted;
use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedIn;
use Pest\Arch\Expectations\ToBeUsedInNothing; use Pest\Arch\Expectations\ToBeUsedInNothing;
use Pest\Arch\Expectations\ToOnlyBeUsedIn; use Pest\Arch\Expectations\ToOnlyBeUsedIn;
use Pest\Arch\Expectations\ToOnlyUse; use Pest\Arch\Expectations\ToOnlyUse;
use Pest\Arch\Expectations\ToUse; use Pest\Arch\Expectations\ToUse;
use Pest\Arch\Expectations\ToUseNothing; use Pest\Arch\Expectations\ToUseNothing;
use Pest\Arch\PendingArchExpectation;
use Pest\Arch\Support\FileLineFinder;
use Pest\Concerns\Extendable; use Pest\Concerns\Extendable;
use Pest\Concerns\Pipeable; use Pest\Concerns\Pipeable;
use Pest\Concerns\Retrievable; use Pest\Concerns\Retrievable;
@ -24,18 +30,22 @@ use Pest\Expectations\HigherOrderExpectation;
use Pest\Expectations\OppositeExpectation; use Pest\Expectations\OppositeExpectation;
use Pest\Matchers\Any; use Pest\Matchers\Any;
use Pest\Support\ExpectationPipeline; use Pest\Support\ExpectationPipeline;
use PHPUnit\Framework\Assert; use PHPUnit\Architecture\Elements\ObjectDescription;
use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\ExpectationFailedException;
use ReflectionEnum;
/** /**
* @internal
*
* @template TValue * @template TValue
* *
* @property OppositeExpectation $not Creates the opposite expectation. * @property OppositeExpectation $not Creates the opposite expectation.
* @property EachExpectation $each Creates an expectation on each element on the traversable value. * @property EachExpectation $each Creates an expectation on each element on the traversable value.
* @property PendingArchExpectation $classes
* @property PendingArchExpectation $traits
* @property PendingArchExpectation $interfaces
* @property PendingArchExpectation $enums
* *
* @mixin Mixins\Expectation<TValue> * @mixin Mixins\Expectation<TValue>
* @mixin PendingArchExpectation
*/ */
final class Expectation final class Expectation
{ {
@ -118,6 +128,40 @@ final class Expectation
exit(1); exit(1);
} }
/**
* Dump the expectation value when the result of the condition is truthy.
*
* @param (\Closure(TValue): bool)|bool $condition
* @return self<TValue>
*/
public function ddWhen(Closure|bool $condition, mixed ...$arguments): Expectation
{
$condition = $condition instanceof Closure ? $condition($this->value) : $condition;
if (! $condition) {
return $this;
}
$this->dd(...$arguments);
}
/**
* Dump the expectation value when the result of the condition is falsy.
*
* @param (\Closure(TValue): bool)|bool $condition
* @return self<TValue>
*/
public function ddUnless(Closure|bool $condition, mixed ...$arguments): Expectation
{
$condition = $condition instanceof Closure ? $condition($this->value) : $condition;
if ($condition) {
return $this;
}
$this->dd(...$arguments);
}
/** /**
* Send the expectation value to Ray along with all given arguments. * Send the expectation value to Ray along with all given arguments.
* *
@ -147,7 +191,7 @@ final class Expectation
* *
* @return EachExpectation<TValue> * @return EachExpectation<TValue>
*/ */
public function each(callable $callback = null): EachExpectation public function each(?callable $callback = null): EachExpectation
{ {
if (! is_iterable($this->value)) { if (! is_iterable($this->value)) {
throw new BadMethodCallException('Expectation value is not iterable.'); throw new BadMethodCallException('Expectation value is not iterable.');
@ -176,30 +220,26 @@ final class Expectation
throw new BadMethodCallException('Expectation value is not iterable.'); throw new BadMethodCallException('Expectation value is not iterable.');
} }
$value = is_array($this->value) ? $this->value : iterator_to_array($this->value); if (count($callbacks) == 0) {
$keys = array_keys($value); throw new InvalidArgumentException('No sequence expectations defined.');
$values = array_values($value);
$callbacksCount = count($callbacks);
$index = 0;
while (count($callbacks) < count($values)) {
$callbacks[] = $callbacks[$index];
$index = $index < count($values) - 1 ? $index + 1 : 0;
} }
if ($callbacksCount > count($values)) { $index = $valuesCount = 0;
Assert::assertLessThanOrEqual(count($value), count($callbacks));
}
foreach ($values as $key => $item) { foreach ($this->value as $key => $value) {
if ($callbacks[$key] instanceof Closure) { $valuesCount++;
call_user_func($callbacks[$key], new self($item), new self($keys[$key]));
continue; if ($callbacks[$index] instanceof Closure) {
$callbacks[$index](new self($value), new self($key));
} else {
(new self($value))->toEqual($callbacks[$index]);
} }
(new self($item))->toEqual($callbacks[$key]); $index = isset($callbacks[$index + 1]) ? $index + 1 : 0;
}
if ($valuesCount < count($callbacks)) {
throw new OutOfRangeException('Sequence expectations are more than the iterable items.');
} }
return $this; return $this;
@ -287,16 +327,36 @@ final class Expectation
* @param array<int, mixed> $parameters * @param array<int, mixed> $parameters
* @return Expectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue> * @return Expectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue>
*/ */
public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation
{ {
if (! self::hasMethod($method)) { if (! self::hasMethod($method)) {
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $method)) {
$pendingArchExpectation = new PendingArchExpectation($this, []);
return $pendingArchExpectation->$method(...$parameters); // @phpstan-ignore-line
}
if (! is_object($this->value)) {
throw new BadMethodCallException(sprintf(
'Method "%s" does not exist in %s.',
$method,
gettype($this->value)
));
}
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
return new HigherOrderExpectation($this, call_user_func_array($this->value->$method(...), $parameters)); return new HigherOrderExpectation($this, call_user_func_array($this->value->$method(...), $parameters));
} }
ExpectationPipeline::for($this->getExpectationClosure($method)) $closure = $this->getExpectationClosure($method);
$reflectionClosure = new \ReflectionFunction($closure);
$expectation = $reflectionClosure->getClosureThis();
assert(is_object($expectation));
ExpectationPipeline::for($closure)
->send(...$parameters) ->send(...$parameters)
->through($this->pipes($method, $this, Expectation::class)) ->through($this->pipes($method, $expectation, Expectation::class))
->run(); ->run();
return $this; return $this;
@ -333,6 +393,11 @@ final class Expectation
public function __get(string $name) public function __get(string $name)
{ {
if (! self::hasMethod($name)) { if (! self::hasMethod($name)) {
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $name)) {
/* @phpstan-ignore-next-line */
return $this->{$name}();
}
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); return new HigherOrderExpectation($this, $this->retrieve($name, $this->value));
} }
@ -356,7 +421,7 @@ final class Expectation
*/ */
public function any(): Any public function any(): Any
{ {
return new Any(); return new Any;
} }
/** /**
@ -369,6 +434,265 @@ final class Expectation
return ToUse::make($this, $targets); return ToUse::make($this, $targets);
} }
/**
* Asserts that the given expectation target use the "declare(strict_types=1)" declaration.
*/
public function toUseStrictTypes(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'),
'to use strict types',
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
);
}
/**
* Asserts that the given expectation target is final.
*/
public function toBeFinal(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && $object->reflectionClass->isFinal(),
'to be final',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target is readonly.
*/
public function toBeReadonly(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line
'to be readonly',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target is trait.
*/
public function toBeTrait(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->isTrait(),
'to be trait',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation targets are traits.
*/
public function toBeTraits(): ArchExpectation
{
return $this->toBeTrait();
}
/**
* Asserts that the given expectation target is abstract.
*/
public function toBeAbstract(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->isAbstract(),
'to be abstract',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target has a specific method.
*/
public function toHaveMethod(string $method): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->hasMethod($method),
sprintf("to have method '%s'", $method),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target is enum.
*/
public function toBeEnum(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum(),
'to be enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation targets are enums.
*/
public function toBeEnums(): ArchExpectation
{
return $this->toBeEnum();
}
/**
* Asserts that the given expectation target is a class.
*/
public function toBeClass(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => class_exists($object->name) && ! enum_exists($object->name),
'to be class',
FileLineFinder::where(fn (string $line): bool => true),
);
}
/**
* Asserts that the given expectation targets are classes.
*/
public function toBeClasses(): ArchExpectation
{
return $this->toBeClass();
}
/**
* Asserts that the given expectation target is interface.
*/
public function toBeInterface(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->isInterface(),
'to be interface',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation targets are interfaces.
*/
public function toBeInterfaces(): ArchExpectation
{
return $this->toBeInterface();
}
/**
* Asserts that the given expectation target to be subclass of the given class.
*
* @param class-string $class
*/
public function toExtend(string $class): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $class === $object->reflectionClass->getName() || $object->reflectionClass->isSubclassOf($class),
sprintf("to extend '%s'", $class),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target to be have a parent class.
*/
public function toExtendNothing(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() === false,
'to extend nothing',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target to not implement any interfaces.
*/
public function toImplementNothing(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() === [],
'to implement nothing',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target to only implement the given interfaces.
*
* @param array<int, class-string>|class-string $interfaces
*/
public function toOnlyImplement(array|string $interfaces): ArchExpectation
{
$interfaces = is_array($interfaces) ? $interfaces : [$interfaces];
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => count($interfaces) === count($object->reflectionClass->getInterfaceNames())
&& array_diff($interfaces, $object->reflectionClass->getInterfaceNames()) === [],
"to only implement '".implode("', '", $interfaces)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target to have the given prefix.
*/
public function toHavePrefix(string $prefix): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => str_starts_with($object->reflectionClass->getShortName(), $prefix),
"to have prefix '{$prefix}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target to have the given suffix.
*/
public function toHaveSuffix(string $suffix): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => str_ends_with($object->reflectionClass->getName(), $suffix),
"to have suffix '{$suffix}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target to implement the given interfaces.
*
* @param array<int, class-string>|class-string $interfaces
*/
public function toImplement(array|string $interfaces): ArchExpectation
{
$interfaces = is_array($interfaces) ? $interfaces : [$interfaces];
return Targeted::make(
$this,
function (ObjectDescription $object) use ($interfaces): bool {
foreach ($interfaces as $interface) {
if (! $object->reflectionClass->implementsInterface($interface)) {
return false;
}
}
return true;
},
"to implement '".implode("', '", $interfaces)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/** /**
* Asserts that the given expectation target "only" use on the given dependencies. * Asserts that the given expectation target "only" use on the given dependencies.
* *
@ -419,4 +743,191 @@ final class Expectation
{ {
return ToBeUsedInNothing::make($this); return ToBeUsedInNothing::make($this);
} }
/**
* Asserts that the given expectation dependency is an invokable class.
*/
public function toBeInvokable(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->hasMethod('__invoke'),
'to be invokable',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
);
}
/**
* Asserts that the given expectation is iterable and contains snake_case keys.
*
* @return self<TValue>
*/
public function toHaveSnakeCaseKeys(string $message = ''): self
{
if (! is_iterable($this->value)) {
InvalidExpectationValue::expected('iterable');
}
foreach ($this->value as $k => $item) {
if (is_string($k)) {
$this->and($k)->toBeSnakeCase($message);
}
if (is_array($item)) {
$this->and($item)->toHaveSnakeCaseKeys($message);
}
}
return $this;
}
/**
* Asserts that the given expectation is iterable and contains kebab-case keys.
*
* @return self<TValue>
*/
public function toHaveKebabCaseKeys(string $message = ''): self
{
if (! is_iterable($this->value)) {
InvalidExpectationValue::expected('iterable');
}
foreach ($this->value as $k => $item) {
if (is_string($k)) {
$this->and($k)->toBeKebabCase($message);
}
if (is_array($item)) {
$this->and($item)->toHaveKebabCaseKeys($message);
}
}
return $this;
}
/**
* Asserts that the given expectation is iterable and contains camelCase keys.
*
* @return self<TValue>
*/
public function toHaveCamelCaseKeys(string $message = ''): self
{
if (! is_iterable($this->value)) {
InvalidExpectationValue::expected('iterable');
}
foreach ($this->value as $k => $item) {
if (is_string($k)) {
$this->and($k)->toBeCamelCase($message);
}
if (is_array($item)) {
$this->and($item)->toHaveCamelCaseKeys($message);
}
}
return $this;
}
/**
* Asserts that the given expectation is iterable and contains StudlyCase keys.
*
* @return self<TValue>
*/
public function toHaveStudlyCaseKeys(string $message = ''): self
{
if (! is_iterable($this->value)) {
InvalidExpectationValue::expected('iterable');
}
foreach ($this->value as $k => $item) {
if (is_string($k)) {
$this->and($k)->toBeStudlyCase($message);
}
if (is_array($item)) {
$this->and($item)->toHaveStudlyCaseKeys($message);
}
}
return $this;
}
/**
* Asserts that the given expectation target to have the given attribute.
*
* @param class-string<Attribute> $attribute
*/
public function toHaveAttribute(string $attribute): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) !== [],
"to have attribute '{$attribute}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target has a constructor method.
*/
public function toHaveConstructor(): ArchExpectation
{
return $this->toHaveMethod('__construct');
}
/**
* Asserts that the given expectation target has a destructor method.
*/
public function toHaveDestructor(): ArchExpectation
{
return $this->toHaveMethod('__destruct');
}
/**
* Asserts that the given expectation target is a backed enum of given type.
*/
private function toBeBackedEnum(string $backingType): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum()
&& (new ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
&& (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line
'to be '.$backingType.' backed enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation targets are string backed enums.
*/
public function toBeStringBackedEnums(): ArchExpectation
{
return $this->toBeStringBackedEnum();
}
/**
* Asserts that the given expectation targets are int backed enums.
*/
public function toBeIntBackedEnums(): ArchExpectation
{
return $this->toBeIntBackedEnum();
}
/**
* Asserts that the given expectation target is a string backed enum.
*/
public function toBeStringBackedEnum(): ArchExpectation
{
return $this->toBeBackedEnum('string');
}
/**
* Asserts that the given expectation target is an int backed enum.
*/
public function toBeIntBackedEnum(): ArchExpectation
{
return $this->toBeBackedEnum('int');
}
} }

View File

@ -4,9 +4,10 @@ declare(strict_types=1);
namespace Pest\Expectations; namespace Pest\Expectations;
use function expect;
use Pest\Expectation; use Pest\Expectation;
use function expect;
/** /**
* @internal * @internal
* *
@ -23,9 +24,7 @@ final class EachExpectation
* *
* @param Expectation<TValue> $original * @param Expectation<TValue> $original
*/ */
public function __construct(private readonly Expectation $original) public function __construct(private readonly Expectation $original) {}
{
}
/** /**
* Creates a new expectation. * Creates a new expectation.

View File

@ -4,16 +4,22 @@ declare(strict_types=1);
namespace Pest\Expectations; namespace Pest\Expectations;
use Attribute;
use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Contracts\ArchExpectation;
use Pest\Arch\Expectations\Targeted;
use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedIn;
use Pest\Arch\Expectations\ToBeUsedInNothing; use Pest\Arch\Expectations\ToBeUsedInNothing;
use Pest\Arch\Expectations\ToUse; use Pest\Arch\Expectations\ToUse;
use Pest\Arch\GroupArchExpectation; use Pest\Arch\GroupArchExpectation;
use Pest\Arch\PendingArchExpectation;
use Pest\Arch\SingleArchExpectation; use Pest\Arch\SingleArchExpectation;
use Pest\Arch\Support\FileLineFinder;
use Pest\Exceptions\InvalidExpectation; use Pest\Exceptions\InvalidExpectation;
use Pest\Expectation; use Pest\Expectation;
use Pest\Support\Arr; use Pest\Support\Arr;
use Pest\Support\Exporter; use Pest\Support\Exporter;
use PHPUnit\Architecture\Elements\ObjectDescription;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\ExpectationFailedException;
/** /**
@ -30,9 +36,7 @@ final class OppositeExpectation
* *
* @param Expectation<TValue> $original * @param Expectation<TValue> $original
*/ */
public function __construct(private readonly Expectation $original) public function __construct(private readonly Expectation $original) {}
{
}
/** /**
* Asserts that the value array not has the provided $keys. * Asserts that the value array not has the provided $keys.
@ -72,6 +76,259 @@ final class OppositeExpectation
} }
/** /**
* Asserts that the given expectation target does not use the "declare(strict_types=1)" declaration.
*/
public function toUseStrictTypes(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'),
'not to use strict types',
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
);
}
/**
* Asserts that the given expectation target is not final.
*/
public function toBeFinal(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isFinal(),
'not to be final',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target is not readonly.
*/
public function toBeReadonly(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line
'not to be readonly',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target is not trait.
*/
public function toBeTrait(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isTrait(),
'not to be trait',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation targets are not traits.
*/
public function toBeTraits(): ArchExpectation
{
return $this->toBeTrait();
}
/**
* Asserts that the given expectation target is not abstract.
*/
public function toBeAbstract(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isAbstract(),
'not to be abstract',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target does not have a specific method.
*/
public function toHaveMethod(string $method): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod($method),
'to not have method',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target is not enum.
*/
public function toBeEnum(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum(),
'not to be enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation targets are not enums.
*/
public function toBeEnums(): ArchExpectation
{
return $this->toBeEnum();
}
/**
* Asserts that the given expectation targets is not class.
*/
public function toBeClass(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! class_exists($object->name),
'not to be class',
FileLineFinder::where(fn (string $line): bool => true),
);
}
/**
* Asserts that the given expectation targets are not classes.
*/
public function toBeClasses(): ArchExpectation
{
return $this->toBeClass();
}
/**
* Asserts that the given expectation target is not interface.
*/
public function toBeInterface(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isInterface(),
'not to be interface',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation targets are not interfaces.
*/
public function toBeInterfaces(): ArchExpectation
{
return $this->toBeInterface();
}
/**
* Asserts that the given expectation target to be not subclass of the given class.
*
* @param class-string $class
*/
public function toExtend(string $class): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isSubclassOf($class),
sprintf("not to extend '%s'", $class),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target to be not have any parent class.
*/
public function toExtendNothing(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() !== false,
'to extend a class',
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
*/
public function toImplement(array|string $interfaces): ArchExpectation
{
$interfaces = is_array($interfaces) ? $interfaces : [$interfaces];
return Targeted::make(
$this->original,
function (ObjectDescription $object) use ($interfaces): bool {
foreach ($interfaces as $interface) {
if ($object->reflectionClass->implementsInterface($interface)) {
return false;
}
}
return true;
},
"not to implement '".implode("', '", $interfaces)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target to not implement any interfaces.
*/
public function toImplementNothing(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() !== [],
'to implement an interface',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Not supported.
*
* @param array<int, class-string>|string $interfaces
*/
public function toOnlyImplement(array|string $interfaces): never
{
throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']);
}
/**
* Asserts that the given expectation target to not have the given prefix.
*/
public function toHavePrefix(string $prefix): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! str_starts_with($object->reflectionClass->getShortName(), $prefix),
"not to have prefix '{$prefix}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation target to not have the given suffix.
*/
public function toHaveSuffix(string $suffix): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! str_ends_with($object->reflectionClass->getName(), $suffix),
"not to have suffix '{$suffix}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Not supported.
*
* @param array<int, string>|string $targets * @param array<int, string>|string $targets
*/ */
public function toOnlyUse(array|string $targets): never public function toOnlyUse(array|string $targets): never
@ -79,6 +336,9 @@ final class OppositeExpectation
throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']); throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']);
} }
/**
* Not supported.
*/
public function toUseNothing(): never public function toUseNothing(): never
{ {
throw InvalidExpectation::fromMethods(['not', 'toUseNothing']); throw InvalidExpectation::fromMethods(['not', 'toUseNothing']);
@ -117,6 +377,34 @@ final class OppositeExpectation
throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']); throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']);
} }
/**
* Asserts that the given expectation dependency is not an invokable class.
*/
public function toBeInvokable(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod('__invoke'),
'to not be invokable',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
);
}
/**
* Asserts that the given expectation target not to have the given attribute.
*
* @param class-string<Attribute> $attribute
*/
public function toHaveAttribute(string $attribute): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) === [],
"to not have attribute '{$attribute}'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
);
}
/** /**
* Handle dynamic method calls into the original expectation. * Handle dynamic method calls into the original expectation.
* *
@ -126,9 +414,13 @@ final class OppositeExpectation
public function __call(string $name, array $arguments): Expectation public function __call(string $name, array $arguments): Expectation
{ {
try { try {
if (! is_object($this->original->value) && method_exists(PendingArchExpectation::class, $name)) {
throw InvalidExpectation::fromMethods(['not', $name]);
}
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
$this->original->{$name}(...$arguments); $this->original->{$name}(...$arguments);
} catch (ExpectationFailedException) { } catch (ExpectationFailedException|AssertionFailedError) {
return $this->original; return $this->original;
} }
@ -143,8 +435,12 @@ final class OppositeExpectation
public function __get(string $name): Expectation public function __get(string $name): Expectation
{ {
try { try {
if (! is_object($this->original->value) && method_exists(PendingArchExpectation::class, $name)) {
throw InvalidExpectation::fromMethods(['not', $name]);
}
$this->original->{$name}; // @phpstan-ignore-line $this->original->{$name}; // @phpstan-ignore-line
} catch (ExpectationFailedException) { // @phpstan-ignore-line } catch (ExpectationFailedException) {
return $this->original; return $this->original;
} }
@ -162,8 +458,76 @@ final class OppositeExpectation
$exporter = Exporter::default(); $exporter = Exporter::default();
$toString = fn ($argument): string => $exporter->shortenedExport($argument); $toString = fn (mixed $argument): string => $exporter->shortenedExport($argument);
throw new ExpectationFailedException(sprintf('Expecting %s not %s %s.', $toString($this->original->value), strtolower((string) preg_replace('/(?<!\ )[A-Z]/', ' $0', $name)), implode(' ', array_map(fn ($argument): string => $toString($argument), $arguments)))); throw new ExpectationFailedException(sprintf(
'Expecting %s not %s %s.',
$toString($this->original->value),
strtolower((string) preg_replace('/(?<!\ )[A-Z]/', ' $0', $name)),
implode(' ', array_map(fn (mixed $argument): string => $toString($argument), $arguments)),
));
}
/**
* Asserts that the given expectation target does not have a constructor method.
*/
public function toHaveConstructor(): ArchExpectation
{
return $this->toHaveMethod('__construct');
}
/**
* Asserts that the given expectation target does not have a destructor method.
*/
public function toHaveDestructor(): ArchExpectation
{
return $this->toHaveMethod('__destruct');
}
/**
* Asserts that the given expectation target is not a backed enum of given type.
*/
private function toBeBackedEnum(string $backingType): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum()
|| ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
|| (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line
'not to be '.$backingType.' backed enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
}
/**
* Asserts that the given expectation targets are not string backed enums.
*/
public function toBeStringBackedEnums(): ArchExpectation
{
return $this->toBeStringBackedEnum();
}
/**
* Asserts that the given expectation targets are not int backed enums.
*/
public function toBeIntBackedEnums(): ArchExpectation
{
return $this->toBeIntBackedEnum();
}
/**
* Asserts that the given expectation target is not a string backed enum.
*/
public function toBeStringBackedEnum(): ArchExpectation
{
return $this->toBeBackedEnum('string');
}
/**
* Asserts that the given expectation target is not an int backed enum.
*/
public function toBeIntBackedEnum(): ArchExpectation
{
return $this->toBeBackedEnum('int');
} }
} }

View File

@ -19,7 +19,7 @@ final class Depends implements AddsAnnotations
public function __invoke(TestCaseMethodFactory $method, array $annotations): array public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{ {
foreach ($method->depends as $depend) { foreach ($method->depends as $depend) {
$depend = Str::evaluable($depend); $depend = Str::evaluable($method->describing !== null ? Str::describe($method->describing, $depend) : $depend);
$annotations[] = "@depends $depend"; $annotations[] = "@depends $depend";
} }

View File

@ -15,12 +15,11 @@ final class TestDox implements AddsAnnotations
public function __invoke(TestCaseMethodFactory $method, array $annotations): array public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{ {
/* /*
* escapes docblock according to * Escapes docblock according to
* https://manual.phpdoc.org/HTMLframesConverter/default/phpDocumentor/tutorial_phpDocumentor.howto.pkg.html#basics.desc * 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 * Note: '@' escaping is not needed as it cannot be the first character of the line (it always starts with @testdox).
*/ */
assert($method->description !== null); assert($method->description !== null);
$methodDescription = str_replace('*/', '{@*}', $method->description); $methodDescription = str_replace('*/', '{@*}', $method->description);

View File

@ -20,7 +20,7 @@ abstract class Attribute
* @param array<int, string> $attributes * @param array<int, string> $attributes
* @return array<int, string> * @return array<int, string>
*/ */
public function __invoke(TestCaseMethodFactory $method, array $attributes): array // @phpstan-ignore-line public function __invoke(TestCaseMethodFactory $method, array $attributes): array // @phpstan-ignore-line
{ {
return $attributes; return $attributes;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -98,7 +98,7 @@ final class TestCaseFactory
{ {
if ('\\' === DIRECTORY_SEPARATOR) { if ('\\' === DIRECTORY_SEPARATOR) {
// In case Windows, strtolower drive name, like in UsesCall. // In case Windows, strtolower drive name, like in UsesCall.
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', static fn ($match): string => strtolower($match['drive']), $filename); $filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', static fn (array $match): string => strtolower($match['drive']), $filename);
} }
$filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename))); $filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename)));
@ -134,7 +134,7 @@ final class TestCaseFactory
$hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class); $hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class);
$traitsCode = sprintf('use %s;', implode(', ', array_map( $traitsCode = sprintf('use %s;', implode(', ', array_map(
static fn ($trait): string => sprintf('\%s', $trait), $this->traits)) static fn (string $trait): string => sprintf('\%s', $trait), $this->traits))
); );
$partsFQN = explode('\\', $classFQN); $partsFQN = explode('\\', $classFQN);
@ -142,7 +142,7 @@ final class TestCaseFactory
$namespace = implode('\\', $partsFQN); $namespace = implode('\\', $partsFQN);
$baseClass = sprintf('\%s', $this->class); $baseClass = sprintf('\%s', $this->class);
if ('' === trim($className)) { if (trim($className) === '') {
$className = 'InvalidTestName'.Str::random(); $className = 'InvalidTestName'.Str::random();
} }
@ -154,7 +154,7 @@ final class TestCaseFactory
foreach ($classAvailableAttributes as $attribute) { foreach ($classAvailableAttributes as $attribute) {
$classAttributes = array_reduce( $classAttributes = array_reduce(
$methods, $methods,
fn (array $carry, TestCaseMethodFactory $methodFactory): array => (new $attribute())->__invoke($methodFactory, $carry), fn (array $carry, TestCaseMethodFactory $methodFactory): array => (new $attribute)->__invoke($methodFactory, $carry),
$classAttributes $classAttributes
); );
} }
@ -241,7 +241,7 @@ final class TestCaseFactory
throw ShouldNotHappen::fromMessage('The test description may not be empty.'); throw ShouldNotHappen::fromMessage('The test description may not be empty.');
} }
if (Str::evaluable($method->description) === $methodName) { if ($methodName === Str::evaluable($method->description)) {
return true; return true;
} }
} }
@ -259,7 +259,7 @@ final class TestCaseFactory
throw ShouldNotHappen::fromMessage('The test description may not be empty.'); throw ShouldNotHappen::fromMessage('The test description may not be empty.');
} }
if (Str::evaluable($method->description) === $methodName) { if ($methodName === Str::evaluable($method->description)) {
return $method; return $method;
} }
} }

View File

@ -22,40 +22,50 @@ final class TestCaseMethodFactory
use HigherOrderable; use HigherOrderable;
/** /**
* Determines if the Test Case Method is a "todo". * The test's describing, if any.
*/
public ?string $describing = null;
/**
* The test's number of repetitions.
*/
public int $repetitions = 1;
/**
* Determines if the test is a "todo".
*/ */
public bool $todo = false; public bool $todo = false;
/** /**
* The Test Case Dataset, if any. * The test's datasets.
* *
* @var array<Closure|iterable<int|string, mixed>|string> * @var array<Closure|iterable<int|string, mixed>|string>
*/ */
public array $datasets = []; public array $datasets = [];
/** /**
* The Test Case depends, if any. * The test's dependencies.
* *
* @var array<int, string> * @var array<int, string>
*/ */
public array $depends = []; public array $depends = [];
/** /**
* The Test Case groups, if any. * The test's groups.
* *
* @var array<int, string> * @var array<int, string>
*/ */
public array $groups = []; public array $groups = [];
/** /**
* The covered classes and functions, if any. * The covered classes and functions.
* *
* @var array<int, \Pest\Factories\Covers\CoversClass|\Pest\Factories\Covers\CoversFunction|\Pest\Factories\Covers\CoversNothing> * @var array<int, \Pest\Factories\Covers\CoversClass|\Pest\Factories\Covers\CoversFunction|\Pest\Factories\Covers\CoversNothing>
*/ */
public array $covers = []; public array $covers = [];
/** /**
* Creates a new Factory instance. * Creates a new test case method factory instance.
*/ */
public function __construct( public function __construct(
public string $filename, public string $filename,
@ -63,14 +73,14 @@ final class TestCaseMethodFactory
public ?Closure $closure, public ?Closure $closure,
) { ) {
$this->closure ??= function (): void { $this->closure ??= function (): void {
Assert::getCount() > 0 ?: self::markTestIncomplete(); // @phpstan-ignore-line (Assert::getCount() > 0 || $this->doesNotPerformAssertions()) ?: self::markTestIncomplete(); // @phpstan-ignore-line
}; };
$this->bootHigherOrderable(); $this->bootHigherOrderable();
} }
/** /**
* Makes the Test Case classes. * Creates the test's closure.
*/ */
public function getClosure(TestCase $concrete): Closure public function getClosure(TestCase $concrete): Closure
{ {
@ -128,25 +138,25 @@ final class TestCaseMethodFactory
$attributes = []; $attributes = [];
foreach ($annotationsToUse as $annotation) { foreach ($annotationsToUse as $annotation) {
$annotations = (new $annotation())->__invoke($this, $annotations); $annotations = (new $annotation)->__invoke($this, $annotations);
} }
foreach ($attributesToUse as $attribute) { foreach ($attributesToUse as $attribute) {
$attributes = (new $attribute())->__invoke($this, $attributes); $attributes = (new $attribute)->__invoke($this, $attributes);
} }
if ($this->datasets !== []) { if ($this->datasets !== [] || $this->repetitions > 1) {
$dataProviderName = $methodName.'_dataset'; $dataProviderName = $methodName.'_dataset';
$annotations[] = "@dataProvider $dataProviderName"; $annotations[] = "@dataProvider $dataProviderName";
$datasetsCode = $this->buildDatasetForEvaluation($methodName, $dataProviderName); $datasetsCode = $this->buildDatasetForEvaluation($methodName, $dataProviderName);
} }
$annotations = implode('', array_map( $annotations = implode('', array_map(
static fn ($annotation): string => sprintf("\n * %s", $annotation), $annotations, static fn (string $annotation): string => sprintf("\n * %s", $annotation), $annotations,
)); ));
$attributes = implode('', array_map( $attributes = implode('', array_map(
static fn ($attribute): string => sprintf("\n %s", $attribute), $attributes, static fn (string $attribute): string => sprintf("\n %s", $attribute), $attributes,
)); ));
return <<<PHP return <<<PHP
@ -172,7 +182,13 @@ final class TestCaseMethodFactory
*/ */
private function buildDatasetForEvaluation(string $methodName, string $dataProviderName): string private function buildDatasetForEvaluation(string $methodName, string $dataProviderName): string
{ {
DatasetsRepository::with($this->filename, $methodName, $this->datasets); $datasets = $this->datasets;
if ($this->repetitions > 1) {
$datasets = [range(1, $this->repetitions), ...$datasets];
}
DatasetsRepository::with($this->filename, $methodName, $datasets);
return <<<EOF return <<<EOF

View File

@ -2,9 +2,13 @@
declare(strict_types=1); declare(strict_types=1);
use Pest\Concerns\Expectable;
use Pest\Exceptions\AfterAllWithinDescribe;
use Pest\Exceptions\BeforeAllWithinDescribe;
use Pest\Expectation; use Pest\Expectation;
use Pest\PendingCalls\AfterEachCall; use Pest\PendingCalls\AfterEachCall;
use Pest\PendingCalls\BeforeEachCall; use Pest\PendingCalls\BeforeEachCall;
use Pest\PendingCalls\DescribeCall;
use Pest\PendingCalls\TestCall; use Pest\PendingCalls\TestCall;
use Pest\PendingCalls\UsesCall; use Pest\PendingCalls\UsesCall;
use Pest\Repositories\DatasetsRepository; use Pest\Repositories\DatasetsRepository;
@ -35,6 +39,12 @@ if (! function_exists('beforeAll')) {
*/ */
function beforeAll(Closure $closure): void function beforeAll(Closure $closure): void
{ {
if (! is_null(DescribeCall::describing())) {
$filename = Backtrace::file();
throw new BeforeAllWithinDescribe($filename);
}
TestSuite::getInstance()->beforeAll->set($closure); TestSuite::getInstance()->beforeAll->set($closure);
} }
} }
@ -43,9 +53,9 @@ if (! function_exists('beforeEach')) {
/** /**
* Runs the given closure before each test in the current file. * Runs the given closure before each test in the current file.
* *
* @return HigherOrderTapProxy<TestCall|TestCase>|TestCall|TestCase|mixed * @return HigherOrderTapProxy<Expectable|TestCall|TestCase>|Expectable|TestCall|TestCase|mixed
*/ */
function beforeEach(Closure $closure = null): BeforeEachCall function beforeEach(?Closure $closure = null): BeforeEachCall
{ {
$filename = Backtrace::file(); $filename = Backtrace::file();
@ -67,6 +77,22 @@ if (! function_exists('dataset')) {
} }
} }
if (! function_exists('describe')) {
/**
* Adds the given closure as a group of tests. The first argument
* is the group description; the second argument is a closure
* that contains the group tests.
*
* @return HigherOrderTapProxy<Expectable|TestCall|TestCase>|Expectable|TestCall|TestCase|mixed
*/
function describe(string $description, Closure $tests): DescribeCall
{
$filename = Backtrace::testFile();
return new DescribeCall(TestSuite::getInstance(), $filename, $description, $tests);
}
}
if (! function_exists('uses')) { if (! function_exists('uses')) {
/** /**
* The uses function binds the given * The uses function binds the given
@ -88,9 +114,9 @@ if (! function_exists('test')) {
* is the test description; the second argument is * is the test description; the second argument is
* a closure that contains the test expectations. * a closure that contains the test expectations.
* *
* @return TestCall|TestCase|mixed * @return Expectable|TestCall|TestCase|mixed
*/ */
function test(string $description = null, Closure $closure = null): HigherOrderTapProxy|TestCall function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall
{ {
if ($description === null && TestSuite::getInstance()->test instanceof \PHPUnit\Framework\TestCase) { if ($description === null && TestSuite::getInstance()->test instanceof \PHPUnit\Framework\TestCase) {
return new HigherOrderTapProxy(TestSuite::getInstance()->test); return new HigherOrderTapProxy(TestSuite::getInstance()->test);
@ -108,9 +134,9 @@ if (! function_exists('it')) {
* is the test description; the second argument is * is the test description; the second argument is
* a closure that contains the test expectations. * a closure that contains the test expectations.
* *
* @return TestCall|TestCase|mixed * @return Expectable|TestCall|TestCase|mixed
*/ */
function it(string $description, Closure $closure = null): TestCall function it(string $description, ?Closure $closure = null): TestCall
{ {
$description = sprintf('it %s', $description); $description = sprintf('it %s', $description);
@ -127,7 +153,7 @@ if (! function_exists('todo')) {
* is marked as incomplete. Yet, Collision, Pest's * is marked as incomplete. Yet, Collision, Pest's
* printer, will display it as a "todo" test. * printer, will display it as a "todo" test.
* *
* @return TestCall|TestCase|mixed * @return Expectable|TestCall|TestCase|mixed
*/ */
function todo(string $description): TestCall function todo(string $description): TestCall
{ {
@ -143,9 +169,9 @@ if (! function_exists('afterEach')) {
/** /**
* Runs the given closure after each test in the current file. * Runs the given closure after each test in the current file.
* *
* @return HigherOrderTapProxy<TestCall|TestCase>|TestCall|mixed * @return Expectable|HigherOrderTapProxy<Expectable|TestCall|TestCase>|TestCall|mixed
*/ */
function afterEach(Closure $closure = null): AfterEachCall function afterEach(?Closure $closure = null): AfterEachCall
{ {
$filename = Backtrace::file(); $filename = Backtrace::file();
@ -159,6 +185,12 @@ if (! function_exists('afterAll')) {
*/ */
function afterAll(Closure $closure): void function afterAll(Closure $closure): void
{ {
if (! is_null(DescribeCall::describing())) {
$filename = Backtrace::file();
throw new AfterAllWithinDescribe($filename);
}
TestSuite::getInstance()->afterAll->set($closure); TestSuite::getInstance()->afterAll->set($closure);
} }
} }

View File

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

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(); $this->disable();
} }

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Pest\Logging\TeamCity; namespace Pest\Logging;
use NunoMaduro\Collision\Adapters\Phpunit\State; use NunoMaduro\Collision\Adapters\Phpunit\State;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
@ -36,7 +36,7 @@ final class Converter
public function __construct( public function __construct(
private readonly string $rootPath, private readonly string $rootPath,
) { ) {
$this->stateGenerator = new StateGenerator(); $this->stateGenerator = new StateGenerator;
} }
/** /**
@ -150,6 +150,14 @@ final class Converter
return Str::after($name, self::PREFIX); return Str::after($name, self::PREFIX);
} }
/**
* Gets the trimmed test class name.
*/
public function getTrimmedTestClassName(TestMethod $test): string
{
return Str::after($test->className(), self::PREFIX);
}
/** /**
* Gets the test suite location. * Gets the test suite location.
*/ */

View File

@ -17,8 +17,7 @@ final class ServiceMessage
public function __construct( public function __construct(
private readonly string $type, private readonly string $type,
private readonly array $parameters, private readonly array $parameters,
) { ) {}
}
public function toString(): string public function toString(): string
{ {
@ -63,7 +62,7 @@ final class ServiceMessage
} }
/** /**
* @param int $duration in milliseconds * @param int $duration in milliseconds
*/ */
public static function testFinished(string $name, int $duration): self public static function testFinished(string $name, int $duration): self
{ {
@ -106,7 +105,7 @@ final class ServiceMessage
]); ]);
} }
public static function testIgnored(string $name, string $message, string $details = null): self public static function testIgnored(string $name, string $message, ?string $details = null): self
{ {
return new self('testIgnored', [ return new self('testIgnored', [
'name' => $name, 'name' => $name,

View File

@ -14,9 +14,7 @@ abstract class Subscriber
/** /**
* Creates a new Subscriber instance. * Creates a new Subscriber instance.
*/ */
public function __construct(private readonly TeamCityLogger $logger) public function __construct(private readonly TeamCityLogger $logger) {}
{
}
/** /**
* Creates a new TeamCityLogger instance. * Creates a new TeamCityLogger instance.

View File

@ -6,6 +6,7 @@ namespace Pest\Logging\TeamCity;
use NunoMaduro\Collision\Adapters\Phpunit\Style; use NunoMaduro\Collision\Adapters\Phpunit\Style;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
use Pest\Logging\Converter;
use Pest\Logging\TeamCity\Subscriber\TestConsideredRiskySubscriber; use Pest\Logging\TeamCity\Subscriber\TestConsideredRiskySubscriber;
use Pest\Logging\TeamCity\Subscriber\TestErroredSubscriber; use Pest\Logging\TeamCity\Subscriber\TestErroredSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestExecutionFinishedSubscriber; use Pest\Logging\TeamCity\Subscriber\TestExecutionFinishedSubscriber;

View File

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

View File

@ -6,20 +6,26 @@ namespace Pest\Mixins;
use BadMethodCallException; use BadMethodCallException;
use Closure; use Closure;
use Countable;
use DateTimeInterface; use DateTimeInterface;
use Error; use Error;
use InvalidArgumentException; use InvalidArgumentException;
use JsonSerializable;
use Pest\Exceptions\InvalidExpectationValue; use Pest\Exceptions\InvalidExpectationValue;
use Pest\Matchers\Any; use Pest\Matchers\Any;
use Pest\Support\Arr; use Pest\Support\Arr;
use Pest\Support\Exporter; use Pest\Support\Exporter;
use Pest\Support\NullClosure; use Pest\Support\NullClosure;
use Pest\Support\Str;
use Pest\TestSuite;
use PHPUnit\Framework\Assert; use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
use ReflectionFunction; use ReflectionFunction;
use ReflectionNamedType; use ReflectionNamedType;
use Throwable; use Throwable;
use Traversable;
/** /**
* @internal * @internal
@ -125,7 +131,7 @@ final class Expectation
* *
* @return self<TValue> * @return self<TValue>
*/ */
public function toBeGreaterThan(int|float|DateTimeInterface $expected, string $message = ''): self public function toBeGreaterThan(int|float|string|DateTimeInterface $expected, string $message = ''): self
{ {
Assert::assertGreaterThan($expected, $this->value, $message); Assert::assertGreaterThan($expected, $this->value, $message);
@ -137,7 +143,7 @@ final class Expectation
* *
* @return self<TValue> * @return self<TValue>
*/ */
public function toBeGreaterThanOrEqual(int|float|DateTimeInterface $expected, string $message = ''): self public function toBeGreaterThanOrEqual(int|float|string|DateTimeInterface $expected, string $message = ''): self
{ {
Assert::assertGreaterThanOrEqual($expected, $this->value, $message); Assert::assertGreaterThanOrEqual($expected, $this->value, $message);
@ -149,7 +155,7 @@ final class Expectation
* *
* @return self<TValue> * @return self<TValue>
*/ */
public function toBeLessThan(int|float|DateTimeInterface $expected, string $message = ''): self public function toBeLessThan(int|float|string|DateTimeInterface $expected, string $message = ''): self
{ {
Assert::assertLessThan($expected, $this->value, $message); Assert::assertLessThan($expected, $this->value, $message);
@ -161,7 +167,7 @@ final class Expectation
* *
* @return self<TValue> * @return self<TValue>
*/ */
public function toBeLessThanOrEqual(int|float|DateTimeInterface $expected, string $message = ''): self public function toBeLessThanOrEqual(int|float|string|DateTimeInterface $expected, string $message = ''): self
{ {
Assert::assertLessThanOrEqual($expected, $this->value, $message); Assert::assertLessThanOrEqual($expected, $this->value, $message);
@ -190,6 +196,24 @@ final class Expectation
return $this; return $this;
} }
/**
* Asserts that $needle equal an element of the value.
*
* @return self<TValue>
*/
public function toContainEqual(mixed ...$needles): self
{
if (! is_iterable($this->value)) {
InvalidExpectationValue::expected('iterable');
}
foreach ($needles as $needle) {
Assert::assertContainsEquals($needle, $this->value);
}
return $this;
}
/** /**
* Asserts that the value starts with $expected. * Asserts that the value starts with $expected.
* *
@ -260,7 +284,7 @@ final class Expectation
public function toHaveCount(int $count, string $message = ''): self public function toHaveCount(int $count, string $message = ''): self
{ {
if (! is_countable($this->value) && ! is_iterable($this->value)) { if (! is_countable($this->value) && ! is_iterable($this->value)) {
InvalidExpectationValue::expected('string'); InvalidExpectationValue::expected('countable|iterable');
} }
Assert::assertCount($count, $this->value, $message); Assert::assertCount($count, $this->value, $message);
@ -268,12 +292,29 @@ final class Expectation
return $this; return $this;
} }
/**
* Asserts that the size of the value and $expected are the same.
*
* @param Countable|iterable<mixed> $expected
* @return self<TValue>
*/
public function toHaveSameSize(Countable|iterable $expected, string $message = ''): self
{
if (! is_countable($this->value) && ! is_iterable($this->value)) {
InvalidExpectationValue::expected('countable|iterable');
}
Assert::assertSameSize($expected, $this->value, $message);
return $this;
}
/** /**
* Asserts that the value contains the property $name. * Asserts that the value contains the property $name.
* *
* @return self<TValue> * @return self<TValue>
*/ */
public function toHaveProperty(string $name, mixed $value = new Any(), string $message = ''): self public function toHaveProperty(string $name, mixed $value = new Any, string $message = ''): self
{ {
$this->toBeObject(); $this->toBeObject();
@ -291,13 +332,13 @@ final class Expectation
/** /**
* Asserts that the value contains the provided properties $names. * Asserts that the value contains the provided properties $names.
* *
* @param iterable<array-key, string> $names * @param iterable<string, mixed>|iterable<int, string> $names
* @return self<TValue> * @return self<TValue>
*/ */
public function toHaveProperties(iterable $names, string $message = ''): self public function toHaveProperties(iterable $names, string $message = ''): self
{ {
foreach ($names as $name => $value) { foreach ($names as $name => $value) {
is_int($name) ? $this->toHaveProperty($value, message: $message) : $this->toHaveProperty($name, $value, $message); is_int($name) ? $this->toHaveProperty($value, message: $message) : $this->toHaveProperty($name, $value, $message); // @phpstan-ignore-line
} }
return $this; return $this;
@ -426,6 +467,18 @@ final class Expectation
return $this; return $this;
} }
/**
* Asserts that the value is a list.
*
* @return self<TValue>
*/
public function toBeList(string $message = ''): self
{
Assert::assertIsList($this->value, $message);
return $this;
}
/** /**
* Asserts that the value is of type bool. * Asserts that the value is of type bool.
* *
@ -498,6 +551,18 @@ final class Expectation
return $this; return $this;
} }
/**
* Asserts that the value contains only digits.
*
* @return self<TValue>
*/
public function toBeDigits(string $message = ''): self
{
Assert::assertTrue(ctype_digit((string) $this->value), $message);
return $this;
}
/** /**
* Asserts that the value is of type object. * Asserts that the value is of type object.
* *
@ -589,7 +654,7 @@ final class Expectation
* *
* @return self<TValue> * @return self<TValue>
*/ */
public function toHaveKey(string|int $key, mixed $value = new Any(), string $message = ''): self public function toHaveKey(string|int $key, mixed $value = new Any, string $message = ''): self
{ {
if (is_object($this->value) && method_exists($this->value, 'toArray')) { if (is_object($this->value) && method_exists($this->value, 'toArray')) {
$array = $this->value->toArray(); $array = $this->value->toArray();
@ -794,6 +859,49 @@ final class Expectation
return $this; return $this;
} }
/**
* Asserts that the value "stringable" matches the given snapshot..
*
* @return self<TValue>
*/
public function toMatchSnapshot(string $message = ''): self
{
$snapshots = TestSuite::getInstance()->snapshots;
$snapshots->startNewExpectation();
$testCase = TestSuite::getInstance()->test;
assert($testCase instanceof TestCase);
$string = match (true) {
is_string($this->value) => $this->value,
is_object($this->value) && method_exists($this->value, 'toSnapshot') => $this->value->toSnapshot(),
is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(),
is_object($this->value) && method_exists($this->value, 'toString') => $this->value->toString(),
$this->value instanceof \Illuminate\Testing\TestResponse => $this->value->getContent(), // @phpstan-ignore-line
is_array($this->value) => json_encode($this->value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
$this->value instanceof Traversable => json_encode(iterator_to_array($this->value), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
$this->value instanceof JsonSerializable => json_encode($this->value->jsonSerialize(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
is_object($this->value) && method_exists($this->value, 'toArray') => json_encode($this->value->toArray(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
default => InvalidExpectationValue::expected('array|object|string'),
};
if ($snapshots->has()) {
[$filename, $content] = $snapshots->get();
Assert::assertSame(
strtr($content, ["\r\n" => "\n", "\r" => "\n"]),
strtr($string, ["\r\n" => "\n", "\r" => "\n"]),
$message === '' ? "Failed asserting that the string value matches its snapshot ($filename)." : $message
);
} else {
$filename = $snapshots->save($string);
TestSuite::getInstance()->registerSnapshotChange("Snapshot created at [$filename]");
}
return $this;
}
/** /**
* Asserts that the value matches a regular expression. * Asserts that the value matches a regular expression.
* *
@ -842,7 +950,7 @@ final class Expectation
* @param (Closure(Throwable): mixed)|string $exception * @param (Closure(Throwable): mixed)|string $exception
* @return self<TValue> * @return self<TValue>
*/ */
public function toThrow(callable|string|Throwable $exception, string $exceptionMessage = null, string $message = ''): self public function toThrow(callable|string|Throwable $exception, ?string $exceptionMessage = null, string $message = ''): self
{ {
$callback = NullClosure::create(); $callback = NullClosure::create();
@ -850,7 +958,7 @@ final class Expectation
$callback = $exception; $callback = $exception;
$parameters = (new ReflectionFunction($exception))->getParameters(); $parameters = (new ReflectionFunction($exception))->getParameters();
if (1 !== count($parameters)) { if (count($parameters) !== 1) {
throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.');
} }
@ -874,7 +982,7 @@ final class Expectation
} }
if (! class_exists($exception)) { if (! class_exists($exception)) {
if ($e instanceof Error && $e->getMessage() === "Class \"$exception\" not found") { if ($e instanceof Error && "Class \"$exception\" not found" === $e->getMessage()) {
Assert::assertTrue(true); Assert::assertTrue(true);
throw $e; throw $e;
@ -890,6 +998,7 @@ final class Expectation
} }
Assert::assertInstanceOf($exception, $e, $message); Assert::assertInstanceOf($exception, $e, $message);
$callback($e); $callback($e);
return $this; return $this;
@ -915,4 +1024,169 @@ final class Expectation
return $this->exporter->shortenedExport($value); return $this->exporter->shortenedExport($value);
} }
/**
* Asserts that the value is uppercase.
*
* @return self<TValue>
*/
public function toBeUppercase(string $message = ''): self
{
Assert::assertTrue(ctype_upper((string) $this->value), $message);
return $this;
}
/**
* Asserts that the value is lowercase.
*
* @return self<TValue>
*/
public function toBeLowercase(string $message = ''): self
{
Assert::assertTrue(ctype_lower((string) $this->value), $message);
return $this;
}
/**
* Asserts that the value is alphanumeric.
*
* @return self<TValue>
*/
public function toBeAlphaNumeric(string $message = ''): self
{
Assert::assertTrue(ctype_alnum((string) $this->value), $message);
return $this;
}
/**
* Asserts that the value is alpha.
*
* @return self<TValue>
*/
public function toBeAlpha(string $message = ''): self
{
Assert::assertTrue(ctype_alpha((string) $this->value), $message);
return $this;
}
/**
* Asserts that the value is snake_case.
*
* @return self<TValue>
*/
public function toBeSnakeCase(string $message = ''): self
{
$value = (string) $this->value;
if ($message === '') {
$message = "Failed asserting that {$value} is snake_case.";
}
Assert::assertTrue((bool) preg_match('/^[\p{Ll}_]+$/u', $value), $message);
return $this;
}
/**
* Asserts that the value is kebab-case.
*
* @return self<TValue>
*/
public function toBeKebabCase(string $message = ''): self
{
$value = (string) $this->value;
if ($message === '') {
$message = "Failed asserting that {$value} is kebab-case.";
}
Assert::assertTrue((bool) preg_match('/^[\p{Ll}-]+$/u', $value), $message);
return $this;
}
/**
* Asserts that the value is camelCase.
*
* @return self<TValue>
*/
public function toBeCamelCase(string $message = ''): self
{
$value = (string) $this->value;
if ($message === '') {
$message = "Failed asserting that {$value} is camelCase.";
}
Assert::assertTrue((bool) preg_match('/^\p{Ll}[\p{Ll}\p{Lu}]+$/u', $value), $message);
return $this;
}
/**
* Asserts that the value is StudlyCase.
*
* @return self<TValue>
*/
public function toBeStudlyCase(string $message = ''): self
{
$value = (string) $this->value;
if ($message === '') {
$message = "Failed asserting that {$value} is StudlyCase.";
}
Assert::assertTrue((bool) preg_match('/^\p{Lu}+\p{Ll}[\p{Ll}\p{Lu}]+$/u', $value), $message);
return $this;
}
/**
* Asserts that the value is UUID.
*
* @return self<TValue>
*/
public function toBeUuid(string $message = ''): self
{
if (! is_string($this->value)) {
InvalidExpectationValue::expected('string');
}
Assert::assertTrue(Str::isUuid($this->value), $message);
return $this;
}
/**
* Asserts that the value is between 2 specified values
*
* @return self<TValue>
*/
public function toBeBetween(int|float|DateTimeInterface $lowestValue, int|float|DateTimeInterface $highestValue, string $message = ''): self
{
Assert::assertGreaterThanOrEqual($lowestValue, $this->value, $message);
Assert::assertLessThanOrEqual($highestValue, $this->value, $message);
return $this;
}
/**
* Asserts that the value is a url
*
* @return self<TValue>
*/
public function toBeUrl(string $message = ''): self
{
if ($message === '') {
$message = "Failed asserting that {$this->value} is a url.";
}
Assert::assertTrue(Str::isUrl((string) $this->value), $message);
return $this;
}
} }

View File

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

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Pest\PendingCalls; namespace Pest\PendingCalls;
use Closure; use Closure;
use Pest\PendingCalls\Concerns\Describable;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\ChainableClosure; use Pest\Support\ChainableClosure;
use Pest\Support\HigherOrderMessageCollection; use Pest\Support\HigherOrderMessageCollection;
@ -16,6 +17,8 @@ use Pest\TestSuite;
*/ */
final class AfterEachCall final class AfterEachCall
{ {
use Describable;
/** /**
* The "afterEach" closure. * The "afterEach" closure.
*/ */
@ -32,11 +35,13 @@ final class AfterEachCall
public function __construct( public function __construct(
private readonly TestSuite $testSuite, private readonly TestSuite $testSuite,
private readonly string $filename, private readonly string $filename,
Closure $closure = null ?Closure $closure = null
) { ) {
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create(); $this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
$this->proxies = new HigherOrderMessageCollection(); $this->proxies = new HigherOrderMessageCollection;
$this->describing = DescribeCall::describing();
} }
/** /**
@ -44,14 +49,23 @@ final class AfterEachCall
*/ */
public function __destruct() public function __destruct()
{ {
$describing = $this->describing;
$proxies = $this->proxies; $proxies = $this->proxies;
$afterEachTestCase = ChainableClosure::boundWhen(
fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line
ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line
)->bindTo($this, self::class);
assert($afterEachTestCase instanceof Closure);
$this->testSuite->afterEach->set( $this->testSuite->afterEach->set(
$this->filename, $this->filename,
ChainableClosure::from(function () use ($proxies): void { $this,
$proxies->chain($this); $afterEachTestCase,
}, $this->closure)
); );
} }
/** /**

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Pest\PendingCalls; namespace Pest\PendingCalls;
use Closure; use Closure;
use Pest\PendingCalls\Concerns\Describable;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\ChainableClosure; use Pest\Support\ChainableClosure;
use Pest\Support\HigherOrderMessageCollection; use Pest\Support\HigherOrderMessageCollection;
@ -16,6 +17,8 @@ use Pest\TestSuite;
*/ */
final class BeforeEachCall final class BeforeEachCall
{ {
use Describable;
/** /**
* Holds the before each closure. * Holds the before each closure.
*/ */
@ -35,14 +38,16 @@ final class BeforeEachCall
* Creates a new Pending Call. * Creates a new Pending Call.
*/ */
public function __construct( public function __construct(
private readonly TestSuite $testSuite, public readonly TestSuite $testSuite,
private readonly string $filename, private readonly string $filename,
Closure $closure = null ?Closure $closure = null
) { ) {
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create(); $this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
$this->testCallProxies = new HigherOrderMessageCollection(); $this->testCallProxies = new HigherOrderMessageCollection;
$this->testCaseProxies = new HigherOrderMessageCollection(); $this->testCaseProxies = new HigherOrderMessageCollection;
$this->describing = DescribeCall::describing();
} }
/** /**
@ -50,16 +55,31 @@ final class BeforeEachCall
*/ */
public function __destruct() public function __destruct()
{ {
$describing = $this->describing;
$testCaseProxies = $this->testCaseProxies; $testCaseProxies = $this->testCaseProxies;
$beforeEachTestCall = function (TestCall $testCall) use ($describing): void {
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
ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line
)->bindTo($this, self::class);
assert($beforeEachTestCase instanceof Closure);
$this->testSuite->beforeEach->set( $this->testSuite->beforeEach->set(
$this->filename, $this->filename,
function (TestCall $testCall): void { $this,
$this->testCallProxies->chain($testCall); $beforeEachTestCall,
}, $beforeEachTestCase,
ChainableClosure::from(function () use ($testCaseProxies): void {
$testCaseProxies->chain($this);
}, $this->closure),
); );
} }

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Pest\PendingCalls\Concerns;
/**
* @internal
*/
trait Describable
{
public ?string $describing = null;
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Pest\PendingCalls;
use Closure;
use Pest\Support\Backtrace;
use Pest\TestSuite;
/**
* @internal
*/
final class DescribeCall
{
/**
* The current describe call.
*/
private static ?string $describing = null;
/**
* Creates a new Pending Call.
*/
public function __construct(
public readonly TestSuite $testSuite,
public readonly string $filename,
public readonly string $description,
public readonly Closure $tests
) {
//
}
/**
* What is the current describing.
*/
public static function describing(): ?string
{
return self::$describing;
}
/**
* Creates the Call.
*/
public function __destruct()
{
self::$describing = $this->description;
try {
($this->tests)();
} finally {
self::$describing = null;
}
}
/**
* Dynamically calls methods on each test call.
*
* @param array<int, mixed> $arguments
*/
public function __call(string $name, array $arguments): BeforeEachCall
{
$filename = Backtrace::file();
$beforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename);
$beforeEachCall->describing = $this->description;
return $beforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line
}
}

View File

@ -5,17 +5,20 @@ declare(strict_types=1);
namespace Pest\PendingCalls; namespace Pest\PendingCalls;
use Closure; use Closure;
use InvalidArgumentException; use Pest\Exceptions\InvalidArgumentException;
use Pest\Factories\Covers\CoversClass; use Pest\Factories\Covers\CoversClass;
use Pest\Factories\Covers\CoversFunction; use Pest\Factories\Covers\CoversFunction;
use Pest\Factories\Covers\CoversNothing; use Pest\Factories\Covers\CoversNothing;
use Pest\Factories\TestCaseMethodFactory; use Pest\Factories\TestCaseMethodFactory;
use Pest\PendingCalls\Concerns\Describable;
use Pest\Plugins\Only; use Pest\Plugins\Only;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\Exporter; use Pest\Support\Exporter;
use Pest\Support\HigherOrderCallables; use Pest\Support\HigherOrderCallables;
use Pest\Support\NullClosure; use Pest\Support\NullClosure;
use Pest\Support\Str;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
@ -25,10 +28,12 @@ use PHPUnit\Framework\TestCase;
*/ */
final class TestCall final class TestCall
{ {
use Describable;
/** /**
* The Test Case Factory. * The Test Case Factory.
*/ */
private readonly TestCaseMethodFactory $testCaseMethod; public readonly TestCaseMethodFactory $testCaseMethod;
/** /**
* If test call is descriptionLess. * If test call is descriptionLess.
@ -41,20 +46,30 @@ final class TestCall
public function __construct( public function __construct(
private readonly TestSuite $testSuite, private readonly TestSuite $testSuite,
private readonly string $filename, private readonly string $filename,
string $description = null, ?string $description = null,
Closure $closure = null ?Closure $closure = null
) { ) {
$this->testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure); $this->testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure);
$this->descriptionLess = $description === null; $this->descriptionLess = $description === null;
$this->testSuite->beforeEach->get($filename)[0]($this); $this->describing = DescribeCall::describing();
$this->testSuite->beforeEach->get($this->filename)[0]($this);
}
/**
* Asserts that the test fails with the given message.
*/
public function fails(?string $message = null): self
{
return $this->throws(AssertionFailedError::class, $message);
} }
/** /**
* Asserts that the test throws the given `$exceptionClass` when called. * Asserts that the test throws the given `$exceptionClass` when called.
*/ */
public function throws(string|int $exception, string $exceptionMessage = null, int $exceptionCode = null): self public function throws(string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self
{ {
if (is_int($exception)) { if (is_int($exception)) {
$exceptionCode = $exception; $exceptionCode = $exception;
@ -86,7 +101,7 @@ final class TestCall
* *
* @param (callable(): bool)|bool $condition * @param (callable(): bool)|bool $condition
*/ */
public function throwsIf(callable|bool $condition, string|int $exception, string $exceptionMessage = null, int $exceptionCode = null): self public function throwsIf(callable|bool $condition, string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self
{ {
$condition = is_callable($condition) $condition = is_callable($condition)
? $condition ? $condition
@ -99,6 +114,24 @@ final class TestCall
return $this; return $this;
} }
/**
* Asserts that the test throws the given `$exceptionClass` when called if the given condition is false.
*
* @param (callable(): bool)|bool $condition
*/
public function throwsUnless(callable|bool $condition, string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self
{
$condition = is_callable($condition)
? $condition
: static fn (): bool => $condition;
if (! $condition()) {
return $this->throws($exception, $exceptionMessage, $exceptionCode);
}
return $this;
}
/** /**
* Runs the current test multiple times with * Runs the current test multiple times with
* each item of the given `iterable`. * each item of the given `iterable`.
@ -175,12 +208,37 @@ final class TestCall
return $this; return $this;
} }
/**
* Skips the current test on the given PHP version.
*/
public function skipOnPhp(string $version): self
{
if (mb_strlen($version) < 2) {
throw new InvalidArgumentException('The version must start with [<] or [>].');
}
if (str_starts_with($version, '>=') || str_starts_with($version, '<=')) {
$operator = substr($version, 0, 2);
$version = substr($version, 2);
} elseif (str_starts_with($version, '>') || str_starts_with($version, '<')) {
$operator = $version[0];
$version = substr($version, 1);
// ensure starts with number:
} elseif (is_numeric($version[0])) {
$operator = '==';
} else {
throw new InvalidArgumentException('The version must start with [<, >, <=, >=] or a number.');
}
return $this->skip(version_compare(PHP_VERSION, $version, $operator), sprintf('This test is skipped on PHP [%s%s].', $operator, $version));
}
/** /**
* Skips the current test if the given test is running on Windows. * Skips the current test if the given test is running on Windows.
*/ */
public function skipOnWindows(): self public function skipOnWindows(): self
{ {
return $this->skipOn('Windows', 'This test is skipped on [Windows].'); return $this->skipOnOs('Windows', 'This test is skipped on [Windows].');
} }
/** /**
@ -188,7 +246,7 @@ final class TestCall
*/ */
public function skipOnMac(): self public function skipOnMac(): self
{ {
return $this->skipOn('Darwin', 'This test is skipped on [Mac].'); return $this->skipOnOs('Darwin', 'This test is skipped on [Mac].');
} }
/** /**
@ -196,19 +254,57 @@ final class TestCall
*/ */
public function skipOnLinux(): self public function skipOnLinux(): self
{ {
return $this->skipOn('Linux', 'This test is skipped on [Linux].'); return $this->skipOnOs('Linux', 'This test is skipped on [Linux].');
} }
/** /**
* Skips the current test if the given test is running on the given operating systems. * Skips the current test if the given test is running on the given operating systems.
*/ */
private function skipOn(string $osFamily, string $message): self private function skipOnOs(string $osFamily, string $message): self
{ {
return PHP_OS_FAMILY === $osFamily return $osFamily === PHP_OS_FAMILY
? $this->skip($message) ? $this->skip($message)
: $this; : $this;
} }
/**
* Skips the current test unless the given test is running on Windows.
*/
public function onlyOnWindows(): self
{
return $this->skipOnMac()->skipOnLinux();
}
/**
* Skips the current test unless the given test is running on Mac.
*/
public function onlyOnMac(): self
{
return $this->skipOnWindows()->skipOnLinux();
}
/**
* Skips the current test unless the given test is running on Linux.
*/
public function onlyOnLinux(): self
{
return $this->skipOnWindows()->skipOnMac();
}
/**
* Repeats the current test the given number of times.
*/
public function repeat(int $times): self
{
if ($times < 1) {
throw new InvalidArgumentException('The number of repetitions must be greater than 0.');
}
$this->testCaseMethod->repetitions = $times;
return $this;
}
/** /**
* Sets the test as "todo". * Sets the test as "todo".
*/ */
@ -273,7 +369,7 @@ final class TestCall
*/ */
public function coversNothing(): self public function coversNothing(): self
{ {
$this->testCaseMethod->covers = [new CoversNothing()]; $this->testCaseMethod->covers = [new CoversNothing];
return $this; return $this;
} }
@ -313,15 +409,17 @@ final class TestCall
* *
* @param array<int, mixed>|null $arguments * @param array<int, mixed>|null $arguments
*/ */
private function addChain(string $file, int $line, string $name, array $arguments = null): self private function addChain(string $file, int $line, string $name, ?array $arguments = null): self
{ {
$exporter = Exporter::default(); $exporter = Exporter::default();
$this->testCaseMethod $this->testCaseMethod
->chains ->chains
->add($file, $line, $name, $arguments); ->add($file, $line, $name, $arguments);
if ($this->descriptionLess) { if ($this->descriptionLess) {
Exporter::default(); Exporter::default();
if ($this->testCaseMethod->description !== null) { if ($this->testCaseMethod->description !== null) {
$this->testCaseMethod->description .= ' → '; $this->testCaseMethod->description .= ' → ';
} }
@ -338,6 +436,11 @@ final class TestCall
*/ */
public function __destruct() public function __destruct()
{ {
if (! is_null($this->describing)) {
$this->testCaseMethod->describing = $this->describing;
$this->testCaseMethod->description = Str::describe($this->describing, $this->testCaseMethod->description); // @phpstan-ignore-line
}
$this->testSuite->tests->set($this->testCaseMethod); $this->testSuite->tests->set($this->testCaseMethod);
} }
} }

View File

@ -66,11 +66,11 @@ final class UsesCall
*/ */
public function in(string ...$targets): void public function in(string ...$targets): void
{ {
$targets = array_map(function ($path): string { $targets = array_map(function (string $path): string {
$startChar = DIRECTORY_SEPARATOR; $startChar = DIRECTORY_SEPARATOR;
if ('\\' === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0) { if ('\\' === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0) {
$path = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', fn ($match): string => strtolower($match['drive']), $path); $path = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', fn (array $match): string => strtolower($match['drive']), $path);
$startChar = strtolower((string) preg_replace('~^([a-z]+:\\\).*$~i', '$1', __DIR__)); $startChar = strtolower((string) preg_replace('~^([a-z]+:\\\).*$~i', '$1', __DIR__));
} }

View File

@ -6,10 +6,10 @@ namespace Pest;
function version(): string function version(): string
{ {
return '2.8.3'; return '2.35.1';
} }
function testDirectory(string $file = ''): string function testDirectory(string $file = ''): string
{ {
return TestSuite::getInstance()->testPath.'/'.$file; return TestSuite::getInstance()->testPath.DIRECTORY_SEPARATOR.$file;
} }

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 * @internal
*/ */
final class CallsShutdown final class CallsTerminable
{ {
/** /**
* Executes the Plugin action. * Executes the Plugin action.
* *
* Provides an opportunity for any plugins to shutdown. * Provides an opportunity for any plugins to terminate.
*/ */
public static function execute(): void public static function execute(): void
{ {
$plugins = Loader::getPlugins(Plugins\Shutdownable::class); $plugins = Loader::getPlugins(Plugins\Terminable::class);
/** @var Plugins\Shutdownable $plugin */ /** @var Plugins\Terminable $plugin */
foreach ($plugins as $plugin) { foreach ($plugins as $plugin) {
$plugin->shutdown(); $plugin->terminate();
} }
} }
} }

View File

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

View File

@ -16,7 +16,17 @@ trait HandleArguments
*/ */
public function hasArgument(string $argument, array $arguments): bool public function hasArgument(string $argument, array $arguments): bool
{ {
return in_array($argument, $arguments, true); foreach ($arguments as $arg) {
if ($arg === $argument) {
return true;
}
if (str_starts_with($arg, "$argument=")) {
return true;
}
}
return false;
} }
/** /**
@ -44,6 +54,6 @@ trait HandleArguments
unset($arguments[$argument]); unset($arguments[$argument]);
return array_flip($arguments); return array_values(array_flip($arguments));
} }
} }

View File

@ -50,7 +50,7 @@ final class Coverage implements AddsOutput, HandlesArguments
*/ */
public function handleArguments(array $originals): array public function handleArguments(array $originals): array
{ {
$arguments = [...[''], ...array_values(array_filter($originals, function ($original): bool { $arguments = [...[''], ...array_values(array_filter($originals, function (string $original): bool {
foreach ([self::COVERAGE_OPTION, self::MIN_OPTION] as $option) { foreach ([self::COVERAGE_OPTION, self::MIN_OPTION] as $option) {
if ($original === sprintf('--%s', $option)) { if ($original === sprintf('--%s', $option)) {
return true; return true;
@ -128,9 +128,9 @@ final class Coverage implements AddsOutput, HandlesArguments
if ($exitCode === 1) { if ($exitCode === 1) {
$this->output->writeln(sprintf( $this->output->writeln(sprintf(
"\n <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected:<fg=red;options=bold> %s %%</>. Minimum:<fg=white;options=bold> %s %%</>.", "\n <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected <fg=white;options=bold> %s %%</>, currently <fg=red;options=bold> %s %%</>.",
number_format($coverage, 1), number_format($this->coverageMin, 1),
number_format($this->coverageMin, 1) number_format($coverage, 1)
)); ));
} }

View File

@ -45,7 +45,7 @@ final class Environment implements HandlesArguments
/** /**
* Gets the environment name. * Gets the environment name.
*/ */
public static function name(string $name = null): string public static function name(?string $name = null): string
{ {
if (is_string($name)) { if (is_string($name)) {
self::$name = $name; self::$name = $name;

View File

@ -6,10 +6,11 @@ namespace Pest\Plugins;
use Pest\Contracts\Plugins\HandlesArguments; use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Support\View; use Pest\Support\View;
use function Pest\version;
use PHPUnit\TextUI\Help as PHPUnitHelp; use PHPUnit\TextUI\Help as PHPUnitHelp;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use function Pest\version;
/** /**
* @internal * @internal
*/ */
@ -60,6 +61,10 @@ final class Help implements HandlesArguments
assert(is_string($argument)); assert(is_string($argument));
if (trim($argument) === '--process-isolation') {
continue;
}
View::render('components.two-column-detail', [ View::render('components.two-column-detail', [
'left' => $this->colorizeOptions($argument), 'left' => $this->colorizeOptions($argument),
'right' => preg_replace(['/</', '/>/'], ['[', ']'], $description), 'right' => preg_replace(['/</', '/>/'], ['[', ']'], $description),
@ -92,10 +97,9 @@ final class Help implements HandlesArguments
*/ */
private function getContent(): array private function getContent(): array
{ {
$helpReflection = new \ReflectionClass(PHPUnitHelp::class); $helpReflection = new PHPUnitHelp;
/** @var array<string, array<int, array{arg: string, desc: string}>> $content */ $content = (fn (): array => $this->elements())->call($helpReflection);
$content = $helpReflection->getConstant('HELP_TEXT');
$content['Configuration'] = [...[[ $content['Configuration'] = [...[[
'arg' => '--init', 'arg' => '--init',
@ -107,6 +111,10 @@ final class Help implements HandlesArguments
'arg' => '--parallel', 'arg' => '--parallel',
'desc' => 'Run tests in parallel', 'desc' => 'Run tests in parallel',
], ],
[
'arg' => '--update-snapshots',
'desc' => 'Update snapshots for tests using the "toMatchSnapshot" expectation',
],
], ...$content['Execution']]; ], ...$content['Execution']];
$content['Selection'] = [[ $content['Selection'] = [[

View File

@ -4,13 +4,13 @@ declare(strict_types=1);
namespace Pest\Plugins; namespace Pest\Plugins;
use Pest\Contracts\Plugins\Shutdownable; use Pest\Contracts\Plugins\Terminable;
use Pest\PendingCalls\TestCall; use Pest\PendingCalls\TestCall;
/** /**
* @internal * @internal
*/ */
final class Only implements Shutdownable final class Only implements Terminable
{ {
/** /**
* The temporary folder. * The temporary folder.
@ -26,7 +26,7 @@ final class Only implements Shutdownable
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function shutdown(): void public function terminate(): void
{ {
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
@ -40,6 +40,10 @@ final class Only implements Shutdownable
*/ */
public static function enable(TestCall $testCall): void public static function enable(TestCall $testCall): void
{ {
if (Environment::name() == Environment::CI) {
return;
}
$testCall->group('__pest_only'); $testCall->group('__pest_only');
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';

View File

@ -13,11 +13,12 @@ use Pest\Plugins\Parallel\Paratest\CleanConsoleOutput;
use Pest\Support\Arr; use Pest\Support\Arr;
use Pest\Support\Container; use Pest\Support\Container;
use Pest\TestSuite; use Pest\TestSuite;
use function Pest\version;
use Stringable; use Stringable;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArgvInput;
use function Pest\version;
final class Parallel implements HandlesArguments final class Parallel implements HandlesArguments
{ {
use HandleArguments; use HandleArguments;
@ -33,14 +34,14 @@ final class Parallel implements HandlesArguments
/** /**
* @var string[] * @var string[]
*/ */
private const UNSUPPORTED_ARGUMENTS = ['--todos', '--retry']; private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry'];
/** /**
* Whether the given command line arguments indicate that the test suite should be run in parallel. * Whether the given command line arguments indicate that the test suite should be run in parallel.
*/ */
public static function isEnabled(): bool public static function isEnabled(): bool
{ {
$argv = new ArgvInput(); $argv = new ArgvInput;
if ($argv->hasParameterOption('--parallel')) { if ($argv->hasParameterOption('--parallel')) {
return true; return true;
} }
@ -115,17 +116,17 @@ final class Parallel implements HandlesArguments
private function runTestSuiteInParallel(array $arguments): int private function runTestSuiteInParallel(array $arguments): int
{ {
$handlers = array_filter( $handlers = array_filter(
array_map(fn ($handler): object|string => Container::getInstance()->get($handler), self::HANDLERS), array_map(fn (string $handler): object|string => Container::getInstance()->get($handler), self::HANDLERS),
fn ($handler): bool => $handler instanceof HandlesArguments, fn (object|string $handler): bool => $handler instanceof HandlesArguments,
); );
$filteredArguments = array_reduce( $filteredArguments = array_reduce(
$handlers, $handlers,
fn ($arguments, HandlesArguments $handler): array => $handler->handleArguments($arguments), fn (array $arguments, HandlesArguments $handler): array => $handler->handleArguments($arguments),
$arguments $arguments
); );
$exitCode = $this->paratestCommand()->run(new ArgvInput($filteredArguments), new CleanConsoleOutput()); $exitCode = $this->paratestCommand()->run(new ArgvInput($filteredArguments), new CleanConsoleOutput);
return CallsAddsOutput::execute($exitCode); return CallsAddsOutput::execute($exitCode);
} }
@ -139,13 +140,13 @@ final class Parallel implements HandlesArguments
private function runWorkerHandlers(array $arguments): array private function runWorkerHandlers(array $arguments): array
{ {
$handlers = array_filter( $handlers = array_filter(
array_map(fn ($handler): object|string => Container::getInstance()->get($handler), self::HANDLERS), array_map(fn (string $handler): object|string => Container::getInstance()->get($handler), self::HANDLERS),
fn ($handler): bool => $handler instanceof HandlersWorkerArguments, fn (object|string $handler): bool => $handler instanceof HandlersWorkerArguments,
); );
return array_reduce( return array_reduce(
$handlers, $handlers,
fn ($arguments, HandlersWorkerArguments $handler): array => $handler->handleWorkerArguments($arguments), fn (array $arguments, HandlersWorkerArguments $handler): array => $handler->handleWorkerArguments($arguments),
$arguments $arguments
); );
} }
@ -172,7 +173,7 @@ final class Parallel implements HandlesArguments
*/ */
private function hasArgumentsThatWouldBeFasterWithoutParallel(): bool private function hasArgumentsThatWouldBeFasterWithoutParallel(): bool
{ {
$arguments = new ArgvInput(); $arguments = new ArgvInput;
foreach (self::UNSUPPORTED_ARGUMENTS as $unsupportedArgument) { foreach (self::UNSUPPORTED_ARGUMENTS as $unsupportedArgument) {
if ($arguments->hasParameterOption($unsupportedArgument)) { if ($arguments->hasParameterOption($unsupportedArgument)) {

View File

@ -30,7 +30,7 @@ final class Parallel implements HandlesArguments
*/ */
public function handleArguments(array $arguments): array public function handleArguments(array $arguments): array
{ {
$args = array_reduce(self::ARGS_TO_REMOVE, fn ($args, $arg): array => $this->popArgument($arg, $args), $arguments); $args = array_reduce(self::ARGS_TO_REMOVE, fn (array $args, string $arg): array => $this->popArgument($arg, $args), $arguments);
return $this->pushArgument('--runner='.WrapperRunner::class, $args); return $this->pushArgument('--runner='.WrapperRunner::class, $args);
} }

View File

@ -4,6 +4,16 @@ declare(strict_types=1);
namespace Pest\Plugins\Parallel\Paratest; namespace Pest\Plugins\Parallel\Paratest;
use ParaTest\Options;
use Pest\Plugins\Parallel\Support\CompactPrinter;
use Pest\Support\StateGenerator;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Output\Printer;
use SebastianBergmann\Timer\Duration;
use SplFileInfo;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Output\OutputInterface;
use function assert; use function assert;
use function fclose; use function fclose;
use function feof; use function feof;
@ -12,16 +22,7 @@ use function fread;
use function fseek; use function fseek;
use function ftell; use function ftell;
use function fwrite; use function fwrite;
use ParaTest\Options;
use Pest\Plugins\Parallel\Support\CompactPrinter;
use Pest\Support\StateGenerator;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Output\Printer;
use SebastianBergmann\Timer\Duration;
use SplFileInfo;
use function strlen; use function strlen;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Output\OutputInterface;
/** /**
* @internal * @internal
@ -62,8 +63,7 @@ final class ResultPrinter
{ {
public function __construct( public function __construct(
private readonly OutputInterface $output, private readonly OutputInterface $output,
) { ) {}
}
public function print(string $buffer): void public function print(string $buffer): void
{ {
@ -78,9 +78,7 @@ final class ResultPrinter
$this->output->write(OutputFormatter::escape($buffer)); $this->output->write(OutputFormatter::escape($buffer));
} }
public function flush(): void public function flush(): void {}
{
}
}; };
$this->compactPrinter = CompactPrinter::default(); $this->compactPrinter = CompactPrinter::default();
@ -171,7 +169,7 @@ final class ResultPrinter
return; return;
} }
$state = (new StateGenerator())->fromPhpUnitTestResult($this->passedTests, $testResult); $state = (new StateGenerator)->fromPhpUnitTestResult($this->passedTests, $testResult);
$this->compactPrinter->errors($state); $this->compactPrinter->errors($state);
$this->compactPrinter->recap($state, $testResult, $duration, $this->options); $this->compactPrinter->recap($state, $testResult, $duration, $this->options);

View File

@ -4,15 +4,9 @@ declare(strict_types=1);
namespace Pest\Plugins\Parallel\Paratest; namespace Pest\Plugins\Parallel\Paratest;
use function array_merge;
use function array_merge_recursive;
use function array_shift;
use function assert;
use function count;
use const DIRECTORY_SEPARATOR; use const DIRECTORY_SEPARATOR;
use function dirname;
use function file_get_contents; use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection;
use function max;
use ParaTest\Coverage\CoverageMerger; use ParaTest\Coverage\CoverageMerger;
use ParaTest\JUnit\LogMerger; use ParaTest\JUnit\LogMerger;
use ParaTest\JUnit\Writer; use ParaTest\JUnit\Writer;
@ -23,16 +17,26 @@ use ParaTest\WrapperRunner\WrapperWorker;
use Pest\Result; use Pest\Result;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Event\TestRunner\WarningTriggered;
use PHPUnit\Runner\CodeCoverage; use PHPUnit\Runner\CodeCoverage;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade; use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry; use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
use PHPUnit\Util\ExcludeList; use PHPUnit\Util\ExcludeList;
use function realpath;
use SebastianBergmann\Timer\Timer; use SebastianBergmann\Timer\Timer;
use SplFileInfo; use SplFileInfo;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\PhpExecutableFinder;
use function array_merge;
use function array_merge_recursive;
use function array_shift;
use function assert;
use function count;
use function dirname;
use function file_get_contents;
use function max;
use function realpath;
use function unlink; use function unlink;
use function unserialize; use function unserialize;
use function usleep; use function usleep;
@ -87,13 +91,13 @@ final class WrapperRunner implements RunnerInterface
private readonly OutputInterface $output private readonly OutputInterface $output
) { ) {
$this->printer = new ResultPrinter($output, $options); $this->printer = new ResultPrinter($output, $options);
$this->timer = new Timer(); $this->timer = new Timer;
$wrapper = realpath( $wrapper = realpath(
dirname(__DIR__, 4).DIRECTORY_SEPARATOR.'bin'.DIRECTORY_SEPARATOR.'worker.php', dirname(__DIR__, 4).DIRECTORY_SEPARATOR.'bin'.DIRECTORY_SEPARATOR.'worker.php',
); );
assert($wrapper !== false); assert($wrapper !== false);
$phpFinder = new PhpExecutableFinder(); $phpFinder = new PhpExecutableFinder;
$phpBin = $phpFinder->find(false); $phpBin = $phpFinder->find(false);
assert($phpBin !== false); assert($phpBin !== false);
$parameters = [$phpBin]; $parameters = [$phpBin];
@ -106,7 +110,7 @@ final class WrapperRunner implements RunnerInterface
$parameters[] = $wrapper; $parameters[] = $wrapper;
$this->parameters = $parameters; $this->parameters = $parameters;
$this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry(); $this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry;
} }
public function run(): int public function run(): int
@ -250,11 +254,11 @@ final class WrapperRunner implements RunnerInterface
private function destroyWorker(int $token): void private function destroyWorker(int $token): void
{ {
// Mutation Testing tells us that the following `unset()` already destroys
// the `WrapperWorker`, which destroys the Symfony's `Process`, which
// automatically calls `Process::stop` within `Process::__destruct()`.
// But we prefer to have an explicit stops.
$this->workers[$token]->stop(); $this->workers[$token]->stop();
// We need to wait for ApplicationForWrapperWorker::end to end
while ($this->workers[$token]->isRunning()) {
usleep(self::CYCLE_SLEEP);
}
unset($this->workers[$token]); unset($this->workers[$token]);
} }
@ -272,7 +276,7 @@ final class WrapperRunner implements RunnerInterface
assert($testResult instanceof TestResult); assert($testResult instanceof TestResult);
$testResultSum = new TestResult( $testResultSum = new TestResult(
$testResultSum->numberOfTests() + $testResult->numberOfTests(), (int) $testResultSum->hasTests() + (int) $testResult->hasTests(),
$testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(), $testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
$testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(), $testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(),
array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()), array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()),
@ -281,23 +285,24 @@ final class WrapperRunner implements RunnerInterface
array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()), array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()),
array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()), array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()),
array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()), array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()),
array_merge_recursive($testResultSum->testTriggeredDeprecationEvents(), $testResult->testTriggeredDeprecationEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpDeprecationEvents(), $testResult->testTriggeredPhpDeprecationEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()),
array_merge_recursive($testResultSum->testTriggeredErrorEvents(), $testResult->testTriggeredErrorEvents()),
array_merge_recursive($testResultSum->testTriggeredNoticeEvents(), $testResult->testTriggeredNoticeEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpNoticeEvents(), $testResult->testTriggeredPhpNoticeEvents()),
array_merge_recursive($testResultSum->testTriggeredWarningEvents(), $testResult->testTriggeredWarningEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpWarningEvents(), $testResult->testTriggeredPhpWarningEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()),
array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()), array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()),
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()), array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
array_merge_recursive($testResultSum->errors(), $testResult->errors()),
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
array_merge_recursive($testResultSum->notices(), $testResult->notices()),
array_merge_recursive($testResultSum->warnings(), $testResult->warnings()),
array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()),
array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()),
array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()),
$testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
); );
} }
$testResultSum = new TestResult( $testResultSum = new TestResult(
$testResultSum->numberOfTests(), ResultReflection::numberOfTests($testResultSum),
$testResultSum->numberOfTestsRun(), $testResultSum->numberOfTestsRun(),
$testResultSum->numberOfAssertions(), $testResultSum->numberOfAssertions(),
$testResultSum->testErroredEvents(), $testResultSum->testErroredEvents(),
@ -306,18 +311,23 @@ final class WrapperRunner implements RunnerInterface
$testResultSum->testSuiteSkippedEvents(), $testResultSum->testSuiteSkippedEvents(),
$testResultSum->testSkippedEvents(), $testResultSum->testSkippedEvents(),
$testResultSum->testMarkedIncompleteEvents(), $testResultSum->testMarkedIncompleteEvents(),
$testResultSum->testTriggeredDeprecationEvents(),
$testResultSum->testTriggeredPhpDeprecationEvents(),
$testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResultSum->testTriggeredPhpunitDeprecationEvents(),
$testResultSum->testTriggeredErrorEvents(),
$testResultSum->testTriggeredNoticeEvents(),
$testResultSum->testTriggeredPhpNoticeEvents(),
$testResultSum->testTriggeredWarningEvents(),
$testResultSum->testTriggeredPhpWarningEvents(),
$testResultSum->testTriggeredPhpunitErrorEvents(), $testResultSum->testTriggeredPhpunitErrorEvents(),
$testResultSum->testTriggeredPhpunitWarningEvents(), $testResultSum->testTriggeredPhpunitWarningEvents(),
$testResultSum->testRunnerTriggeredDeprecationEvents(), $testResultSum->testRunnerTriggeredDeprecationEvents(),
array_values(array_filter($testResultSum->testRunnerTriggeredWarningEvents(), fn ($event): bool => ! str_contains($event->message(), 'No tests found'))), array_values(array_filter(
$testResultSum->testRunnerTriggeredWarningEvents(),
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')
)),
$testResultSum->errors(),
$testResultSum->deprecations(),
$testResultSum->notices(),
$testResultSum->warnings(),
$testResultSum->phpDeprecations(),
$testResultSum->phpNotices(),
$testResultSum->phpWarnings(),
$testResultSum->numberOfIssuesIgnoredByBaseline(),
); );
$this->printer->printResults( $this->printer->printResults(
@ -347,12 +357,21 @@ final class WrapperRunner implements RunnerInterface
return; return;
} }
$coverageManager = new CodeCoverage(); $coverageManager = new CodeCoverage;
$coverageManager->init( $coverageManager->init(
$this->options->configuration, $this->options->configuration,
$this->codeCoverageFilterRegistry, $this->codeCoverageFilterRegistry,
false, 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()); $coverageMerger = new CoverageMerger($coverageManager->codeCoverage());
foreach ($this->coverageFiles as $coverageFile) { foreach ($this->coverageFiles as $coverageFile) {
$coverageMerger->addCoverageFromFile($coverageFile); $coverageMerger->addCoverageFromFile($coverageFile);
@ -370,8 +389,8 @@ final class WrapperRunner implements RunnerInterface
return; return;
} }
$testSuite = (new LogMerger())->merge($this->junitFiles); $testSuite = (new LogMerger)->merge($this->junitFiles);
(new Writer())->write( (new Writer)->write(
$testSuite, $testSuite,
$this->options->configuration->logfileJunit(), $this->options->configuration->logfileJunit(),
); );

View File

@ -16,8 +16,9 @@ use PHPUnit\TestRunner\TestResult\TestResult as PHPUnitTestResult;
use SebastianBergmann\Timer\Duration; use SebastianBergmann\Timer\Duration;
use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use function Termwind\render;
use Termwind\Terminal; use Termwind\Terminal;
use function Termwind\render;
use function Termwind\terminal; use function Termwind\terminal;
/** /**
@ -134,6 +135,10 @@ final class CompactPrinter
null, null,
null, null,
null, null,
null,
null,
null,
null,
); );
$telemetry = new Info( $telemetry = new Info(

View File

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

35
src/Plugins/Snapshot.php Normal file
View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Exceptions\InvalidOption;
use Pest\TestSuite;
/**
* @internal
*/
final class Snapshot implements HandlesArguments
{
use Concerns\HandleArguments;
/**
* {@inheritDoc}
*/
public function handleArguments(array $arguments): array
{
if (! $this->hasArgument('--update-snapshots', $arguments)) {
return $arguments;
}
if ($this->hasArgument('--parallel', $arguments)) {
throw new InvalidOption('The [--update-snapshots] option is not supported when running in parallel.');
}
TestSuite::getInstance()->snapshots->flush();
return $this->popArgument('--update-snapshots', $arguments);
}
}

38
src/Plugins/Verbose.php Normal file
View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins;
use Pest\Contracts\Plugins\HandlesArguments;
/**
* @internal
*/
final class Verbose implements HandlesArguments
{
use Concerns\HandleArguments;
/**
* The list of verbosity levels.
*/
private const VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
/**
* {@inheritDoc}
*/
public function handleArguments(array $arguments): array
{
foreach (self::VERBOSITY_LEVELS as $level) {
if ($this->hasArgument('-'.$level, $arguments)) {
$arguments = $this->popArgument('-'.$level, $arguments);
}
}
if ($this->hasArgument('--quiet', $arguments)) {
return $this->popArgument('--quiet', $arguments);
}
return $arguments;
}
}

View File

@ -6,6 +6,7 @@ namespace Pest\Plugins;
use Pest\Contracts\Plugins\HandlesArguments; use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Support\View; use Pest\Support\View;
use function Pest\version; use function Pest\version;
/** /**

View File

@ -6,7 +6,7 @@ namespace Pest\Repositories;
use Closure; use Closure;
use Mockery; use Mockery;
use Pest\Exceptions\AfterEachAlreadyExist; use Pest\PendingCalls\AfterEachCall;
use Pest\Support\ChainableClosure; use Pest\Support\ChainableClosure;
use Pest\Support\NullClosure; use Pest\Support\NullClosure;
@ -23,13 +23,18 @@ final class AfterEachRepository
/** /**
* Sets a after each closure. * Sets a after each closure.
*/ */
public function set(string $filename, Closure $closure): void public function set(string $filename, AfterEachCall $afterEachCall, Closure $afterEachTestCase): void
{ {
if (array_key_exists($filename, $this->state)) { if (array_key_exists($filename, $this->state)) {
throw new AfterEachAlreadyExist($filename); $fromAfterEachTestCase = $this->state[$filename];
$afterEachTestCase = ChainableClosure::bound($fromAfterEachTestCase, $afterEachTestCase)
->bindTo($afterEachCall, $afterEachCall::class);
} }
$this->state[$filename] = $closure; assert($afterEachTestCase instanceof Closure);
$this->state[$filename] = $afterEachTestCase;
} }
/** /**
@ -39,7 +44,7 @@ final class AfterEachRepository
{ {
$afterEach = $this->state[$filename] ?? NullClosure::create(); $afterEach = $this->state[$filename] ?? NullClosure::create();
return ChainableClosure::from(function (): void { return ChainableClosure::bound(function (): void {
if (class_exists(Mockery::class)) { if (class_exists(Mockery::class)) {
if ($container = Mockery::getContainer()) { if ($container = Mockery::getContainer()) {
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Pest\Repositories; namespace Pest\Repositories;
use Closure; use Closure;
use Pest\Exceptions\BeforeEachAlreadyExist; use Pest\Exceptions\BeforeAllAlreadyExist;
use Pest\Support\NullClosure; use Pest\Support\NullClosure;
use Pest\Support\Reflection; use Pest\Support\Reflection;
@ -39,7 +39,7 @@ final class BeforeAllRepository
$filename = Reflection::getFileNameFromClosure($closure); $filename = Reflection::getFileNameFromClosure($closure);
if (array_key_exists($filename, $this->state)) { if (array_key_exists($filename, $this->state)) {
throw new BeforeEachAlreadyExist($filename); throw new BeforeAllAlreadyExist($filename);
} }
$this->state[$filename] = $closure; $this->state[$filename] = $closure;

View File

@ -5,7 +5,8 @@ declare(strict_types=1);
namespace Pest\Repositories; namespace Pest\Repositories;
use Closure; use Closure;
use Pest\Exceptions\BeforeEachAlreadyExist; use Pest\PendingCalls\BeforeEachCall;
use Pest\Support\ChainableClosure;
use Pest\Support\NullClosure; use Pest\Support\NullClosure;
/** /**
@ -21,10 +22,14 @@ final class BeforeEachRepository
/** /**
* Sets a before each closure. * Sets a before each closure.
*/ */
public function set(string $filename, Closure $beforeEachTestCall, Closure $beforeEachTestCase): void public function set(string $filename, BeforeEachCall $beforeEachCall, Closure $beforeEachTestCall, Closure $beforeEachTestCase): void
{ {
if (array_key_exists($filename, $this->state)) { if (array_key_exists($filename, $this->state)) {
throw new BeforeEachAlreadyExist($filename); [$fromBeforeEachTestCall, $fromBeforeEachTestCase] = $this->state[$filename];
$beforeEachTestCall = ChainableClosure::unbound($fromBeforeEachTestCall, $beforeEachTestCall);
$beforeEachTestCase = ChainableClosure::bound($fromBeforeEachTestCase, $beforeEachTestCase)->bindTo($beforeEachCall, $beforeEachCall::class);
assert($beforeEachTestCase instanceof Closure);
} }
$this->state[$filename] = [$beforeEachTestCall, $beforeEachTestCase]; $this->state[$filename] = [$beforeEachTestCall, $beforeEachTestCase];

View File

@ -10,9 +10,10 @@ use Pest\Exceptions\DatasetAlreadyExists;
use Pest\Exceptions\DatasetDoesNotExist; use Pest\Exceptions\DatasetDoesNotExist;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
use Pest\Support\Exporter; use Pest\Support\Exporter;
use function sprintf;
use Traversable; use Traversable;
use function sprintf;
/** /**
* @internal * @internal
*/ */
@ -66,11 +67,11 @@ final class DatasetsRepository
} }
/** /**
* @return Closure|array<int|string, mixed>|never * @return Closure|array<int|string, mixed>
* *
* @throws ShouldNotHappen * @throws ShouldNotHappen
*/ */
public static function get(string $filename, string $description) public static function get(string $filename, string $description): Closure|array
{ {
$dataset = self::$withs[$filename.self::SEPARATOR.$description]; $dataset = self::$withs[$filename.self::SEPARATOR.$description];
@ -138,7 +139,7 @@ final class DatasetsRepository
/** /**
* @param array<Closure|iterable<int|string, mixed>|string> $datasets * @param array<Closure|iterable<int|string, mixed>|string> $datasets
* @return array<array<mixed>> * @return array<int, array<int, mixed>>
*/ */
private static function processDatasets(array $datasets, string $currentTestFile): array private static function processDatasets(array $datasets, string $currentTestFile): array
{ {
@ -193,7 +194,7 @@ final class DatasetsRepository
$closestScopeDatasetKey = array_reduce( $closestScopeDatasetKey = array_reduce(
array_keys($matchingDatasets), array_keys($matchingDatasets),
fn ($keyA, $keyB) => $keyA !== null && strlen((string) $keyA) > strlen($keyB) ? $keyA : $keyB fn (string|int|null $keyA, string|int|null $keyB): string|int|null => $keyA !== null && strlen((string) $keyA) > strlen((string) $keyB) ? $keyA : $keyB
); );
if ($closestScopeDatasetKey === null) { if ($closestScopeDatasetKey === null) {

View File

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Pest\Repositories;
use Pest\Exceptions\ShouldNotHappen;
use Pest\TestSuite;
/**
* @internal
*/
final class SnapshotRepository
{
/** @var array<string, int> */
private static array $expectationsCounter = [];
/**
* Creates a snapshot repository instance.
*/
public function __construct(
readonly private string $testsPath,
readonly private string $snapshotsPath,
) {}
/**
* Checks if the snapshot exists.
*/
public function has(): bool
{
return file_exists($this->getSnapshotFilename());
}
/**
* Gets the snapshot.
*
* @return array{0: string, 1: string}
*
* @throws ShouldNotHappen
*/
public function get(): array
{
$contents = file_get_contents($snapshotFilename = $this->getSnapshotFilename());
if ($contents === false) {
throw ShouldNotHappen::fromMessage('Snapshot file could not be read.');
}
$snapshot = str_replace(dirname($this->testsPath).'/', '', $snapshotFilename);
return [$snapshot, $contents];
}
/**
* Saves the given snapshot for the given test case.
*/
public function save(string $snapshot): string
{
$snapshotFilename = $this->getSnapshotFilename();
if (! file_exists(dirname($snapshotFilename))) {
mkdir(dirname($snapshotFilename), 0755, true);
}
file_put_contents($snapshotFilename, $snapshot);
return str_replace(dirname($this->testsPath).'/', '', $snapshotFilename);
}
/**
* Flushes the snapshots.
*/
public function flush(): void
{
$absoluteSnapshotsPath = $this->testsPath.'/'.$this->snapshotsPath;
$deleteDirectory = function (string $path) use (&$deleteDirectory): void {
if (file_exists($path)) {
$scannedDir = scandir($path);
assert(is_array($scannedDir));
$files = array_diff($scannedDir, ['.', '..']);
foreach ($files as $file) {
if (is_dir($path.'/'.$file)) {
$deleteDirectory($path.'/'.$file);
} else {
unlink($path.'/'.$file);
}
}
rmdir($path);
}
};
if (file_exists($absoluteSnapshotsPath)) {
$deleteDirectory($absoluteSnapshotsPath);
}
}
/**
* Gets the snapshot's "filename".
*/
private function getSnapshotFilename(): string
{
$relativePath = str_replace($this->testsPath, '', TestSuite::getInstance()->getFilename());
// remove extension from filename
$relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.'));
$description = TestSuite::getInstance()->getDescription();
if ($this->getCurrentSnapshotCounter() > 1) {
$description .= '__'.$this->getCurrentSnapshotCounter();
}
return sprintf('%s/%s.snap', $this->testsPath.'/'.$this->snapshotsPath.$relativePath, $description);
}
private function getCurrentSnapshotKey(): string
{
return TestSuite::getInstance()->getFilename().'###'.TestSuite::getInstance()->getDescription();
}
private function getCurrentSnapshotCounter(): int
{
return self::$expectationsCounter[$this->getCurrentSnapshotKey()] ?? 0;
}
public function startNewExpectation(): void
{
$key = $this->getCurrentSnapshotKey();
if (! isset(self::$expectationsCounter[$key])) {
self::$expectationsCounter[$key] = 0;
}
self::$expectationsCounter[$key]++;
}
}

View File

@ -25,17 +25,17 @@ final class TestRepository
private array $testCases = []; private array $testCases = [];
/** /**
* @var array<string, array{0: array<int, string>, 1: array<int, string>, 2: array<int, string|Closure>}> * @var array<string, array{0: array<int, string>, 1: array<int, string>, 2: array<int, array<int, string|Closure>>}>
*/ */
private array $uses = []; private array $uses = [];
/** /**
* @var array<int, TestCaseFilter> * @var array<int, TestCaseFilter>
*/ */
private array $testCaseFilters = []; private array $testCaseFilters = [];
/** /**
* @var array<int, TestCaseMethodFilter> * @var array<int, TestCaseMethodFilter>
*/ */
private array $testCaseMethodFilters = []; private array $testCaseMethodFilters = [];
@ -77,12 +77,17 @@ final class TestRepository
throw new TestCaseClassOrTraitNotFound($classOrTrait); throw new TestCaseClassOrTraitNotFound($classOrTrait);
} }
$hooks = array_map(fn (Closure $hook): array => [$hook], $hooks);
foreach ($paths as $path) { foreach ($paths as $path) {
if (array_key_exists($path, $this->uses)) { if (array_key_exists($path, $this->uses)) {
$this->uses[$path] = [ $this->uses[$path] = [
[...$this->uses[$path][0], ...$classOrTraits], [...$this->uses[$path][0], ...$classOrTraits],
[...$this->uses[$path][1], ...$groups], [...$this->uses[$path][1], ...$groups],
$this->uses[$path][2] + $hooks, array_map(
fn (int $index): array => [...$this->uses[$path][2][$index] ?? [], ...($hooks[$index] ?? [])],
range(0, 3),
),
]; ];
} else { } else {
$this->uses[$path] = [$classOrTraits, $groups, $hooks]; $this->uses[$path] = [$classOrTraits, $groups, $hooks];
@ -189,10 +194,11 @@ final class TestRepository
$method->groups = [...$groups, ...$method->groups]; $method->groups = [...$groups, ...$method->groups];
} }
$testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeAll', [$hooks[0] ?? null]); foreach (['__addBeforeAll', '__addBeforeEach', '__addAfterEach', '__addAfterAll'] as $index => $name) {
$testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeEach', [$hooks[1] ?? null]); foreach ($hooks[$index] ?? [null] as $hook) {
$testCase->factoryProxies->add($testCase->filename, 0, '__addAfterEach', [$hooks[2] ?? null]); $testCase->factoryProxies->add($testCase->filename, 0, $name, [$hook]);
$testCase->factoryProxies->add($testCase->filename, 0, '__addAfterAll', [$hooks[3] ?? null]); }
}
} }
} }

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest; namespace Pest;
use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection;
use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Configuration\Configuration;
@ -44,7 +45,7 @@ final class Result
return self::SUCCESS_EXIT; return self::SUCCESS_EXIT;
} }
if ($configuration->failOnEmptyTestSuite() && $result->numberOfTests() === 0) { if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) {
return self::FAILURE_EXIT; return self::FAILURE_EXIT;
} }
@ -54,8 +55,8 @@ final class Result
} }
$warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents() $warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents()
+ $result->numberOfTestsWithTestTriggeredWarningEvents() + count($result->warnings())
+ $result->numberOfTestsWithTestTriggeredPhpWarningEvents(); + count($result->phpWarnings());
if ($configuration->failOnWarning() && $warnings > 0) { if ($configuration->failOnWarning() && $warnings > 0) {
$returnCode = self::FAILURE_EXIT; $returnCode = self::FAILURE_EXIT;

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Pest\Subscribers; namespace Pest\Subscribers;
use Pest\Logging\TeamCity\Converter; use Pest\Logging\Converter;
use Pest\Logging\TeamCity\TeamCityLogger; use Pest\Logging\TeamCity\TeamCityLogger;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Event\TestRunner\Configured; use PHPUnit\Event\TestRunner\Configured;
@ -24,8 +24,7 @@ final class EnsureTeamCityEnabled implements ConfiguredSubscriber
private readonly InputInterface $input, private readonly InputInterface $input,
private readonly OutputInterface $output, private readonly OutputInterface $output,
private readonly TestSuite $testSuite, private readonly TestSuite $testSuite,
) { ) {}
}
/** /**
* Runs the subscriber. * Runs the subscriber.

View File

@ -78,9 +78,7 @@ final class Backtrace
*/ */
public static function file(): string public static function file(): string
{ {
$trace = debug_backtrace(self::BACKTRACE_OPTIONS)[1]; $trace = self::backtrace();
assert(array_key_exists(self::FILE, $trace));
return $trace[self::FILE]; return $trace[self::FILE];
} }
@ -90,9 +88,7 @@ final class Backtrace
*/ */
public static function dirname(): string public static function dirname(): string
{ {
$trace = debug_backtrace(self::BACKTRACE_OPTIONS)[1]; $trace = self::backtrace();
assert(array_key_exists(self::FILE, $trace));
return dirname($trace[self::FILE]); return dirname($trace[self::FILE]);
} }
@ -102,8 +98,34 @@ final class Backtrace
*/ */
public static function line(): int public static function line(): int
{ {
$trace = debug_backtrace(self::BACKTRACE_OPTIONS)[1]; $trace = self::backtrace();
return $trace['line'] ?? 0; return $trace['line'] ?? 0;
} }
/**
* @return array{function: string, line?: int, file: string, class?: class-string, type?: string, args?: mixed[], object?: object}
*/
private static function backtrace(): array
{
$backtrace = debug_backtrace(self::BACKTRACE_OPTIONS);
foreach ($backtrace as $trace) {
if (! isset($trace['file'])) {
continue;
}
if (($GLOBALS['__PEST_INTERNAL_TEST_SUITE'] ?? false) && str_contains($trace['file'], 'pest'.DIRECTORY_SEPARATOR.'src')) {
continue;
}
if (str_contains($trace['file'], DIRECTORY_SEPARATOR.'pestphp'.DIRECTORY_SEPARATOR.'pest'.DIRECTORY_SEPARATOR.'src')) {
continue;
}
return $trace;
}
throw ShouldNotHappen::fromMessage('Backtrace not found.');
}
} }

View File

@ -13,9 +13,25 @@ use Pest\Exceptions\ShouldNotHappen;
final class ChainableClosure final class ChainableClosure
{ {
/** /**
* Calls the given `$closure` and chains the `$next` closure. * Calls the given `$closure` when the given condition is true, "bound" to the same object.
*/ */
public static function from(Closure $closure, Closure $next): Closure public static function boundWhen(Closure $condition, Closure $next): Closure
{
return function () use ($condition, $next): void {
if (! is_object($this)) { // @phpstan-ignore-line
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
}
if (\Pest\Support\Closure::bind($condition, $this, self::class)(...func_get_args())) {
\Pest\Support\Closure::bind($next, $this, self::class)(...func_get_args());
}
};
}
/**
* Calls the given `$closure` and chains the `$next` closure, "bound" to the same object.
*/
public static function bound(Closure $closure, Closure $next): Closure
{ {
return function () use ($closure, $next): void { return function () use ($closure, $next): void {
if (! is_object($this)) { // @phpstan-ignore-line if (! is_object($this)) { // @phpstan-ignore-line
@ -28,9 +44,20 @@ final class ChainableClosure
} }
/** /**
* Call the given static `$closure` and chains the `$next` closure. * Calls the given `$closure` and chains the `$next` closure, "unbound" of any object.
*/ */
public static function fromStatic(Closure $closure, Closure $next): Closure public static function unbound(Closure $closure, Closure $next): Closure
{
return function () use ($closure, $next): void {
$closure(...func_get_args());
$next(...func_get_args());
};
}
/**
* Call the given static `$closure` and chains the `$next` closure, "bound" to the same object statically.
*/
public static function boundStatically(Closure $closure, Closure $next): Closure
{ {
return static function () use ($closure, $next): void { return static function () use ($closure, $next): void {
\Pest\Support\Closure::bind($closure, null, self::class)(...func_get_args()); \Pest\Support\Closure::bind($closure, null, self::class)(...func_get_args());

View File

@ -26,7 +26,7 @@ final class Container
public static function getInstance(): self public static function getInstance(): self
{ {
if (! self::$instance instanceof \Pest\Support\Container) { if (! self::$instance instanceof \Pest\Support\Container) {
self::$instance = new self(); self::$instance = new self;
} }
return self::$instance; return self::$instance;

View File

@ -10,6 +10,7 @@ use SebastianBergmann\CodeCoverage\Node\Directory;
use SebastianBergmann\CodeCoverage\Node\File; use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\Environment\Runtime; use SebastianBergmann\Environment\Runtime;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use function Termwind\render; use function Termwind\render;
use function Termwind\renderUsing; use function Termwind\renderUsing;
use function Termwind\terminal; use function Termwind\terminal;
@ -36,7 +37,7 @@ final class Coverage
*/ */
public static function isAvailable(): bool public static function isAvailable(): bool
{ {
$runtime = new Runtime(); $runtime = new Runtime;
if (! $runtime->canCollectCodeCoverage()) { if (! $runtime->canCollectCodeCoverage()) {
return false; return false;
@ -66,7 +67,7 @@ final class Coverage
*/ */
public static function usingXdebug(): bool public static function usingXdebug(): bool
{ {
return (new Runtime())->hasXdebug(); return (new Runtime)->hasXdebug();
} }
/** /**
@ -159,10 +160,11 @@ final class Coverage
* ['11', '20..25', '50', '60..80']; * ['11', '20..25', '50', '60..80'];
* ``` * ```
* *
*
* @param File $file * @param File $file
* @return array<int, string> * @return array<int, string>
*/ */
public static function getMissingCoverage($file): array public static function getMissingCoverage(mixed $file): array
{ {
$shouldBeNewLine = true; $shouldBeNewLine = true;

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Pest\Support; namespace Pest\Support;
use function Pest\testDirectory;
/** /**
* @internal * @internal
*/ */
@ -25,6 +27,10 @@ final class DatasetInfo
public static function scope(string $file): string public static function scope(string $file): string
{ {
if (Str::endsWith($file, testDirectory('Pest.php'))) {
return dirname($file);
}
if (self::isInsideADatasetsDirectory($file)) { if (self::isInsideADatasetsDirectory($file)) {
return dirname($file, 2); return dirname($file, 2);
} }

View File

@ -18,11 +18,9 @@ final class ExceptionTrace
/** /**
* Ensures the given closure reports the good execution context. * Ensures the given closure reports the good execution context.
* *
* @return mixed
*
* @throws Throwable * @throws Throwable
*/ */
public static function ensure(Closure $closure) public static function ensure(Closure $closure): mixed
{ {
try { try {
return $closure(); return $closure();

View File

@ -30,8 +30,7 @@ final class ExpectationPipeline
*/ */
public function __construct( public function __construct(
private readonly Closure $closure private readonly Closure $closure
) { ) {}
}
/** /**
* Creates a new instance of Expectation Pipeline with given closure. * Creates a new instance of Expectation Pipeline with given closure.
@ -84,6 +83,6 @@ final class ExpectationPipeline
*/ */
public function carry(): Closure public function carry(): Closure
{ {
return fn ($stack, $pipe): Closure => fn () => $pipe($stack, ...$this->passables); return fn (mixed $stack, callable $pipe): Closure => fn () => $pipe($stack, ...$this->passables);
} }
} }

View File

@ -32,7 +32,7 @@ final class Exporter
public static function default(): self public static function default(): self
{ {
return new self( return new self(
new BaseExporter() new BaseExporter
); );
} }
@ -41,13 +41,13 @@ final class Exporter
* *
* @param array<int|string, mixed> $data * @param array<int|string, mixed> $data
*/ */
public function shortenedRecursiveExport(array &$data, Context $context = null): string public function shortenedRecursiveExport(array &$data, ?Context $context = null): string
{ {
$result = []; $result = [];
$array = $data; $array = $data;
$itemsCount = 0; $itemsCount = 0;
$exporter = self::default(); $exporter = self::default();
$context ??= new Context(); $context ??= new Context;
$context->add($data); $context->add($data);
@ -64,6 +64,8 @@ final class Exporter
continue; continue;
} }
assert(is_array($data));
$result[] = $context->contains($data[$key]) !== false $result[] = $context->contains($data[$key]) !== false
? '*RECURSION*' ? '*RECURSION*'
: sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context)); : sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context));

View File

@ -58,7 +58,7 @@ final class HigherOrderMessageCollection
/** /**
* Count the number of messages with the given name. * Count the number of messages with the given name.
* *
* @param string $name A higher order message name (usually a method name) * @param string $name A higher order message name (usually a method name)
*/ */
public function count(string $name): int public function count(string $name): int
{ {

View File

@ -16,7 +16,6 @@ final class NullClosure
*/ */
public static function create(): Closure public static function create(): Closure
{ {
return Closure::fromCallable(function (): void { return Closure::fromCallable(function (): void {});
});
} }
} }

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Pest\Support; namespace Pest\Support;
use Closure; use Closure;
use InvalidArgumentException;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
use Pest\TestSuite; use Pest\TestSuite;
use ReflectionClass; use ReflectionClass;
@ -24,9 +25,8 @@ final class Reflection
* Calls the given method with args on the given object. * Calls the given method with args on the given object.
* *
* @param array<int, mixed> $args * @param array<int, mixed> $args
* @return mixed
*/ */
public static function call(object $object, string $method, array $args = []) public static function call(object $object, string $method, array $args = []): mixed
{ {
$reflectionClass = new ReflectionClass($object); $reflectionClass = new ReflectionClass($object);
@ -53,9 +53,8 @@ final class Reflection
* Bind a callable to the TestCase and return the result. * Bind a callable to the TestCase and return the result.
* *
* @param array<int, mixed> $args * @param array<int, mixed> $args
* @return mixed
*/ */
public static function bindCallable(callable $callable, array $args = []) public static function bindCallable(callable $callable, array $args = []): mixed
{ {
return Closure::fromCallable($callable)->bindTo(TestSuite::getInstance()->test)(...$args); return Closure::fromCallable($callable)->bindTo(TestSuite::getInstance()->test)(...$args);
} }
@ -63,16 +62,22 @@ final class Reflection
/** /**
* Bind a callable to the TestCase and return the result, * Bind a callable to the TestCase and return the result,
* passing in the current dataset values as arguments. * passing in the current dataset values as arguments.
*
* @return mixed
*/ */
public static function bindCallableWithData(callable $callable) public static function bindCallableWithData(callable $callable): mixed
{ {
$test = TestSuite::getInstance()->test; $test = TestSuite::getInstance()->test;
return $test instanceof \PHPUnit\Framework\TestCase if (! $test instanceof \PHPUnit\Framework\TestCase) {
? Closure::fromCallable($callable)->bindTo($test)(...$test->providedData()) return self::bindCallable($callable);
: self::bindCallable($callable); }
foreach ($test->providedData() as $value) {
if ($value instanceof Closure) {
throw new InvalidArgumentException('Bound datasets are not supported while doing high order testing.');
}
}
return Closure::fromCallable($callable)->bindTo($test)(...$test->providedData());
} }
/** /**
@ -87,10 +92,8 @@ final class Reflection
/** /**
* Gets the property value from of the given object. * Gets the property value from of the given object.
*
* @return mixed
*/ */
public static function getPropertyValue(object $object, string $property) public static function getPropertyValue(object $object, string $property): mixed
{ {
$reflectionClass = new ReflectionClass($object); $reflectionClass = new ReflectionClass($object);
@ -196,7 +199,7 @@ final class Reflection
} }
$arguments[$parameter->getName()] = implode('|', array_map( $arguments[$parameter->getName()] = implode('|', array_map(
static fn (ReflectionNamedType $type): string => $type->getName(), static fn (ReflectionNamedType $type): string => $type->getName(), // @phpstan-ignore-line
($types instanceof ReflectionNamedType) ($types instanceof ReflectionNamedType)
? [$types] // NOTE: normalize as list of to handle unions ? [$types] // NOTE: normalize as list of to handle unions
: $types->getTypes(), : $types->getTypes(),
@ -206,10 +209,7 @@ final class Reflection
return $arguments; return $arguments;
} }
/** public static function getFunctionVariable(Closure $function, string $key): mixed
* @return mixed
*/
public static function getFunctionVariable(Closure $function, string $key)
{ {
return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null; return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null;
} }

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