Compare commits

...

231 Commits
v2.23.2 ... 2.x

Author SHA1 Message Date
d66361b272 release: 2.36.1 2026-01-28 02:02:41 +00:00
93b5611059 chore: deprecates php 8.1 2026-01-28 01:55:42 +00:00
2ded999adf chore: bumps dependencies 2026-01-28 01:51:26 +00:00
cde074cfd4 chore: removes static jobs 2026-01-28 01:36:09 +00:00
499480f28a chore: only runs CI against stable 2026-01-28 01:34:18 +00:00
f8c88bd14d chore: requires latest versions of collision and termwind 2024-10-15 16:30:56 +01:00
d454a36a48 removes non reported error 2024-10-15 15:34:49 +01:00
61b6b8c7d9 release: v2.36.0 2024-10-15 15:31:46 +01:00
e8aaa586cb feat: php 8.4 support 2024-10-15 15:31:29 +01:00
9ceb0834ae chore: updates snapshots 2024-08-22 21:07:39 +01:00
86d2191cae chore: refactor TestClosureMustNotBeStatic 2024-08-22 20:59:42 +01:00
748beb17d5 chore: adjusts memory limit 2024-08-22 20:59:42 +01:00
7ba235f61a Merge pull request #1129 from tomb1n0/bugfix/alwaysCallTeardownRegardlessOfExceptions
[Bug] Always call parent teardown even if an exception is thrown
2024-08-22 20:50:59 +01:00
700bd517f4 Merge pull request #1117 from peterfox/bug/catch-static-closures
[Bug] provided explaination for static closures
2024-08-22 20:33:58 +01:00
cbcfa2c5e2 Merge pull request #1100 from faissaloux/fix-use-strict-types
Fix `toUseStrictTypes`
2024-08-22 20:32:21 +01:00
243e45a551 Merge pull request #1097 from arifszn/2.x
[2.x] Modify `Result::exitCode` logic to address warning handling with `--fail-on-warning`
2024-08-22 20:31:34 +01:00
823c3d4b17 chore: updates snapshots 2024-08-20 22:49:23 +01:00
39c9b15bc0 Update visual_snapshot_of_help_command_output.snap 2024-08-20 22:44:39 +01:00
b13acb630d release: 2.35.1 2024-08-20 22:41:50 +01:00
172d94c0ca chore: bumps depedencies 2024-08-20 22:41:07 +01:00
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
adb2fb51df Always call parent teardown even if an exception is thrown 2024-04-08 10:03:55 +01:00
a7a3e4240e release: v2.34.7 2024-04-05 08:44:17 +01:00
e4af33867b chore: bumps dependencies 2024-04-05 08:44:07 +01:00
680111fb1e release: v2.34.6 2024-03-28 11:36:46 +00:00
aa6ff95ea4 chore: bumps dependencies 2024-03-28 11:36:36 +00:00
863a0cc837 release: v2.34.5 2024-03-22 08:44:19 +00:00
126a84a63e chore: bumps dependencies 2024-03-22 08:44:13 +00:00
0ccbe5c8f0 Remove Laravel serialisable closure 2024-03-17 17:23:17 +00:00
a4f8ae1a12 Handles tests where a static closure is provided 2024-03-17 16:48:43 +00:00
6094682158 Add static closure check 2024-03-17 12:04:38 +00:00
d519e40b95 chore: adjusts workflow 2024-03-15 21:14:22 +00:00
6a1161ead8 release: v2.34.4 2024-03-14 19:44:18 +00:00
a1b3547dd6 chore: fixes paratest version 2024-03-14 19:42:03 +00:00
b9e3146a47 release: v2.34.3 2024-03-14 19:40:23 +00:00
ce1607cba9 chore: bumps phpunit 2024-03-14 19:40:16 +00:00
ac07bc1770 chore: override changes 2024-03-14 19:40:03 +00:00
521a41dd10 fix: no duplicate --no-output 2024-03-14 19:39:45 +00:00
1b68b340e8 chore: fixes static analysis 2024-03-14 19:39:31 +00:00
853f6efce6 release: v2.34.2 2024-03-11 18:05:47 +00:00
62a9a78ee2 chore: bumps dependencies 2024-03-11 18:05:37 +00:00
78d9fd31d0 release: v2.34.1 2024-02-28 15:15:55 +00:00
2c3234fb3d fix bool type 2024-02-21 17:09:16 +01:00
1b64fef7ba fix toUseStrictTypes 2024-02-21 16:58:40 +01:00
a136231503 fix: modify Result::exitCode logic to address warning handling with --fail-on-warning 2024-02-20 18:24:37 +06:00
602b696348 release: v2.34.0 2024-02-17 10:06:53 +00:00
5b0f88c227 release: v2.33.6 2024-02-12 08:55:32 +00:00
f31a2c3220 chore: fixes paratest 2024-02-12 08:53:04 +00:00
e8fa98c810 release: 2.33.5 2024-02-12 08:44:52 +00:00
07e314fbf5 chore: bumps dependencies 2024-02-12 08:44:40 +00:00
4baf27911e release: 2.33.4 2024-02-02 16:54:54 +00:00
12e48a14d1 chore: fixes deps 2024-02-02 16:53:58 +00:00
1bc0f79508 release: 2.33.3 2024-02-02 16:51:42 +00:00
cb0f256791 release: 2.33.2 2024-02-02 16:50:40 +00:00
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
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
e888f3613b refactor: change falsy to false 2023-08-24 16:40:30 +07:00
6c3d8829ce feat: throwsUnless method 2023-08-24 09:28:47 +07:00
8ea7b2b802 Add errors-only flag 2023-08-18 10:13:28 +01:00
175 changed files with 1892 additions and 811 deletions

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

@ -1,43 +0,0 @@
name: Static Analysis
on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'
jobs:
static:
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
name: Static Tests
runs-on: ubuntu-latest
strategy:
matrix:
dependency-version: [prefer-lowest, prefer-stable]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
tools: composer:v2
coverage: none
- name: Install Dependencies
run: composer update --prefer-stable --no-interaction --no-progress --ansi
- name: Type Check
run: composer test:type:check
- name: Type Coverage
run: composer test:type:coverage
- name: Refacto
run: composer test:refacto
- name: Style
run: composer test:lint

View File

@ -3,25 +3,25 @@ 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', '8.3'] symfony: ['6.4', '7.0']
dependency-version: [prefer-lowest, prefer-stable] php: ['8.2', '8.3', '8.4']
dependency_version: [prefer-stable]
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 +36,13 @@ jobs:
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install PHP dependencies - name: Install PHP dependencies
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi 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

@ -21,19 +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)
- [Fathom Analytics](https://usefathom.com/) - [Localazy](https://localazy.com/?ref=pestphp)
- [Meema](https://meema.io) - [Route4Me](https://www.route4me.com/?ref=pestphp)
- [Zapiet](https://www.zapiet.com) - [Zapiet](https://www.zapiet.com/?ref=pestphp)
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

@ -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);
@ -81,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

@ -17,16 +17,17 @@
} }
], ],
"require": { "require": {
"php": "^8.1.0", "php": "^8.2.0",
"brianium/paratest": "^7.3.0", "brianium/paratest": "^7.4.9",
"nunomaduro/collision": "^7.9.0|^8.0.0", "nunomaduro/collision": "^7.11.0|^8.5.0",
"nunomaduro/termwind": "^1.15.1|^2.0.0", "nunomaduro/termwind": "^1.16.0|^2.3.3",
"pestphp/pest-plugin": "^2.1.1", "pestphp/pest-plugin": "^2.1.1",
"pestphp/pest-plugin-arch": "^2.4.0", "pestphp/pest-plugin-arch": "^2.7.0",
"phpunit/phpunit": "^10.4.1" "phpunit/phpunit": "^10.5.63"
}, },
"conflict": { "conflict": {
"phpunit/phpunit": ">10.4.1", "filp/whoops": "<2.16.0",
"phpunit/phpunit": ">10.5.63",
"sebastian/exporter": "<5.1.0", "sebastian/exporter": "<5.1.0",
"webmozart/assert": "<1.11.0" "webmozart/assert": "<1.11.0"
}, },
@ -51,9 +52,9 @@
] ]
}, },
"require-dev": { "require-dev": {
"pestphp/pest-dev-tools": "^2.16.0", "pestphp/pest-dev-tools": "^2.17.0",
"pestphp/pest-plugin-type-coverage": "^2.4.0", "pestphp/pest-plugin-type-coverage": "^2.8.7",
"symfony/process": "^6.3.4" "symfony/process": "^6.4.0|^7.4.4"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,
@ -68,22 +69,12 @@
"bin/pest" "bin/pest"
], ],
"scripts": { "scripts": {
"refacto": "rector",
"lint": "pint",
"test:refacto": "rector --dry-run",
"test:lint": "pint --test",
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
"test:type:coverage": "php bin/pest --type-coverage --min=100",
"test: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", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
"test": [ "test": [
"@test:refacto",
"@test:lint",
"@test:type:check",
"@test:type:coverage",
"@test:unit", "@test:unit",
"@test:parallel", "@test:parallel",
"@test:integration" "@test:integration"
@ -109,6 +100,11 @@
"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

@ -59,7 +59,6 @@ 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;
@ -91,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;
@ -129,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

@ -59,19 +59,14 @@ use function array_map;
*/ */
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() &&
@ -83,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()
@ -106,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()
@ -115,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

@ -1,6 +1,5 @@
includes: includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/ergebnis/phpstan-rules/rules.neon
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
parameters: parameters:
@ -12,12 +11,4 @@ parameters:
reportUnmatchedIgnoredErrors: true reportUnmatchedIgnoredErrors: true
ignoreErrors: ignoreErrors:
- "#has a nullable return type declaration.#"
- "#Language construct isset\\(\\) should not be used.#"
- "#is not allowed to extend#"
- "#is concrete, but does not have a Test suffix#"
- "#with a nullable type declaration#"
- "#type mixed is not subtype of native#" - "#type mixed is not subtype of native#"
- "# with null as default value#"
- "#has parameter \\$closure with default value.#"
- "#has parameter \\$description with default value.#"

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

@ -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

@ -193,6 +193,7 @@ trait Testable
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$description = $this->dataName() ? $method->description.' with '.$this->dataName() : $method->description; $description = $this->dataName() ? $method->description.' with '.$this->dataName() : $method->description;
$description = htmlspecialchars(html_entity_decode($description), ENT_NOQUOTES);
if ($method->repetitions > 1) { if ($method->repetitions > 1) {
$matches = []; $matches = [];
@ -233,11 +234,13 @@ trait Testable
$afterEach = ChainableClosure::bound($this->__afterEach, $afterEach); $afterEach = ChainableClosure::bound($this->__afterEach, $afterEach);
} }
$this->__callClosure($afterEach, func_get_args()); try {
$this->__callClosure($afterEach, func_get_args());
} finally {
parent::tearDown();
parent::tearDown(); TestSuite::getInstance()->test = null;
}
TestSuite::getInstance()->test = null;
} }
/** /**
@ -289,7 +292,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;
} }

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

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

View File

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

View File

@ -32,10 +32,9 @@ use Pest\Matchers\Any;
use Pest\Support\ExpectationPipeline; use Pest\Support\ExpectationPipeline;
use PHPUnit\Architecture\Elements\ObjectDescription; 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.
@ -192,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.');
@ -349,9 +348,15 @@ final class Expectation
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;
@ -416,7 +421,7 @@ final class Expectation
*/ */
public function any(): Any public function any(): Any
{ {
return new Any(); return new Any;
} }
/** /**
@ -436,7 +441,7 @@ final class Expectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'), fn (ObjectDescription $object): bool => (bool) preg_match('/^<\?php\s+declare\(.*?strict_types\s?=\s?1.*?\);/', (string) file_get_contents($object->path)),
'to use strict types', 'to use strict types',
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')), FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
); );
@ -510,7 +515,7 @@ final class Expectation
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => $object->reflectionClass->hasMethod($method), fn (ObjectDescription $object): bool => $object->reflectionClass->hasMethod($method),
'to have method', sprintf("to have method '%s'", $method),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
@ -537,7 +542,7 @@ final class Expectation
} }
/** /**
* Asserts that the given expectation targets is an class. * Asserts that the given expectation target is a class.
*/ */
public function toBeClass(): ArchExpectation public function toBeClass(): ArchExpectation
{ {
@ -878,4 +883,51 @@ final class Expectation
{ {
return $this->toHaveMethod('__destruct'); 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

@ -24,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

@ -36,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.
@ -84,7 +82,7 @@ final class OppositeExpectation
{ {
return Targeted::make( return Targeted::make(
$this->original, $this->original,
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'), fn (ObjectDescription $object): bool => ! (bool) preg_match('/^<\?php\s+declare\(.*?strict_types\s?=\s?1.*?\);/', (string) file_get_contents($object->path)),
'not to use strict types', 'not to use strict types',
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')), FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
); );
@ -485,4 +483,51 @@ final class OppositeExpectation
{ {
return $this->toHaveMethod('__destruct'); 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

@ -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
{ {
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

@ -11,6 +11,7 @@ use Pest\Contracts\HasPrintableTestCaseName;
use Pest\Exceptions\DatasetMissing; use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
use Pest\Exceptions\TestAlreadyExist; use Pest\Exceptions\TestAlreadyExist;
use Pest\Exceptions\TestClosureMustNotBeStatic;
use Pest\Exceptions\TestDescriptionMissing; use Pest\Exceptions\TestDescriptionMissing;
use Pest\Factories\Concerns\HigherOrderable; use Pest\Factories\Concerns\HigherOrderable;
use Pest\Support\Reflection; use Pest\Support\Reflection;
@ -154,7 +155,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
); );
} }
@ -193,7 +194,7 @@ final class TestCaseFactory
} }
PHP; PHP;
eval($classCode); // @phpstan-ignore-line eval($classCode);
} catch (ParseError $caught) { } catch (ParseError $caught) {
throw new RuntimeException(sprintf( throw new RuntimeException(sprintf(
"Unable to create test case for test file at %s. \n %s", "Unable to create test case for test file at %s. \n %s",
@ -216,6 +217,14 @@ final class TestCaseFactory
throw new TestAlreadyExist($method->filename, $method->description); throw new TestAlreadyExist($method->filename, $method->description);
} }
if (
$method->closure instanceof \Closure &&
(new \ReflectionFunction($method->closure))->isStatic()
) {
throw new TestClosureMustNotBeStatic($method);
}
if (! $method->receivesArguments()) { if (! $method->receivesArguments()) {
if (! $method->closure instanceof \Closure) { if (! $method->closure instanceof \Closure) {
throw ShouldNotHappen::fromMessage('The test closure may not be empty.'); throw ShouldNotHappen::fromMessage('The test closure may not be empty.');

View File

@ -73,7 +73,7 @@ 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();
@ -138,11 +138,11 @@ 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 !== [] || $this->repetitions > 1) { if ($this->datasets !== [] || $this->repetitions > 1) {

View File

@ -55,7 +55,7 @@ if (! function_exists('beforeEach')) {
* *
* @return HigherOrderTapProxy<Expectable|TestCall|TestCase>|Expectable|TestCall|TestCase|mixed * @return HigherOrderTapProxy<Expectable|TestCall|TestCase>|Expectable|TestCall|TestCase|mixed
*/ */
function beforeEach(Closure $closure = null): BeforeEachCall function beforeEach(?Closure $closure = null): BeforeEachCall
{ {
$filename = Backtrace::file(); $filename = Backtrace::file();
@ -116,7 +116,7 @@ if (! function_exists('test')) {
* *
* @return Expectable|TestCall|TestCase|mixed * @return Expectable|TestCall|TestCase|mixed
*/ */
function test(string $description = null, Closure $closure = null): HigherOrderTapProxy|TestCall function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall
{ {
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);
@ -136,7 +136,7 @@ if (! function_exists('it')) {
* *
* @return Expectable|TestCall|TestCase|mixed * @return Expectable|TestCall|TestCase|mixed
*/ */
function it(string $description, Closure $closure = null): TestCall function it(string $description, ?Closure $closure = null): TestCall
{ {
$description = sprintf('it %s', $description); $description = sprintf('it %s', $description);
@ -171,7 +171,7 @@ if (! function_exists('afterEach')) {
* *
* @return Expectable|HigherOrderTapProxy<Expectable|TestCall|TestCase>|TestCall|mixed * @return Expectable|HigherOrderTapProxy<Expectable|TestCall|TestCase>|TestCall|mixed
*/ */
function afterEach(Closure $closure = null): AfterEachCall function afterEach(?Closure $closure = null): AfterEachCall
{ {
$filename = Backtrace::file(); $filename = Backtrace::file();

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

@ -40,7 +40,7 @@ final class KernelDump
*/ */
public function disable(): void public function disable(): void
{ {
@ob_clean(); // @phpstan-ignore-line @ob_clean();
if ($this->buffer !== '') { if ($this->buffer !== '') {
$this->flush(); $this->flush();
@ -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

@ -131,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);
@ -143,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);
@ -155,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);
@ -167,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);
@ -196,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.
* *
@ -296,7 +314,7 @@ final class Expectation
* *
* @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();
@ -314,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;
@ -449,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.
* *
@ -624,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();
@ -844,6 +874,7 @@ final class Expectation
$string = match (true) { $string = match (true) {
is_string($this->value) => $this->value, 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(),
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 $this->value instanceof \Illuminate\Testing\TestResponse => $this->value->getContent(), // @phpstan-ignore-line
@ -919,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();

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

@ -35,11 +35,11 @@ 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(); $this->describing = DescribeCall::describing();
} }

View File

@ -40,12 +40,12 @@ final class BeforeEachCall
public function __construct( public function __construct(
public 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(); $this->describing = DescribeCall::describing();
} }

View File

@ -18,6 +18,7 @@ use Pest\Support\HigherOrderCallables;
use Pest\Support\NullClosure; use Pest\Support\NullClosure;
use Pest\Support\Str; use Pest\Support\Str;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
@ -45,8 +46,8 @@ 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);
@ -57,10 +58,18 @@ final class TestCall
$this->testSuite->beforeEach->get($this->filename)[0]($this); $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;
@ -92,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
@ -105,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`.
@ -181,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].');
} }
/** /**
@ -194,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].');
} }
/** /**
@ -202,19 +254,43 @@ 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 $osFamily === PHP_OS_FAMILY 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. * Repeats the current test the given number of times.
*/ */
@ -293,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;
} }
@ -333,7 +409,7 @@ 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();

View File

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

View File

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

View File

@ -10,20 +10,20 @@ use Pest\Plugin\Loader;
/** /**
* @internal * @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;
} }
/** /**

View File

@ -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

@ -61,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),
@ -93,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',

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

@ -34,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;
} }
@ -126,7 +126,7 @@ final class Parallel implements HandlesArguments
$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);
} }
@ -173,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

@ -63,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
{ {
@ -79,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();
@ -172,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

@ -91,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];
@ -110,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
@ -357,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);
@ -380,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

@ -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);
} }
} }

View File

@ -21,8 +21,7 @@ final class SnapshotRepository
public function __construct( public function __construct(
readonly private string $testsPath, readonly private string $testsPath,
readonly private string $snapshotsPath, readonly private string $snapshotsPath,
) { ) {}
}
/** /**
* Checks if the snapshot exists. * Checks if the snapshot exists.

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

@ -40,28 +40,31 @@ final class Result
*/ */
public static function exitCode(Configuration $configuration, TestResult $result): int public static function exitCode(Configuration $configuration, TestResult $result): int
{ {
if ($result->wasSuccessfulIgnoringPhpunitWarnings() if ($result->wasSuccessful()) {
&& ! $result->hasTestTriggeredPhpunitWarningEvents()) { if ($configuration->failOnWarning()) {
return self::SUCCESS_EXIT; $warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents()
+ count($result->warnings())
+ count($result->phpWarnings());
if ($warnings > 0) {
return self::FAILURE_EXIT;
}
}
if (! $result->hasTestTriggeredPhpunitWarningEvents()) {
return self::SUCCESS_EXIT;
}
} }
if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) { if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) {
return self::FAILURE_EXIT; return self::FAILURE_EXIT;
} }
if ($result->wasSuccessfulIgnoringPhpunitWarnings()) { if ($result->wasSuccessful()) {
if ($configuration->failOnRisky() && $result->hasTestConsideredRiskyEvents()) { if ($configuration->failOnRisky() && $result->hasTestConsideredRiskyEvents()) {
$returnCode = self::FAILURE_EXIT; $returnCode = self::FAILURE_EXIT;
} }
$warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents() +
+count($result->warnings())
+ count($result->phpWarnings());
if ($configuration->failOnWarning() && $warnings > 0) {
$returnCode = self::FAILURE_EXIT;
}
if ($configuration->failOnIncomplete() && $result->hasTestMarkedIncompleteEvents()) { if ($configuration->failOnIncomplete() && $result->hasTestMarkedIncompleteEvents()) {
$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

@ -115,7 +115,11 @@ final class Backtrace
continue; continue;
} }
if (str_contains($trace['file'], 'pest'.DIRECTORY_SEPARATOR.'src')) { 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; continue;
} }

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

@ -37,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;
@ -67,7 +67,7 @@ final class Coverage
*/ */
public static function usingXdebug(): bool public static function usingXdebug(): bool
{ {
return (new Runtime())->hasXdebug(); return (new Runtime)->hasXdebug();
} }
/** /**

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.

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;
@ -66,9 +67,17 @@ final class Reflection
{ {
$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());
} }
/** /**
@ -190,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(),

View File

@ -20,7 +20,7 @@ final class StateGenerator
{ {
public function fromPhpUnitTestResult(int $passedTests, PHPUnitTestResult $testResult): State public function fromPhpUnitTestResult(int $passedTests, PHPUnitTestResult $testResult): State
{ {
$state = new State(); $state = new State;
foreach ($testResult->testErroredEvents() as $testResultEvent) { foreach ($testResult->testErroredEvents() as $testResultEvent) {
if ($testResultEvent instanceof Errored) { if ($testResultEvent instanceof Errored) {

View File

@ -24,7 +24,7 @@ final class Str
* Create a (unsecure & non-cryptographically safe) random alpha-numeric * Create a (unsecure & non-cryptographically safe) random alpha-numeric
* string value. * string value.
* *
* @param int $length the length of the resulting randomized string * @param int $length the length of the resulting randomized string
* *
* @see https://github.com/laravel/framework/blob/4.2/src/Illuminate/Support/Str.php#L240-L242 * @see https://github.com/laravel/framework/blob/4.2/src/Illuminate/Support/Str.php#L240-L242
*/ */

View File

@ -76,7 +76,7 @@ final class GitDirtyTestCaseFilter implements TestCaseFilter
$dirtyFiles = array_values($dirtyFiles); $dirtyFiles = array_values($dirtyFiles);
if ($dirtyFiles === []) { if ($dirtyFiles === []) {
Panic::with(new NoDirtyTestsFound()); Panic::with(new NoDirtyTestsFound);
} }
$this->changedFiles = $dirtyFiles; $this->changedFiles = $dirtyFiles;

View File

@ -71,11 +71,11 @@ final class TestSuite
string $rootPath, string $rootPath,
public string $testPath, public string $testPath,
) { ) {
$this->beforeAll = new BeforeAllRepository(); $this->beforeAll = new BeforeAllRepository;
$this->beforeEach = new BeforeEachRepository(); $this->beforeEach = new BeforeEachRepository;
$this->tests = new TestRepository(); $this->tests = new TestRepository;
$this->afterEach = new AfterEachRepository(); $this->afterEach = new AfterEachRepository;
$this->afterAll = new AfterAllRepository(); $this->afterAll = new AfterAllRepository;
$this->rootPath = (string) realpath($rootPath); $this->rootPath = (string) realpath($rootPath);
$this->snapshots = new SnapshotRepository( $this->snapshots = new SnapshotRepository(
implode(DIRECTORY_SEPARATOR, [$this->rootPath, $this->testPath]), implode(DIRECTORY_SEPARATOR, [$this->rootPath, $this->testPath]),
@ -87,8 +87,8 @@ final class TestSuite
* Returns the current instance of the test suite. * Returns the current instance of the test suite.
*/ */
public static function getInstance( public static function getInstance(
string $rootPath = null, ?string $rootPath = null,
string $testPath = null, ?string $testPath = null,
): TestSuite { ): TestSuite {
if (is_string($rootPath) && is_string($testPath)) { if (is_string($rootPath) && is_string($testPath)) {
self::$instance = new TestSuite($rootPath, $testPath); self::$instance = new TestSuite($rootPath, $testPath);
@ -101,7 +101,7 @@ final class TestSuite
} }
if (! self::$instance instanceof self) { if (! self::$instance instanceof self) {
Panic::with(new InvalidPestCommand()); Panic::with(new InvalidPestCommand);
} }
return self::$instance; return self::$instance;

View File

@ -6,5 +6,5 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
{ {
use CreatesApplication; //
} }

View File

@ -0,0 +1 @@
{"version":"pest_2.32.2","defects":[],"times":{"P\\Tests\\Playground::__pest_evaluable_basic":0.005}}

View File

@ -0,0 +1 @@
<input type="hidden" name="_token" value="1" />

View File

@ -0,0 +1,3 @@
{
"key": " <div class=\"container\">\n <div class=\"row\">\n <div class=\"col-md-12\">\n <h1>Snapshot<\/h1>\n <\/div>\n <\/div>\n <\/div>"
}

View File

@ -1,5 +1,5 @@
Pest Testing Framework 2.23.2. Pest Testing Framework 2.36.1.
USAGE: pest <file> [options] USAGE: pest <file> [options]
@ -38,7 +38,6 @@
EXECUTION OPTIONS: EXECUTION OPTIONS:
--parallel ........................................... Run tests in parallel --parallel ........................................... Run tests in parallel
--update-snapshots Update snapshots for tests using the "toMatchSnapshot" expectation --update-snapshots Update snapshots for tests using the "toMatchSnapshot" expectation
--process-isolation ................ Run each test in a separate PHP process
--globals-backup ................. Backup and restore $GLOBALS for each test --globals-backup ................. Backup and restore $GLOBALS for each test
--static-backup ......... Backup and restore static properties for each test --static-backup ......... Backup and restore static properties for each test
--strict-coverage ................... Be strict about code coverage metadata --strict-coverage ................... Be strict about code coverage metadata
@ -46,7 +45,7 @@
--disallow-test-output ................. Be strict about output during tests --disallow-test-output ................. Be strict about output during tests
--enforce-time-limit ................. Enforce time limit based on test size --enforce-time-limit ................. Enforce time limit based on test size
--default-time-limit [sec] Timeout in seconds for tests that have no declared size --default-time-limit [sec] Timeout in seconds for tests that have no declared size
--dont-report-useless-tests .. Do not report tests that do not test anything --do-not-report-useless-tests Do not report tests that do not test anything
--stop-on-defect ... Stop after first error, failure, warning, or risky test --stop-on-defect ... Stop after first error, failure, warning, or risky test
--stop-on-error ..................................... Stop after first error --stop-on-error ..................................... Stop after first error
--stop-on-failure ................................. Stop after first failure --stop-on-failure ................................. Stop after first failure
@ -56,12 +55,25 @@
--stop-on-notice ............. Stop after first test that triggered a notice --stop-on-notice ............. Stop after first test that triggered a notice
--stop-on-skipped ............................ Stop after first skipped test --stop-on-skipped ............................ Stop after first skipped test
--stop-on-incomplete ...................... Stop after first incomplete test --stop-on-incomplete ...................... Stop after first incomplete test
--fail-on-empty-test-suite Signal failure using shell exit code when no tests were run
--fail-on-warning Signal failure using shell exit code when a warning was triggered --fail-on-warning Signal failure using shell exit code when a warning was triggered
--fail-on-risky Signal failure using shell exit code when a test was considered risky --fail-on-risky Signal failure using shell exit code when a test was considered risky
--fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered --fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered
--fail-on-phpunit-deprecation Signal failure using shell exit code when a PHPUnit deprecation was triggered
--fail-on-phpunit-warning Signal failure using shell exit code when a PHPUnit warning was triggered
--fail-on-notice Signal failure using shell exit code when a notice was triggered --fail-on-notice Signal failure using shell exit code when a notice was triggered
--fail-on-skipped Signal failure using shell exit code when a test was skipped --fail-on-skipped Signal failure using shell exit code when a test was skipped
--fail-on-incomplete Signal failure using shell exit code when a test was marked incomplete --fail-on-incomplete Signal failure using shell exit code when a test was marked incomplete
--fail-on-all-issues Signal failure using shell exit code when an issue is triggered
--do-not-fail-on-empty-test-suite Do not signal failure using shell exit code when no tests were run
--do-not-fail-on-warning Do not signal failure using shell exit code when a warning was triggered
--do-not-fail-on-risky Do not signal failure using shell exit code when a test was considered risky
--do-not-fail-on-deprecation Do not signal failure using shell exit code when a deprecation was triggered
--do-not-fail-on-phpunit-deprecation Do not signal failure using shell exit code when a PHPUnit deprecation was triggered
--do-not-fail-on-phpunit-warning Do not signal failure using shell exit code when a PHPUnit warning was triggered
--do-not-fail-on-notice Do not signal failure using shell exit code when a notice was triggered
--do-not-fail-on-skipped Do not signal failure using shell exit code when a test was skipped
--do-not-fail-on-incomplete Do not signal failure using shell exit code when a test was marked incomplete
--cache-result ............................ Write test results to cache file --cache-result ............................ Write test results to cache file
--do-not-cache-result .............. Do not write test results to cache file --do-not-cache-result .............. Do not write test results to cache file
--order-by [order] Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size --order-by [order] Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size
@ -78,12 +90,15 @@
--display-incomplete .................. Display details for incomplete tests --display-incomplete .................. Display details for incomplete tests
--display-skipped ........................ Display details for skipped tests --display-skipped ........................ Display details for skipped tests
--display-deprecations . Display details for deprecations triggered by tests --display-deprecations . Display details for deprecations triggered by tests
--display-phpunit-deprecations .... Display details for PHPUnit deprecations
--display-errors ............. Display details for errors triggered by tests --display-errors ............. Display details for errors triggered by tests
--display-notices ........... Display details for notices triggered by tests --display-notices ........... Display details for notices triggered by tests
--display-warnings ......... Display details for warnings triggered by tests --display-warnings ......... Display details for warnings triggered by tests
--display-all-issues ..... Display details for all issues that are triggered
--reverse-list .............................. Print defects in reverse order --reverse-list .............................. Print defects in reverse order
--teamcity . Replace default progress and result output with TeamCity format --teamcity . Replace default progress and result output with TeamCity format
--testdox ................ Replace default result output with TestDox format --testdox ................ Replace default result output with TestDox format
--debug Replace default progress and result output with debugging information
--compact ................ Replace default result output with Compact format --compact ................ Replace default result output with Compact format
LOGGING OPTIONS: LOGGING OPTIONS:
@ -104,6 +119,8 @@
--coverage-html [dir] Write code coverage report in HTML format to directory --coverage-html [dir] Write code coverage report in HTML format to directory
--coverage-php [file] .......... Write serialized code coverage data to file --coverage-php [file] .......... Write serialized code coverage data to file
--coverage-text=[file] Write code coverage report in text format to file [default: standard output] --coverage-text=[file] Write code coverage report in text format to file [default: standard output]
--only-summary-for-coverage-text Option for code coverage report in text format: only show summary
--show-uncovered-for-coverage-text Option for code coverage report in text format: show uncovered files
--coverage-xml [dir] . Write code coverage report in XML format to directory --coverage-xml [dir] . Write code coverage report in XML format to directory
--warm-coverage-cache ........................... Warm static analysis cache --warm-coverage-cache ........................... Warm static analysis cache
--coverage-filter [dir] ........... Include [dir] in code coverage reporting --coverage-filter [dir] ........... Include [dir] in code coverage reporting

View File

@ -1,3 +1,3 @@
Pest Testing Framework 2.23.2. Pest Testing Framework 2.36.1.

View File

@ -154,6 +154,10 @@
✓ it can correctly resolve a bound dataset that returns an array with (Closure) ✓ it can correctly resolve a bound dataset that returns an array with (Closure)
✓ it can correctly resolve a bound dataset that returns an array but wants to be spread with (Closure) ✓ it can correctly resolve a bound dataset that returns an array but wants to be spread with (Closure)
↓ forbids to define tests in Datasets dirs and Datasets.php files ↓ forbids to define tests in Datasets dirs and Datasets.php files
✓ it may be used with high order with dataset "formal"
✓ it may be used with high order with dataset "informal"
✓ it may be used with high order even when bound with dataset "formal"
✓ it may be used with high order even when bound with dataset "informal"
PASS Tests\Features\Depends PASS Tests\Features\Depends
✓ first ✓ first
@ -215,6 +219,14 @@
✓ it can just define the code if given condition is true ✓ it can just define the code if given condition is true
✓ it can just define the message if given condition is 1 ✓ it can just define the message if given condition is 1
✓ it can just define the code if given condition is 1 ✓ it can just define the code if given condition is 1
✓ it not catch exceptions if given condition is true
✓ it catch exceptions if given condition is false
✓ it catch exceptions and messages if given condition is false
✓ it catch exceptions, messages and code if given condition is false
✓ it can just define the message if given condition is false
✓ it can just define the code if given condition is false
✓ it can just define the message if given condition is 0
✓ it can just define the code if given condition is 0
PASS Tests\Features\Expect\HigherOrder\methods PASS Tests\Features\Expect\HigherOrder\methods
✓ it can access methods ✓ it can access methods
@ -418,6 +430,7 @@
PASS Tests\Features\Expect\toBeGreaterThan PASS Tests\Features\Expect\toBeGreaterThan
✓ passes ✓ passes
✓ passes with DateTime and DateTimeImmutable ✓ passes with DateTime and DateTimeImmutable
✓ passes with strings
✓ failures ✓ failures
✓ failures with custom message ✓ failures with custom message
✓ not failures ✓ not failures
@ -425,6 +438,7 @@
PASS Tests\Features\Expect\toBeGreaterThanOrEqual PASS Tests\Features\Expect\toBeGreaterThanOrEqual
✓ passes ✓ passes
✓ passes with DateTime and DateTimeImmutable ✓ passes with DateTime and DateTimeImmutable
✓ passes with strings
✓ failures ✓ failures
✓ failures with custom message ✓ failures with custom message
✓ not failures ✓ not failures
@ -453,6 +467,10 @@
✓ failures with custom message ✓ failures with custom message
✓ not failures ✓ not failures
PASS Tests\Features\Expect\toBeIntBackedEnum
✓ enum is backed by int
✓ enum is not backed by int
PASS Tests\Features\Expect\toBeInvokable PASS Tests\Features\Expect\toBeInvokable
✓ class is invokable ✓ class is invokable
✓ opposite class is invokable ✓ opposite class is invokable
@ -482,6 +500,7 @@
PASS Tests\Features\Expect\toBeLessThan PASS Tests\Features\Expect\toBeLessThan
✓ passes ✓ passes
✓ passes with DateTime and DateTimeImmutable ✓ passes with DateTime and DateTimeImmutable
✓ passes with strings
✓ failures ✓ failures
✓ failures with custom message ✓ failures with custom message
✓ not failures ✓ not failures
@ -489,6 +508,13 @@
PASS Tests\Features\Expect\toBeLessThanOrEqual PASS Tests\Features\Expect\toBeLessThanOrEqual
✓ passes ✓ passes
✓ passes with DateTime and DateTimeImmutable ✓ passes with DateTime and DateTimeImmutable
✓ passes with strings
✓ failures
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeList
✓ pass
✓ failures ✓ failures
✓ failures with custom message ✓ failures with custom message
✓ not failures ✓ not failures
@ -559,6 +585,10 @@
✓ failures with custom message ✓ failures with custom message
✓ not failures ✓ not failures
PASS Tests\Features\Expect\toBeStringBackedEnum
✓ enum is backed by string
✓ enum is not backed by string
PASS Tests\Features\Expect\toBeStudlyCase PASS Tests\Features\Expect\toBeStudlyCase
✓ pass ✓ pass
✓ failures ✓ failures
@ -629,6 +659,16 @@
✓ failures with multiple needles (some failing) ✓ failures with multiple needles (some failing)
✓ not failures ✓ not failures
✓ not failures with multiple needles (all failing) ✓ not failures with multiple needles (all failing)
✓ not failures with multiple needles (some failing)
PASS Tests\Features\Expect\toContainEqual
✓ passes arrays
✓ passes arrays with multiple needles
✓ failures
✓ failures with multiple needles (all failing)
✓ failures with multiple needles (some failing)
✓ not failures
✓ not failures with multiple needles (all failing)
✓ not failures with multiple needles (some failing) ✓ not failures with multiple needles (some failing)
PASS Tests\Features\Expect\toContainOnlyInstancesOf PASS Tests\Features\Expect\toContainOnlyInstancesOf
@ -822,12 +862,14 @@
PASS Tests\Features\Expect\toMatchSnapshot PASS Tests\Features\Expect\toMatchSnapshot
✓ pass ✓ pass
✓ pass using pipes
✓ pass with __toString ✓ pass with __toString
✓ pass with toString ✓ pass with toString
✓ pass with dataset with ('my-datas-set-value') ✓ pass with dataset with ('my-datas-set-value')
✓ within describe → pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value')
✓ pass with toArray ✓ pass with toArray
✓ pass with array ✓ pass with array
✓ pass with toSnapshot
✓ failures ✓ failures
✓ failures with custom message ✓ failures with custom message
✓ not failures ✓ not failures
@ -889,6 +931,14 @@
✓ it skips with falsy closure condition ✓ it skips with falsy closure condition
✓ it can be used in higher order tests ✓ it can be used in higher order tests
PASS Tests\Features\Fail
✓ it may fail
✓ it may fail with the given message
PASS Tests\Features\Fails
✓ it may fail
✓ it may fail with the given message
WARN Tests\Features\Helpers WARN Tests\Features\Helpers
✓ it can set/get properties on $this ✓ it can set/get properties on $this
! it gets null if property do not exist → Undefined property Tests\Features\Helpers::$wqdwqdqw ! it gets null if property do not exist → Undefined property Tests\Features\Helpers::$wqdwqdqw
@ -1076,6 +1126,11 @@
- it can use something in the test case as a condition → This test was skipped - it can use something in the test case as a condition → This test was skipped
- it can user higher order callables and skip - it can user higher order callables and skip
WARN Tests\Features\SkipOnPhp
✓ it can run on php version
✓ it can run on specific php version
- it can skip on php versions depending on constraint → This test is skipped on PHP [>=7.4.0].
PASS Tests\Features\Test PASS Tests\Features\Test
✓ a test ✓ a test
✓ higher order message test ✓ higher order message test
@ -1112,17 +1167,9 @@
PASS Tests\Helpers\TestInHelpers PASS Tests\Helpers\TestInHelpers
✓ it executes tests in the Helpers directory ✓ it executes tests in the Helpers directory
PASS Tests\Hooks\AfterAllTest
✓ global afterAll execution order
✓ it only gets called once per file
PASS Tests\Hooks\AfterEachTest PASS Tests\Hooks\AfterEachTest
✓ global afterEach execution order ✓ global afterEach execution order
PASS Tests\Hooks\BeforeAllTest
✓ global beforeAll execution order
✓ it only gets called once per file
PASS Tests\Hooks\BeforeEachTest PASS Tests\Hooks\BeforeEachTest
✓ global beforeEach execution order ✓ global beforeEach execution order
@ -1194,6 +1241,7 @@
✓ it show the correct description for mixed named and not-named datasets ✓ it show the correct description for mixed named and not-named datasets
✓ it shows the correct description for long texts with newlines ✓ it shows the correct description for long texts with newlines
✓ it shows the correct description for arrays with many elements ✓ it shows the correct description for arrays with many elements
✓ it shows the correct description of datasets with html
PASS Tests\Unit\Expectations\OppositeExpectation PASS Tests\Unit\Expectations\OppositeExpectation
✓ it throw expectation failed exception with string argument ✓ it throw expectation failed exception with string argument
@ -1202,6 +1250,14 @@
PASS Tests\Unit\Overrides\ThrowableBuilder PASS Tests\Unit\Overrides\ThrowableBuilder
✓ collision editor can be added to the stack trace ✓ collision editor can be added to the stack trace
PASS Tests\Unit\Plugins\Concerns\HandleArguments
✓ method hasArgument with ('--long-argument', true)
✓ method hasArgument with ('-a', true)
✓ method hasArgument with ('--with-equal-sign', true)
✓ method hasArgument with ('someValue', true)
✓ method hasArgument with ('--a', false)
✓ method hasArgument with ('--undefined-argument', false)
PASS Tests\Unit\Plugins\Environment PASS Tests\Unit\Plugins\Environment
✓ environment is set to CI when --ci option is used ✓ environment is set to CI when --ci option is used
✓ environment is set to Local when --ci option is not used ✓ environment is set to Local when --ci option is not used
@ -1221,26 +1277,6 @@
✓ it can resolve builtin value types ✓ it can resolve builtin value types
✓ it cannot resolve a parameter without type ✓ it cannot resolve a parameter without type
PASS Tests\Unit\Support\DatasetInfo
✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datase…rs.php', true)
✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datasets.php', false)
✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…rs.php', true)
✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…rs.php', false)
✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…ts.php', false)
✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Datase…rs.php', false)
✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Datasets.php', true)
✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur…rs.php', false) #1
✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur…rs.php', false) #2
✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur…ts.php', true)
✓ it computes the dataset scope with ('/var/www/project/tests/Datase…rs.php', '/var/www/project/tests')
✓ it computes the dataset scope with ('/var/www/project/tests/Datasets.php', '/var/www/project/tests')
✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Features')
✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Featur…rs.php') #1
✓ it computes the dataset scope with ('/var/www/project/tests/Featur…ts.php', '/var/www/project/tests/Features')
✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Featur…ollers')
✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Featur…rs.php') #2
✓ it computes the dataset scope with ('/var/www/project/tests/Featur…ts.php', '/var/www/project/tests/Featur…ollers')
PASS Tests\Unit\Support\ExceptionTrace PASS Tests\Unit\Support\ExceptionTrace
✓ it ensures the given closures reports the correct class name ✓ it ensures the given closures reports the correct class name
✓ it ensures the given closures reports the correct class name and suggests the [uses()] function ✓ it ensures the given closures reports the correct class name and suggests the [uses()] function
@ -1315,6 +1351,7 @@
PASS Tests\Unit\TestSuite PASS Tests\Unit\TestSuite
✓ it does not allow to add the same test description twice ✓ it does not allow to add the same test description twice
✓ it does not allow static closures
✓ it alerts users about tests with arguments but no input ✓ it alerts users about tests with arguments but no input
✓ it can return an array of all test suite filenames ✓ it can return an array of all test suite filenames
@ -1329,6 +1366,10 @@
PASS Tests\Visual\Help PASS Tests\Visual\Help
✓ visual snapshot of help command output ✓ visual snapshot of help command output
PASS Tests\Visual\JUnit
✓ junit output
✓ junit with parallel
PASS Tests\Visual\Parallel PASS Tests\Visual\Parallel
✓ parallel ✓ parallel
✓ a parallel test can extend another test with same name ✓ a parallel test can extend another test with same name
@ -1346,10 +1387,12 @@
- visual snapshot of team city with ('SuccessOnly.php') - visual snapshot of team city with ('SuccessOnly.php')
PASS Tests\Visual\Todo PASS Tests\Visual\Todo
✓ todos
✓ todos in parallel
✓ todo ✓ todo
✓ todo in parallel ✓ todo in parallel
WARN Tests\Visual\Version WARN Tests\Visual\Version
- visual snapshot of help command output - visual snapshot of help command output
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 19 skipped, 958 passed (2281 assertions) Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 20 skipped, 988 passed (2381 assertions)

View File

@ -0,0 +1,31 @@
TODO Tests\Features\BeforeEachProxiesToTestCallWithTodo - 4 todos
↓ is marked as todo 1
↓ is marked as todo 2
↓ is marked as todo 3
↓ shouldBeMarkedAsTodo
TODO Tests\Features\DatasetsTests - 1 todo
↓ forbids to define tests in Datasets dirs and Datasets.php files
TODO Tests\Features\Describe - 5 todos
↓ todo
↓ todo on hook → should not fail
↓ todo on hook → should run
↓ todo on describe → should not fail
↓ todo on describe → should run
TODO Tests\Features\Todo - 3 todos
↓ something todo later
↓ something todo later chained
↓ something todo later chained and with function body
PASS Tests\CustomTestCase\ChildTest
✓ override method
PASS Tests\CustomTestCase\ExecutedTest
✓ that gets executed
PASS Tests\CustomTestCase\ParentTest
✓ override method
Tests: 13 todos, 3 passed (3 assertions)

View File

@ -2,12 +2,12 @@
use Pest\Expectation; use Pest\Expectation;
test('globals') arch('globals')
->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep']) ->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep'])
->not->toBeUsed() ->not->toBeUsed()
->ignoring(Expectation::class); ->ignoring(Expectation::class);
test('dependencies') arch('dependencies')
->expect('Pest') ->expect('Pest')
->toOnlyUse([ ->toOnlyUse([
'dd', 'dd',
@ -24,7 +24,7 @@ test('dependencies')
'Symfony\Component\Process', 'Symfony\Component\Process',
])->ignoring(['Composer', 'PHPUnit', 'SebastianBergmann']); ])->ignoring(['Composer', 'PHPUnit', 'SebastianBergmann']);
test('contracts') arch('contracts')
->expect('Pest\Contracts') ->expect('Pest\Contracts')
->toOnlyUse([ ->toOnlyUse([
'NunoMaduro\Collision\Contracts', 'NunoMaduro\Collision\Contracts',

View File

@ -1,6 +1,6 @@
<?php <?php
$state = new stdClass(); $state = new stdClass;
beforeEach(function () use ($state) { beforeEach(function () use ($state) {
$this->state = $state; $this->state = $state;

View File

@ -1,6 +1,6 @@
<?php <?php
$foo = new \stdClass(); $foo = new \stdClass;
$foo->bar = 0; $foo->bar = 0;
beforeAll(function () use ($foo) { beforeAll(function () use ($foo) {

View File

@ -7,7 +7,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
it('has plugin')->assertTrue(class_exists(CoveragePlugin::class)); it('has plugin')->assertTrue(class_exists(CoveragePlugin::class));
it('adds coverage if --coverage exist', function () { it('adds coverage if --coverage exist', function () {
$plugin = new CoveragePlugin(new ConsoleOutput()); $plugin = new CoveragePlugin(new ConsoleOutput);
expect($plugin->coverage)->toBeFalse(); expect($plugin->coverage)->toBeFalse();
$arguments = $plugin->handleArguments([]); $arguments = $plugin->handleArguments([]);
@ -20,7 +20,7 @@ it('adds coverage if --coverage exist', function () {
})->skip(! \Pest\Support\Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available'); })->skip(! \Pest\Support\Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available');
it('adds coverage if --min exist', function () { it('adds coverage if --min exist', function () {
$plugin = new CoveragePlugin(new ConsoleOutput()); $plugin = new CoveragePlugin(new ConsoleOutput);
expect($plugin->coverageMin)->toEqual(0.0) expect($plugin->coverageMin)->toEqual(0.0)
->and($plugin->coverage)->toBeFalse(); ->and($plugin->coverage)->toBeFalse();
@ -35,7 +35,7 @@ it('adds coverage if --min exist', function () {
}); });
it('generates coverage based on file input', function () { it('generates coverage based on file input', function () {
expect(Coverage::getMissingCoverage(new class() expect(Coverage::getMissingCoverage(new class
{ {
public function lineCoverageData(): array public function lineCoverageData(): array
{ {

View File

@ -10,9 +10,7 @@ use Tests\Fixtures\Covers\CoversTrait;
$runCounter = 0; $runCounter = 0;
function testCoversFunction() function testCoversFunction() {}
{
}
it('uses the correct PHPUnit attribute for class', function () { it('uses the correct PHPUnit attribute for class', function () {
$attributes = (new ReflectionClass($this))->getAttributes(); $attributes = (new ReflectionClass($this))->getAttributes();

View File

@ -46,7 +46,7 @@ test('it truncates the description', function () {
// it gets tested by the integration test // it gets tested by the integration test
})->with([str_repeat('Fooo', 10)]); })->with([str_repeat('Fooo', 10)]);
$state = new stdClass(); $state = new stdClass;
$state->text = ''; $state->text = '';
$datasets = [[1], [2]]; $datasets = [[1], [2]];
@ -119,7 +119,7 @@ class Bar
} }
$namedDatasets = [ $namedDatasets = [
new Bar(), new Bar,
]; ];
test('lazy named datasets', function ($text) { test('lazy named datasets', function ($text) {
@ -132,12 +132,12 @@ it('creates unique test case names', function (string $name, Plugin $plugin, boo
expect(true)->toBeTrue(); expect(true)->toBeTrue();
$counter++; $counter++;
})->with([ })->with([
['Name 1', new Plugin(), true], ['Name 1', new Plugin, true],
['Name 1', new Plugin(), true], ['Name 1', new Plugin, true],
['Name 1', new Plugin(), false], ['Name 1', new Plugin, false],
['Name 2', new Plugin(), false], ['Name 2', new Plugin, false],
['Name 2', new Plugin(), true], ['Name 2', new Plugin, true],
['Name 1', new Plugin(), true], ['Name 1', new Plugin, true],
]); ]);
it('creates unique test case names - count', function () use (&$counter) { it('creates unique test case names - count', function () use (&$counter) {
@ -230,7 +230,7 @@ test('more than two datasets did the job right', function () use ($state) {
expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324135136145146235236245246'); expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324135136145146235236245246');
}); });
$wrapped_generator_state = new stdClass(); $wrapped_generator_state = new stdClass;
$wrapped_generator_state->text = ''; $wrapped_generator_state->text = '';
$wrapped_generator_function_datasets = [1, 2, 3, 4]; $wrapped_generator_function_datasets = [1, 2, 3, 4];
@ -361,3 +361,23 @@ it('can correctly resolve a bound dataset that returns an array but wants to be
]); ]);
todo('forbids to define tests in Datasets dirs and Datasets.php files'); todo('forbids to define tests in Datasets dirs and Datasets.php files');
dataset('greeting-string', [
'formal' => 'Evening',
'informal' => 'yo',
]);
it('may be used with high order')
->with('greeting-string')
->expect(fn (string $greeting) => $greeting)
->throwsNoExceptions();
dataset('greeting-bound', [
'formal' => fn () => 'Evening',
'informal' => fn () => 'yo',
]);
it('may be used with high order even when bound')
->with('greeting-bound')
->expect(fn (string $greeting) => $greeting)
->throws(InvalidArgumentException::class);

View File

@ -3,7 +3,7 @@
it('gives access the the underlying expectException', function () { it('gives access the the underlying expectException', function () {
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
throw new InvalidArgumentException(); throw new InvalidArgumentException;
}); });
it('catch exceptions', function () { it('catch exceptions', function () {
@ -59,3 +59,37 @@ it('can just define the message if given condition is 1', function () {
it('can just define the code if given condition is 1', function () { it('can just define the code if given condition is 1', function () {
throw new Exception('Something bad happened', 1); throw new Exception('Something bad happened', 1);
})->throwsIf(1, 1); })->throwsIf(1, 1);
it('not catch exceptions if given condition is true', function () {
$this->assertTrue(true);
})->throwsUnless(true, Exception::class);
it('catch exceptions if given condition is false', function () {
throw new Exception('Something bad happened');
})->throwsUnless(function () {
return false;
}, Exception::class);
it('catch exceptions and messages if given condition is false', function () {
throw new Exception('Something bad happened');
})->throwsUnless(false, Exception::class, 'Something bad happened');
it('catch exceptions, messages and code if given condition is false', function () {
throw new Exception('Something bad happened', 1);
})->throwsUnless(false, Exception::class, 'Something bad happened', 1);
it('can just define the message if given condition is false', function () {
throw new Exception('Something bad happened');
})->throwsUnless(false, 'Something bad happened');
it('can just define the code if given condition is false', function () {
throw new Exception('Something bad happened', 1);
})->throwsUnless(false, 1);
it('can just define the message if given condition is 0', function () {
throw new Exception('Something bad happened');
})->throwsUnless(0, 'Something bad happened');
it('can just define the code if given condition is 0', function () {
throw new Exception('Something bad happened', 1);
})->throwsUnless(0, 1);

View File

@ -1,29 +1,29 @@
<?php <?php
it('can access methods', function () { it('can access methods', function () {
expect(new HasMethods()) expect(new HasMethods)
->name()->toBeString()->toEqual('Has Methods'); ->name()->toBeString()->toEqual('Has Methods');
}); });
it('can access multiple methods', function () { it('can access multiple methods', function () {
expect(new HasMethods()) expect(new HasMethods)
->name()->toBeString()->toEqual('Has Methods') ->name()->toBeString()->toEqual('Has Methods')
->quantity()->toBeInt()->toEqual(20); ->quantity()->toBeInt()->toEqual(20);
}); });
it('works with not', function () { it('works with not', function () {
expect(new HasMethods()) expect(new HasMethods)
->name()->not->toEqual('world')->toEqual('Has Methods') ->name()->not->toEqual('world')->toEqual('Has Methods')
->quantity()->toEqual(20)->not()->toEqual('bar')->not->toBeNull; ->quantity()->toEqual(20)->not()->toEqual('bar')->not->toBeNull;
}); });
it('can accept arguments', function () { it('can accept arguments', function () {
expect(new HasMethods()) expect(new HasMethods)
->multiply(5, 4)->toBeInt->toEqual(20); ->multiply(5, 4)->toBeInt->toEqual(20);
}); });
it('works with each', function () { it('works with each', function () {
expect(new HasMethods()) expect(new HasMethods)
->attributes()->toBeArray->each->not()->toBeNull ->attributes()->toBeArray->each->not()->toBeNull
->attributes()->each(function ($attribute) { ->attributes()->each(function ($attribute) {
$attribute->not->toBeNull(); $attribute->not->toBeNull();
@ -31,14 +31,14 @@ it('works with each', function () {
}); });
it('works inside of each', function () { it('works inside of each', function () {
expect(new HasMethods()) expect(new HasMethods)
->books()->each(function ($book) { ->books()->each(function ($book) {
$book->title->not->toBeNull->cost->toBeGreaterThan(19); $book->title->not->toBeNull->cost->toBeGreaterThan(19);
}); });
}); });
it('works with sequence', function () { it('works with sequence', function () {
expect(new HasMethods()) expect(new HasMethods)
->books()->sequence( ->books()->sequence(
function ($book) { function ($book) {
$book->title->toEqual('Foo')->cost->toEqual(20); $book->title->toEqual('Foo')->cost->toEqual(20);
@ -50,7 +50,7 @@ it('works with sequence', function () {
}); });
it('can compose complex expectations', function () { it('can compose complex expectations', function () {
expect(new HasMethods()) expect(new HasMethods)
->toBeObject() ->toBeObject()
->name()->toEqual('Has Methods')->not()->toEqual('bar') ->name()->toEqual('Has Methods')->not()->toEqual('bar')
->quantity()->not->toEqual('world')->toEqual(20)->toBeInt ->quantity()->not->toEqual('world')->toEqual(20)->toBeInt
@ -68,7 +68,7 @@ it('can compose complex expectations', function () {
}); });
it('can handle nested method calls', function () { it('can handle nested method calls', function () {
expect(new HasMethods()) expect(new HasMethods)
->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString() ->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString()
->newInstance()->name()->toEqual('Has Methods')->not->toBeInt ->newInstance()->name()->toEqual('Has Methods')->not->toBeInt
->name()->toEqual('Has Methods') ->name()->toEqual('Has Methods')
@ -76,14 +76,14 @@ it('can handle nested method calls', function () {
}); });
it('works with higher order tests') it('works with higher order tests')
->expect(new HasMethods()) ->expect(new HasMethods)
->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString() ->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString()
->newInstance()->name()->toEqual('Has Methods')->not->toBeArray ->newInstance()->name()->toEqual('Has Methods')->not->toBeArray
->name()->toEqual('Has Methods') ->name()->toEqual('Has Methods')
->books()->each->toBeArray; ->books()->each->toBeArray;
it('can use the scoped method to lock into the given level for expectations', function () { it('can use the scoped method to lock into the given level for expectations', function () {
expect(new HasMethods()) expect(new HasMethods)
->attributes()->scoped(fn ($attributes) => $attributes ->attributes()->scoped(fn ($attributes) => $attributes
->name->toBe('Has Methods') ->name->toBe('Has Methods')
->quantity->toBe(20) ->quantity->toBe(20)
@ -100,7 +100,7 @@ it('can use the scoped method to lock into the given level for expectations', fu
}); });
it('works consistently with the json expectation method', function () { it('works consistently with the json expectation method', function () {
expect(new HasMethods()) expect(new HasMethods)
->jsonString()->json()->id->toBe(1) ->jsonString()->json()->id->toBe(1)
->jsonString()->json()->name->toBe('Has Methods')->toBeString() ->jsonString()->json()->name->toBe('Has Methods')->toBeString()
->jsonString()->json()->quantity->toBe(20)->toBeInt(); ->jsonString()->json()->quantity->toBe(20)->toBeInt();
@ -152,6 +152,6 @@ class HasMethods
public function newInstance() public function newInstance()
{ {
return new static(); return new static;
} }
} }

View File

@ -1,7 +1,7 @@
<?php <?php
it('can access methods and properties', function () { it('can access methods and properties', function () {
expect(new HasMethodsAndProperties()) expect(new HasMethodsAndProperties)
->name->toEqual('Has Methods and Properties')->not()->toEqual('bar') ->name->toEqual('Has Methods and Properties')->not()->toEqual('bar')
->multiply(3, 4)->not->toBeString->toEqual(12) ->multiply(3, 4)->not->toBeString->toEqual(12)
->posts->each(function ($post) { ->posts->each(function ($post) {
@ -19,7 +19,7 @@ it('can access methods and properties', function () {
}); });
it('can handle nested methods and properties', function () { it('can handle nested methods and properties', function () {
expect(new HasMethodsAndProperties()) expect(new HasMethodsAndProperties)
->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt ->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt
->newInstance()->meta->foo->toBeArray() ->newInstance()->meta->foo->toBeArray()
->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5) ->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5)
@ -27,14 +27,14 @@ it('can handle nested methods and properties', function () {
}); });
it('works with higher order tests') it('works with higher order tests')
->expect(new HasMethodsAndProperties()) ->expect(new HasMethodsAndProperties)
->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt ->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt
->newInstance()->meta->foo->toBeArray ->newInstance()->meta->foo->toBeArray
->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5) ->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5)
->newInstance()->books()->toBeArray(); ->newInstance()->books()->toBeArray();
it('can start a new higher order expectation using the and syntax', function () { it('can start a new higher order expectation using the and syntax', function () {
expect(new HasMethodsAndProperties()) expect(new HasMethodsAndProperties)
->toBeInstanceOf(HasMethodsAndProperties::class) ->toBeInstanceOf(HasMethodsAndProperties::class)
->meta->toBeArray ->meta->toBeArray
->and(['foo' => 'bar']) ->and(['foo' => 'bar'])
@ -45,7 +45,7 @@ it('can start a new higher order expectation using the and syntax', function ()
}); });
it('can start a new higher order expectation using the and syntax in higher order tests') it('can start a new higher order expectation using the and syntax in higher order tests')
->expect(new HasMethodsAndProperties()) ->expect(new HasMethodsAndProperties)
->toBeInstanceOf(HasMethodsAndProperties::class) ->toBeInstanceOf(HasMethodsAndProperties::class)
->meta->toBeArray ->meta->toBeArray
->and(['foo' => 'bar']) ->and(['foo' => 'bar'])
@ -53,7 +53,7 @@ it('can start a new higher order expectation using the and syntax in higher orde
->foo->toEqual('bar'); ->foo->toEqual('bar');
it('can start a new higher order expectation using the and syntax without nesting expectations', function () { it('can start a new higher order expectation using the and syntax without nesting expectations', function () {
expect(new HasMethodsAndProperties()) expect(new HasMethodsAndProperties)
->toBeInstanceOf(HasMethodsAndProperties::class) ->toBeInstanceOf(HasMethodsAndProperties::class)
->meta ->meta
->sequence( ->sequence(
@ -101,6 +101,6 @@ class HasMethodsAndProperties
public function newInstance() public function newInstance()
{ {
return new static(); return new static;
} }
} }

View File

@ -53,7 +53,7 @@ it('can compose complex expectations', function () {
}); });
it('works with objects', function () { it('works with objects', function () {
expect(new HasProperties()) expect(new HasProperties)
->name->toEqual('foo')->not->toEqual('world') ->name->toEqual('foo')->not->toEqual('world')
->posts->toHaveCount(2)->each(function ($post) { ->posts->toHaveCount(2)->each(function ($post) {
$post->is_published->toBeTrue(); $post->is_published->toBeTrue();
@ -69,13 +69,13 @@ it('works with objects', function () {
}); });
it('works with nested properties', function () { it('works with nested properties', function () {
expect(new HasProperties()) expect(new HasProperties)
->nested->foo->bar->toBeString()->toEqual('baz') ->nested->foo->bar->toBeString()->toEqual('baz')
->posts->toBeArray()->toHaveCount(2); ->posts->toBeArray()->toHaveCount(2);
}); });
it('works with higher order tests') it('works with higher order tests')
->expect(new HasProperties()) ->expect(new HasProperties)
->nested->foo->bar->toBeString()->toEqual('baz') ->nested->foo->bar->toBeString()->toEqual('baz')
->posts->toBeArray()->toHaveCount(2); ->posts->toBeArray()->toHaveCount(2);

View File

@ -56,7 +56,7 @@ class State
} }
} }
$state = new State(); $state = new State;
/* /*
* Overrides toBe to assert two Characters are the same * Overrides toBe to assert two Characters are the same

View File

@ -5,8 +5,8 @@ use PHPUnit\Framework\ExpectationFailedException;
expect(true)->toBeTrue()->and(false)->toBeFalse(); expect(true)->toBeTrue()->and(false)->toBeFalse();
test('strict comparisons', function () { test('strict comparisons', function () {
$nuno = new stdClass(); $nuno = new stdClass;
$dries = new stdClass(); $dries = new stdClass;
expect($nuno)->toBe($nuno)->not->toBe($dries); expect($nuno)->toBe($nuno)->not->toBe($dries);
}); });

View File

@ -3,8 +3,7 @@
use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () { test('pass', function () {
expect(function () { expect(function () {})->toBeCallable();
})->toBeCallable();
expect(null)->not->toBeCallable(); expect(null)->not->toBeCallable();
}); });

View File

@ -8,14 +8,19 @@ test('passes', function () {
}); });
test('passes with DateTime and DateTimeImmutable', function () { test('passes with DateTime and DateTimeImmutable', function () {
$now = new DateTime(); $now = new DateTime;
$past = (new DateTimeImmutable())->modify('-1 day'); $past = (new DateTimeImmutable)->modify('-1 day');
expect($now)->toBeGreaterThan($past); expect($now)->toBeGreaterThan($past);
expect($past)->not->toBeGreaterThan($now); expect($past)->not->toBeGreaterThan($now);
}); });
test('passes with strings', function () {
expect('b')->toBeGreaterThan('a');
expect('a')->not->toBeGreaterThan('a');
});
test('failures', function () { test('failures', function () {
expect(4)->toBeGreaterThan(4); expect(4)->toBeGreaterThan(4);
})->throws(ExpectationFailedException::class); })->throws(ExpectationFailedException::class);

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